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, 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 cx,
446 );
447
448 let mut block_ids = block_ids.into_iter();
449 for group_state in &mut groups_to_add {
450 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
451 }
452 });
453
454 for ix in group_ixs_to_remove.into_iter().rev() {
455 path_state.diagnostic_groups.remove(ix);
456 }
457 path_state.diagnostic_groups.extend(groups_to_add);
458 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
459 let range_a = &a.primary_diagnostic.range;
460 let range_b = &b.primary_diagnostic.range;
461 range_a
462 .start
463 .cmp(&range_b.start, &snapshot)
464 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
465 });
466
467 if path_state.diagnostic_groups.is_empty() {
468 self.path_states.remove(path_ix);
469 }
470
471 self.editor.update(cx, |editor, cx| {
472 let groups;
473 let mut selections;
474 let new_excerpt_ids_by_selection_id;
475 if was_empty {
476 groups = self.path_states.first()?.diagnostic_groups.as_slice();
477 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
478 selections = vec![Selection {
479 id: 0,
480 start: 0,
481 end: 0,
482 reversed: false,
483 goal: SelectionGoal::None,
484 }];
485 } else {
486 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
487 new_excerpt_ids_by_selection_id =
488 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
489 selections = editor.selections.all::<usize>(cx);
490 }
491
492 // If any selection has lost its position, move it to start of the next primary diagnostic.
493 let snapshot = editor.snapshot(cx);
494 for selection in &mut selections {
495 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
496 let group_ix = match groups.binary_search_by(|probe| {
497 probe
498 .excerpts
499 .last()
500 .unwrap()
501 .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
502 }) {
503 Ok(ix) | Err(ix) => ix,
504 };
505 if let Some(group) = groups.get(group_ix) {
506 let offset = excerpts_snapshot
507 .anchor_in_excerpt(
508 group.excerpts[group.primary_excerpt_ix].clone(),
509 group.primary_diagnostic.range.start,
510 )
511 .to_offset(&excerpts_snapshot);
512 selection.start = offset;
513 selection.end = offset;
514 }
515 }
516 }
517 editor.change_selections(None, cx, |s| {
518 s.select(selections);
519 });
520 Some(())
521 });
522
523 if self.path_states.is_empty() {
524 if self.editor.is_focused(cx) {
525 cx.focus_self();
526 }
527 } else if cx.handle().is_focused(cx) {
528 cx.focus(&self.editor);
529 }
530 cx.notify();
531 }
532
533 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
534 self.summary = self.project.read(cx).diagnostic_summary(cx);
535 cx.emit(Event::TitleChanged);
536 }
537}
538
539impl Item for ProjectDiagnosticsEditor {
540 fn tab_content<T: View>(
541 &self,
542 _detail: Option<usize>,
543 style: &theme::Tab,
544 cx: &AppContext,
545 ) -> AnyElement<T> {
546 render_summary(
547 &self.summary,
548 &style.label.text,
549 &theme::current(cx).project_diagnostics,
550 )
551 }
552
553 fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
554 self.editor.for_each_project_item(cx, f)
555 }
556
557 fn is_singleton(&self, _: &AppContext) -> bool {
558 false
559 }
560
561 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
562 self.editor
563 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
564 }
565
566 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
567 self.editor
568 .update(cx, |editor, cx| editor.navigate(data, cx))
569 }
570
571 fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
572 Some("Project Diagnostics".into())
573 }
574
575 fn is_dirty(&self, cx: &AppContext) -> bool {
576 self.excerpts.read(cx).is_dirty(cx)
577 }
578
579 fn has_conflict(&self, cx: &AppContext) -> bool {
580 self.excerpts.read(cx).has_conflict(cx)
581 }
582
583 fn can_save(&self, _: &AppContext) -> bool {
584 true
585 }
586
587 fn save(
588 &mut self,
589 project: ModelHandle<Project>,
590 cx: &mut ViewContext<Self>,
591 ) -> Task<Result<()>> {
592 self.editor.save(project, cx)
593 }
594
595 fn reload(
596 &mut self,
597 project: ModelHandle<Project>,
598 cx: &mut ViewContext<Self>,
599 ) -> Task<Result<()>> {
600 self.editor.reload(project, cx)
601 }
602
603 fn save_as(
604 &mut self,
605 _: ModelHandle<Project>,
606 _: PathBuf,
607 _: &mut ViewContext<Self>,
608 ) -> Task<Result<()>> {
609 unreachable!()
610 }
611
612 fn git_diff_recalc(
613 &mut self,
614 project: ModelHandle<Project>,
615 cx: &mut ViewContext<Self>,
616 ) -> Task<Result<()>> {
617 self.editor
618 .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
619 }
620
621 fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
622 Editor::to_item_events(event)
623 }
624
625 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
626 self.editor.update(cx, |editor, _| {
627 editor.set_nav_history(Some(nav_history));
628 });
629 }
630
631 fn clone_on_split(
632 &self,
633 _workspace_id: workspace::WorkspaceId,
634 cx: &mut ViewContext<Self>,
635 ) -> Option<Self>
636 where
637 Self: Sized,
638 {
639 Some(ProjectDiagnosticsEditor::new(
640 self.project.clone(),
641 self.workspace.clone(),
642 cx,
643 ))
644 }
645
646 fn act_as_type<'a>(
647 &'a self,
648 type_id: TypeId,
649 self_handle: &'a ViewHandle<Self>,
650 _: &'a AppContext,
651 ) -> Option<&AnyViewHandle> {
652 if type_id == TypeId::of::<Self>() {
653 Some(self_handle)
654 } else if type_id == TypeId::of::<Editor>() {
655 Some(&self.editor)
656 } else {
657 None
658 }
659 }
660
661 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
662 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
663 }
664
665 fn serialized_item_kind() -> Option<&'static str> {
666 Some("diagnostics")
667 }
668
669 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
670 self.editor.breadcrumbs(theme, cx)
671 }
672
673 fn breadcrumb_location(&self) -> ToolbarItemLocation {
674 ToolbarItemLocation::PrimaryLeft { flex: None }
675 }
676
677 fn deserialize(
678 project: ModelHandle<Project>,
679 workspace: WeakViewHandle<Workspace>,
680 _workspace_id: workspace::WorkspaceId,
681 _item_id: workspace::ItemId,
682 cx: &mut ViewContext<Pane>,
683 ) -> Task<Result<ViewHandle<Self>>> {
684 Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
685 }
686}
687
688fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
689 let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
690 Arc::new(move |cx| {
691 let settings = settings::get::<ThemeSettings>(cx);
692 let theme = &settings.theme.editor;
693 let style = theme.diagnostic_header.clone();
694 let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
695 let icon_width = cx.em_width * style.icon_width_factor;
696 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
697 Svg::new("icons/circle_x_mark_12.svg")
698 .with_color(theme.error_diagnostic.message.text.color)
699 } else {
700 Svg::new("icons/triangle_exclamation_12.svg")
701 .with_color(theme.warning_diagnostic.message.text.color)
702 };
703
704 Flex::row()
705 .with_child(
706 icon.constrained()
707 .with_width(icon_width)
708 .aligned()
709 .contained()
710 .with_margin_right(cx.gutter_padding),
711 )
712 .with_children(diagnostic.source.as_ref().map(|source| {
713 Label::new(
714 format!("{source}: "),
715 style.source.label.clone().with_font_size(font_size),
716 )
717 .contained()
718 .with_style(style.message.container)
719 .aligned()
720 }))
721 .with_child(
722 Label::new(
723 message.clone(),
724 style.message.label.clone().with_font_size(font_size),
725 )
726 .with_highlights(highlights.clone())
727 .contained()
728 .with_style(style.message.container)
729 .aligned(),
730 )
731 .with_children(diagnostic.code.clone().map(|code| {
732 Label::new(code, style.code.text.clone().with_font_size(font_size))
733 .contained()
734 .with_style(style.code.container)
735 .aligned()
736 }))
737 .contained()
738 .with_style(style.container)
739 .with_padding_left(cx.gutter_padding)
740 .with_padding_right(cx.gutter_padding)
741 .expanded()
742 .into_any_named("diagnostic header")
743 })
744}
745
746pub(crate) fn render_summary<T: View>(
747 summary: &DiagnosticSummary,
748 text_style: &TextStyle,
749 theme: &theme::ProjectDiagnostics,
750) -> AnyElement<T> {
751 if summary.error_count == 0 && summary.warning_count == 0 {
752 Label::new("No problems", text_style.clone()).into_any()
753 } else {
754 let icon_width = theme.tab_icon_width;
755 let icon_spacing = theme.tab_icon_spacing;
756 let summary_spacing = theme.tab_summary_spacing;
757 Flex::row()
758 .with_child(
759 Svg::new("icons/circle_x_mark_12.svg")
760 .with_color(text_style.color)
761 .constrained()
762 .with_width(icon_width)
763 .aligned()
764 .contained()
765 .with_margin_right(icon_spacing),
766 )
767 .with_child(
768 Label::new(
769 summary.error_count.to_string(),
770 LabelStyle {
771 text: text_style.clone(),
772 highlight_text: None,
773 },
774 )
775 .aligned(),
776 )
777 .with_child(
778 Svg::new("icons/triangle_exclamation_12.svg")
779 .with_color(text_style.color)
780 .constrained()
781 .with_width(icon_width)
782 .aligned()
783 .contained()
784 .with_margin_left(summary_spacing)
785 .with_margin_right(icon_spacing),
786 )
787 .with_child(
788 Label::new(
789 summary.warning_count.to_string(),
790 LabelStyle {
791 text: text_style.clone(),
792 highlight_text: None,
793 },
794 )
795 .aligned(),
796 )
797 .into_any()
798 }
799}
800
801fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
802 lhs: &DiagnosticEntry<L>,
803 rhs: &DiagnosticEntry<R>,
804 snapshot: &language::BufferSnapshot,
805) -> Ordering {
806 lhs.range
807 .start
808 .to_offset(snapshot)
809 .cmp(&rhs.range.start.to_offset(snapshot))
810 .then_with(|| {
811 lhs.range
812 .end
813 .to_offset(snapshot)
814 .cmp(&rhs.range.end.to_offset(snapshot))
815 })
816 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use editor::{
823 display_map::{BlockContext, TransformBlock},
824 DisplayPoint,
825 };
826 use gpui::{TestAppContext, WindowContext};
827 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
828 use project::FakeFs;
829 use serde_json::json;
830 use settings::SettingsStore;
831 use unindent::Unindent as _;
832
833 #[gpui::test]
834 async fn test_diagnostics(cx: &mut TestAppContext) {
835 init_test(cx);
836
837 let fs = FakeFs::new(cx.background());
838 fs.insert_tree(
839 "/test",
840 json!({
841 "consts.rs": "
842 const a: i32 = 'a';
843 const b: i32 = c;
844 "
845 .unindent(),
846
847 "main.rs": "
848 fn main() {
849 let x = vec![];
850 let y = vec![];
851 a(x);
852 b(y);
853 // comment 1
854 // comment 2
855 c(y);
856 d(x);
857 }
858 "
859 .unindent(),
860 }),
861 )
862 .await;
863
864 let language_server_id = LanguageServerId(0);
865 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
866 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
867
868 // Create some diagnostics
869 project.update(cx, |project, cx| {
870 project
871 .update_diagnostic_entries(
872 language_server_id,
873 PathBuf::from("/test/main.rs"),
874 None,
875 vec![
876 DiagnosticEntry {
877 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
878 diagnostic: Diagnostic {
879 message:
880 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
881 .to_string(),
882 severity: DiagnosticSeverity::INFORMATION,
883 is_primary: false,
884 is_disk_based: true,
885 group_id: 1,
886 ..Default::default()
887 },
888 },
889 DiagnosticEntry {
890 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
891 diagnostic: Diagnostic {
892 message:
893 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
894 .to_string(),
895 severity: DiagnosticSeverity::INFORMATION,
896 is_primary: false,
897 is_disk_based: true,
898 group_id: 0,
899 ..Default::default()
900 },
901 },
902 DiagnosticEntry {
903 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
904 diagnostic: Diagnostic {
905 message: "value moved here".to_string(),
906 severity: DiagnosticSeverity::INFORMATION,
907 is_primary: false,
908 is_disk_based: true,
909 group_id: 1,
910 ..Default::default()
911 },
912 },
913 DiagnosticEntry {
914 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
915 diagnostic: Diagnostic {
916 message: "value moved here".to_string(),
917 severity: DiagnosticSeverity::INFORMATION,
918 is_primary: false,
919 is_disk_based: true,
920 group_id: 0,
921 ..Default::default()
922 },
923 },
924 DiagnosticEntry {
925 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
926 diagnostic: Diagnostic {
927 message: "use of moved value\nvalue used here after move".to_string(),
928 severity: DiagnosticSeverity::ERROR,
929 is_primary: true,
930 is_disk_based: true,
931 group_id: 0,
932 ..Default::default()
933 },
934 },
935 DiagnosticEntry {
936 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
937 diagnostic: Diagnostic {
938 message: "use of moved value\nvalue used here after move".to_string(),
939 severity: DiagnosticSeverity::ERROR,
940 is_primary: true,
941 is_disk_based: true,
942 group_id: 1,
943 ..Default::default()
944 },
945 },
946 ],
947 cx,
948 )
949 .unwrap();
950 });
951
952 // Open the project diagnostics view while there are already diagnostics.
953 let view = cx.add_view(window_id, |cx| {
954 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
955 });
956
957 view.next_notification(cx).await;
958 view.update(cx, |view, cx| {
959 assert_eq!(
960 editor_blocks(&view.editor, cx),
961 [
962 (0, "path header block".into()),
963 (2, "diagnostic header".into()),
964 (15, "collapsed context".into()),
965 (16, "diagnostic header".into()),
966 (25, "collapsed context".into()),
967 ]
968 );
969 assert_eq!(
970 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
971 concat!(
972 //
973 // main.rs
974 //
975 "\n", // filename
976 "\n", // padding
977 // diagnostic group 1
978 "\n", // primary message
979 "\n", // padding
980 " let x = vec![];\n",
981 " let y = vec![];\n",
982 "\n", // supporting diagnostic
983 " a(x);\n",
984 " b(y);\n",
985 "\n", // supporting diagnostic
986 " // comment 1\n",
987 " // comment 2\n",
988 " c(y);\n",
989 "\n", // supporting diagnostic
990 " d(x);\n",
991 "\n", // context ellipsis
992 // diagnostic group 2
993 "\n", // primary message
994 "\n", // padding
995 "fn main() {\n",
996 " let x = vec![];\n",
997 "\n", // supporting diagnostic
998 " let y = vec![];\n",
999 " a(x);\n",
1000 "\n", // supporting diagnostic
1001 " b(y);\n",
1002 "\n", // context ellipsis
1003 " c(y);\n",
1004 " d(x);\n",
1005 "\n", // supporting diagnostic
1006 "}"
1007 )
1008 );
1009
1010 // Cursor is at the first diagnostic
1011 view.editor.update(cx, |editor, cx| {
1012 assert_eq!(
1013 editor.selections.display_ranges(cx),
1014 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1015 );
1016 });
1017 });
1018
1019 // Diagnostics are added for another earlier path.
1020 project.update(cx, |project, cx| {
1021 project.disk_based_diagnostics_started(language_server_id, cx);
1022 project
1023 .update_diagnostic_entries(
1024 language_server_id,
1025 PathBuf::from("/test/consts.rs"),
1026 None,
1027 vec![DiagnosticEntry {
1028 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1029 diagnostic: Diagnostic {
1030 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1031 severity: DiagnosticSeverity::ERROR,
1032 is_primary: true,
1033 is_disk_based: true,
1034 group_id: 0,
1035 ..Default::default()
1036 },
1037 }],
1038 cx,
1039 )
1040 .unwrap();
1041 project.disk_based_diagnostics_finished(language_server_id, cx);
1042 });
1043
1044 view.next_notification(cx).await;
1045 view.update(cx, |view, cx| {
1046 assert_eq!(
1047 editor_blocks(&view.editor, cx),
1048 [
1049 (0, "path header block".into()),
1050 (2, "diagnostic header".into()),
1051 (7, "path header block".into()),
1052 (9, "diagnostic header".into()),
1053 (22, "collapsed context".into()),
1054 (23, "diagnostic header".into()),
1055 (32, "collapsed context".into()),
1056 ]
1057 );
1058 assert_eq!(
1059 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1060 concat!(
1061 //
1062 // consts.rs
1063 //
1064 "\n", // filename
1065 "\n", // padding
1066 // diagnostic group 1
1067 "\n", // primary message
1068 "\n", // padding
1069 "const a: i32 = 'a';\n",
1070 "\n", // supporting diagnostic
1071 "const b: i32 = c;\n",
1072 //
1073 // main.rs
1074 //
1075 "\n", // filename
1076 "\n", // padding
1077 // diagnostic group 1
1078 "\n", // primary message
1079 "\n", // padding
1080 " let x = vec![];\n",
1081 " let y = vec![];\n",
1082 "\n", // supporting diagnostic
1083 " a(x);\n",
1084 " b(y);\n",
1085 "\n", // supporting diagnostic
1086 " // comment 1\n",
1087 " // comment 2\n",
1088 " c(y);\n",
1089 "\n", // supporting diagnostic
1090 " d(x);\n",
1091 "\n", // collapsed context
1092 // diagnostic group 2
1093 "\n", // primary message
1094 "\n", // filename
1095 "fn main() {\n",
1096 " let x = vec![];\n",
1097 "\n", // supporting diagnostic
1098 " let y = vec![];\n",
1099 " a(x);\n",
1100 "\n", // supporting diagnostic
1101 " b(y);\n",
1102 "\n", // context ellipsis
1103 " c(y);\n",
1104 " d(x);\n",
1105 "\n", // supporting diagnostic
1106 "}"
1107 )
1108 );
1109
1110 // Cursor keeps its position.
1111 view.editor.update(cx, |editor, cx| {
1112 assert_eq!(
1113 editor.selections.display_ranges(cx),
1114 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1115 );
1116 });
1117 });
1118
1119 // Diagnostics are added to the first path
1120 project.update(cx, |project, cx| {
1121 project.disk_based_diagnostics_started(language_server_id, cx);
1122 project
1123 .update_diagnostic_entries(
1124 language_server_id,
1125 PathBuf::from("/test/consts.rs"),
1126 None,
1127 vec![
1128 DiagnosticEntry {
1129 range: Unclipped(PointUtf16::new(0, 15))
1130 ..Unclipped(PointUtf16::new(0, 15)),
1131 diagnostic: Diagnostic {
1132 message: "mismatched types\nexpected `usize`, found `char`"
1133 .to_string(),
1134 severity: DiagnosticSeverity::ERROR,
1135 is_primary: true,
1136 is_disk_based: true,
1137 group_id: 0,
1138 ..Default::default()
1139 },
1140 },
1141 DiagnosticEntry {
1142 range: Unclipped(PointUtf16::new(1, 15))
1143 ..Unclipped(PointUtf16::new(1, 15)),
1144 diagnostic: Diagnostic {
1145 message: "unresolved name `c`".to_string(),
1146 severity: DiagnosticSeverity::ERROR,
1147 is_primary: true,
1148 is_disk_based: true,
1149 group_id: 1,
1150 ..Default::default()
1151 },
1152 },
1153 ],
1154 cx,
1155 )
1156 .unwrap();
1157 project.disk_based_diagnostics_finished(language_server_id, cx);
1158 });
1159
1160 view.next_notification(cx).await;
1161 view.update(cx, |view, cx| {
1162 assert_eq!(
1163 editor_blocks(&view.editor, cx),
1164 [
1165 (0, "path header block".into()),
1166 (2, "diagnostic header".into()),
1167 (7, "collapsed context".into()),
1168 (8, "diagnostic header".into()),
1169 (13, "path header block".into()),
1170 (15, "diagnostic header".into()),
1171 (28, "collapsed context".into()),
1172 (29, "diagnostic header".into()),
1173 (38, "collapsed context".into()),
1174 ]
1175 );
1176 assert_eq!(
1177 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1178 concat!(
1179 //
1180 // consts.rs
1181 //
1182 "\n", // filename
1183 "\n", // padding
1184 // diagnostic group 1
1185 "\n", // primary message
1186 "\n", // padding
1187 "const a: i32 = 'a';\n",
1188 "\n", // supporting diagnostic
1189 "const b: i32 = c;\n",
1190 "\n", // context ellipsis
1191 // diagnostic group 2
1192 "\n", // primary message
1193 "\n", // padding
1194 "const a: i32 = 'a';\n",
1195 "const b: i32 = c;\n",
1196 "\n", // supporting diagnostic
1197 //
1198 // main.rs
1199 //
1200 "\n", // filename
1201 "\n", // padding
1202 // diagnostic group 1
1203 "\n", // primary message
1204 "\n", // padding
1205 " let x = vec![];\n",
1206 " let y = vec![];\n",
1207 "\n", // supporting diagnostic
1208 " a(x);\n",
1209 " b(y);\n",
1210 "\n", // supporting diagnostic
1211 " // comment 1\n",
1212 " // comment 2\n",
1213 " c(y);\n",
1214 "\n", // supporting diagnostic
1215 " d(x);\n",
1216 "\n", // context ellipsis
1217 // diagnostic group 2
1218 "\n", // primary message
1219 "\n", // filename
1220 "fn main() {\n",
1221 " let x = vec![];\n",
1222 "\n", // supporting diagnostic
1223 " let y = vec![];\n",
1224 " a(x);\n",
1225 "\n", // supporting diagnostic
1226 " b(y);\n",
1227 "\n", // context ellipsis
1228 " c(y);\n",
1229 " d(x);\n",
1230 "\n", // supporting diagnostic
1231 "}"
1232 )
1233 );
1234 });
1235 }
1236
1237 #[gpui::test]
1238 async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1239 init_test(cx);
1240
1241 let fs = FakeFs::new(cx.background());
1242 fs.insert_tree(
1243 "/test",
1244 json!({
1245 "main.js": "
1246 a();
1247 b();
1248 c();
1249 d();
1250 e();
1251 ".unindent()
1252 }),
1253 )
1254 .await;
1255
1256 let server_id_1 = LanguageServerId(100);
1257 let server_id_2 = LanguageServerId(101);
1258 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1259 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1260
1261 let view = cx.add_view(window_id, |cx| {
1262 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1263 });
1264
1265 // Two language servers start updating diagnostics
1266 project.update(cx, |project, cx| {
1267 project.disk_based_diagnostics_started(server_id_1, cx);
1268 project.disk_based_diagnostics_started(server_id_2, cx);
1269 project
1270 .update_diagnostic_entries(
1271 server_id_1,
1272 PathBuf::from("/test/main.js"),
1273 None,
1274 vec![DiagnosticEntry {
1275 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1276 diagnostic: Diagnostic {
1277 message: "error 1".to_string(),
1278 severity: DiagnosticSeverity::WARNING,
1279 is_primary: true,
1280 is_disk_based: true,
1281 group_id: 1,
1282 ..Default::default()
1283 },
1284 }],
1285 cx,
1286 )
1287 .unwrap();
1288 project
1289 .update_diagnostic_entries(
1290 server_id_2,
1291 PathBuf::from("/test/main.js"),
1292 None,
1293 vec![DiagnosticEntry {
1294 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1295 diagnostic: Diagnostic {
1296 message: "warning 1".to_string(),
1297 severity: DiagnosticSeverity::ERROR,
1298 is_primary: true,
1299 is_disk_based: true,
1300 group_id: 2,
1301 ..Default::default()
1302 },
1303 }],
1304 cx,
1305 )
1306 .unwrap();
1307 });
1308
1309 // The first language server finishes
1310 project.update(cx, |project, cx| {
1311 project.disk_based_diagnostics_finished(server_id_1, cx);
1312 });
1313
1314 // Only the first language server's diagnostics are shown.
1315 cx.foreground().run_until_parked();
1316 view.update(cx, |view, cx| {
1317 assert_eq!(
1318 editor_blocks(&view.editor, cx),
1319 [
1320 (0, "path header block".into()),
1321 (2, "diagnostic header".into()),
1322 ]
1323 );
1324 assert_eq!(
1325 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1326 concat!(
1327 "\n", // filename
1328 "\n", // padding
1329 // diagnostic group 1
1330 "\n", // primary message
1331 "\n", // padding
1332 "a();\n", //
1333 "b();",
1334 )
1335 );
1336 });
1337
1338 // The second language server finishes
1339 project.update(cx, |project, cx| {
1340 project.disk_based_diagnostics_finished(server_id_2, cx);
1341 });
1342
1343 // Both language server's diagnostics are shown.
1344 cx.foreground().run_until_parked();
1345 view.update(cx, |view, cx| {
1346 assert_eq!(
1347 editor_blocks(&view.editor, cx),
1348 [
1349 (0, "path header block".into()),
1350 (2, "diagnostic header".into()),
1351 (6, "collapsed context".into()),
1352 (7, "diagnostic header".into()),
1353 ]
1354 );
1355 assert_eq!(
1356 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1357 concat!(
1358 "\n", // filename
1359 "\n", // padding
1360 // diagnostic group 1
1361 "\n", // primary message
1362 "\n", // padding
1363 "a();\n", // location
1364 "b();\n", //
1365 "\n", // collapsed context
1366 // diagnostic group 2
1367 "\n", // primary message
1368 "\n", // padding
1369 "a();\n", // context
1370 "b();\n", //
1371 "c();", // context
1372 )
1373 );
1374 });
1375
1376 // Both language servers start updating diagnostics, and the first server finishes.
1377 project.update(cx, |project, cx| {
1378 project.disk_based_diagnostics_started(server_id_1, cx);
1379 project.disk_based_diagnostics_started(server_id_2, cx);
1380 project
1381 .update_diagnostic_entries(
1382 server_id_1,
1383 PathBuf::from("/test/main.js"),
1384 None,
1385 vec![DiagnosticEntry {
1386 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1387 diagnostic: Diagnostic {
1388 message: "warning 2".to_string(),
1389 severity: DiagnosticSeverity::WARNING,
1390 is_primary: true,
1391 is_disk_based: true,
1392 group_id: 1,
1393 ..Default::default()
1394 },
1395 }],
1396 cx,
1397 )
1398 .unwrap();
1399 project
1400 .update_diagnostic_entries(
1401 server_id_2,
1402 PathBuf::from("/test/main.rs"),
1403 None,
1404 vec![],
1405 cx,
1406 )
1407 .unwrap();
1408 project.disk_based_diagnostics_finished(server_id_1, cx);
1409 });
1410
1411 // Only the first language server's diagnostics are updated.
1412 cx.foreground().run_until_parked();
1413 view.update(cx, |view, cx| {
1414 assert_eq!(
1415 editor_blocks(&view.editor, cx),
1416 [
1417 (0, "path header block".into()),
1418 (2, "diagnostic header".into()),
1419 (7, "collapsed context".into()),
1420 (8, "diagnostic header".into()),
1421 ]
1422 );
1423 assert_eq!(
1424 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1425 concat!(
1426 "\n", // filename
1427 "\n", // padding
1428 // diagnostic group 1
1429 "\n", // primary message
1430 "\n", // padding
1431 "a();\n", // location
1432 "b();\n", //
1433 "c();\n", // context
1434 "\n", // collapsed context
1435 // diagnostic group 2
1436 "\n", // primary message
1437 "\n", // padding
1438 "b();\n", // context
1439 "c();\n", //
1440 "d();", // context
1441 )
1442 );
1443 });
1444
1445 // The second language server finishes.
1446 project.update(cx, |project, cx| {
1447 project
1448 .update_diagnostic_entries(
1449 server_id_2,
1450 PathBuf::from("/test/main.js"),
1451 None,
1452 vec![DiagnosticEntry {
1453 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1454 diagnostic: Diagnostic {
1455 message: "warning 2".to_string(),
1456 severity: DiagnosticSeverity::WARNING,
1457 is_primary: true,
1458 is_disk_based: true,
1459 group_id: 1,
1460 ..Default::default()
1461 },
1462 }],
1463 cx,
1464 )
1465 .unwrap();
1466 project.disk_based_diagnostics_finished(server_id_2, cx);
1467 });
1468
1469 // Both language servers' diagnostics are updated.
1470 cx.foreground().run_until_parked();
1471 view.update(cx, |view, cx| {
1472 assert_eq!(
1473 editor_blocks(&view.editor, cx),
1474 [
1475 (0, "path header block".into()),
1476 (2, "diagnostic header".into()),
1477 (7, "collapsed context".into()),
1478 (8, "diagnostic header".into()),
1479 ]
1480 );
1481 assert_eq!(
1482 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1483 concat!(
1484 "\n", // filename
1485 "\n", // padding
1486 // diagnostic group 1
1487 "\n", // primary message
1488 "\n", // padding
1489 "b();\n", // location
1490 "c();\n", //
1491 "d();\n", // context
1492 "\n", // collapsed context
1493 // diagnostic group 2
1494 "\n", // primary message
1495 "\n", // padding
1496 "c();\n", // context
1497 "d();\n", //
1498 "e();", // context
1499 )
1500 );
1501 });
1502 }
1503
1504 fn init_test(cx: &mut TestAppContext) {
1505 cx.update(|cx| {
1506 cx.set_global(SettingsStore::test(cx));
1507 theme::init((), cx);
1508 language::init(cx);
1509 client::init_settings(cx);
1510 workspace::init_settings(cx);
1511 });
1512 }
1513
1514 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1515 editor.update(cx, |editor, cx| {
1516 let snapshot = editor.snapshot(cx);
1517 snapshot
1518 .blocks_in_range(0..snapshot.max_point().row())
1519 .filter_map(|(row, block)| {
1520 let name = match block {
1521 TransformBlock::Custom(block) => block
1522 .render(&mut BlockContext {
1523 view_context: cx,
1524 anchor_x: 0.,
1525 scroll_x: 0.,
1526 gutter_padding: 0.,
1527 gutter_width: 0.,
1528 line_height: 0.,
1529 em_width: 0.,
1530 })
1531 .name()?
1532 .to_string(),
1533 TransformBlock::ExcerptHeader {
1534 starts_new_buffer, ..
1535 } => {
1536 if *starts_new_buffer {
1537 "path header block".to_string()
1538 } else {
1539 "collapsed context".to_string()
1540 }
1541 }
1542 };
1543
1544 Some((row, name))
1545 })
1546 .collect()
1547 })
1548 }
1549}