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 dbg!("Focus in");
109 if dbg!(cx.is_self_focused()) && dbg!(!self.path_states.is_empty()) {
110 dbg!(cx.focus(&self.editor));
111 }
112 }
113
114 fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
115 let project = self.project.read(cx);
116 json!({
117 "project": json!({
118 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
119 "summary": project.diagnostic_summary(cx),
120 }),
121 "summary": self.summary,
122 "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
123 (path.path.to_string_lossy(), server_id.0)
124 ).collect::<Vec<_>>(),
125 "paths_states": self.path_states.iter().map(|state|
126 json!({
127 "path": state.path.path.to_string_lossy(),
128 "groups": state.diagnostic_groups.iter().map(|group|
129 json!({
130 "block_count": group.blocks.len(),
131 "excerpt_count": group.excerpts.len(),
132 })
133 ).collect::<Vec<_>>(),
134 })
135 ).collect::<Vec<_>>(),
136 })
137 }
138}
139
140impl ProjectDiagnosticsEditor {
141 fn new(
142 project_handle: ModelHandle<Project>,
143 workspace: WeakViewHandle<Workspace>,
144 cx: &mut ViewContext<Self>,
145 ) -> Self {
146 cx.subscribe(&project_handle, |this, _, event, cx| match event {
147 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
148 this.update_excerpts(Some(*language_server_id), cx);
149 this.update_title(cx);
150 }
151 project::Event::DiagnosticsUpdated {
152 language_server_id,
153 path,
154 } => {
155 this.paths_to_update
156 .insert((path.clone(), *language_server_id));
157 }
158 _ => {}
159 })
160 .detach();
161
162 let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
163 let editor = cx.add_view(|cx| {
164 let mut editor =
165 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
166 editor.set_vertical_scroll_margin(5, cx);
167 editor
168 });
169 cx.subscribe(&editor, |this, _, event, cx| {
170 cx.emit(event.clone());
171 if event == &editor::Event::Focused && this.path_states.is_empty() {
172 cx.focus_self()
173 }
174 })
175 .detach();
176
177 let project = project_handle.read(cx);
178 let paths_to_update = project
179 .diagnostic_summaries(cx)
180 .map(|(path, server_id, _)| (path, server_id))
181 .collect();
182 let summary = project.diagnostic_summary(cx);
183 let mut this = Self {
184 project: project_handle,
185 summary,
186 workspace,
187 excerpts,
188 editor,
189 path_states: Default::default(),
190 paths_to_update,
191 };
192 this.update_excerpts(None, cx);
193 this
194 }
195
196 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
197 if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
198 workspace.activate_item(&existing, cx);
199 } else {
200 let workspace_handle = cx.weak_handle();
201 let diagnostics = cx.add_view(|cx| {
202 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
203 });
204 workspace.add_item(Box::new(diagnostics), cx);
205 }
206 }
207
208 fn update_excerpts(
209 &mut self,
210 language_server_id: Option<LanguageServerId>,
211 cx: &mut ViewContext<Self>,
212 ) {
213 let mut paths = Vec::new();
214 self.paths_to_update.retain(|(path, server_id)| {
215 if language_server_id
216 .map_or(true, |language_server_id| language_server_id == *server_id)
217 {
218 paths.push(path.clone());
219 false
220 } else {
221 true
222 }
223 });
224 let project = self.project.clone();
225 cx.spawn(|this, mut cx| {
226 async move {
227 for path in paths {
228 let buffer = project
229 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
230 .await?;
231 this.update(&mut cx, |this, cx| {
232 this.populate_excerpts(path, language_server_id, buffer, cx)
233 })?;
234 }
235 Result::<_, anyhow::Error>::Ok(())
236 }
237 .log_err()
238 })
239 .detach();
240 }
241
242 fn populate_excerpts(
243 &mut self,
244 path: ProjectPath,
245 language_server_id: Option<LanguageServerId>,
246 buffer: ModelHandle<Buffer>,
247 cx: &mut ViewContext<Self>,
248 ) {
249 let was_empty = self.path_states.is_empty();
250 let snapshot = buffer.read(cx).snapshot();
251 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
252 Ok(ix) => ix,
253 Err(ix) => {
254 self.path_states.insert(
255 ix,
256 PathState {
257 path: path.clone(),
258 diagnostic_groups: Default::default(),
259 },
260 );
261 ix
262 }
263 };
264
265 let mut prev_excerpt_id = if path_ix > 0 {
266 let prev_path_last_group = &self.path_states[path_ix - 1]
267 .diagnostic_groups
268 .last()
269 .unwrap();
270 prev_path_last_group.excerpts.last().unwrap().clone()
271 } else {
272 ExcerptId::min()
273 };
274
275 let path_state = &mut self.path_states[path_ix];
276 let mut groups_to_add = Vec::new();
277 let mut group_ixs_to_remove = Vec::new();
278 let mut blocks_to_add = Vec::new();
279 let mut blocks_to_remove = HashSet::default();
280 let mut first_excerpt_id = None;
281 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
282 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
283 let mut new_groups = snapshot
284 .diagnostic_groups(language_server_id)
285 .into_iter()
286 .filter(|(_, group)| {
287 group.entries[group.primary_ix].diagnostic.severity
288 <= DiagnosticSeverity::WARNING
289 })
290 .peekable();
291 loop {
292 let mut to_insert = None;
293 let mut to_remove = None;
294 let mut to_keep = None;
295 match (old_groups.peek(), new_groups.peek()) {
296 (None, None) => break,
297 (None, Some(_)) => to_insert = new_groups.next(),
298 (Some((_, old_group)), None) => {
299 if language_server_id.map_or(true, |id| id == old_group.language_server_id)
300 {
301 to_remove = old_groups.next();
302 } else {
303 to_keep = old_groups.next();
304 }
305 }
306 (Some((_, old_group)), Some((_, new_group))) => {
307 let old_primary = &old_group.primary_diagnostic;
308 let new_primary = &new_group.entries[new_group.primary_ix];
309 match compare_diagnostics(old_primary, new_primary, &snapshot) {
310 Ordering::Less => {
311 if language_server_id
312 .map_or(true, |id| id == old_group.language_server_id)
313 {
314 to_remove = old_groups.next();
315 } else {
316 to_keep = old_groups.next();
317 }
318 }
319 Ordering::Equal => {
320 to_keep = old_groups.next();
321 new_groups.next();
322 }
323 Ordering::Greater => to_insert = new_groups.next(),
324 }
325 }
326 }
327
328 if let Some((language_server_id, group)) = to_insert {
329 let mut group_state = DiagnosticGroupState {
330 language_server_id,
331 primary_diagnostic: group.entries[group.primary_ix].clone(),
332 primary_excerpt_ix: 0,
333 excerpts: Default::default(),
334 blocks: Default::default(),
335 block_count: 0,
336 };
337 let mut pending_range: Option<(Range<Point>, usize)> = None;
338 let mut is_first_excerpt_for_group = true;
339 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
340 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
341 if let Some((range, start_ix)) = &mut pending_range {
342 if let Some(entry) = resolved_entry.as_ref() {
343 if entry.range.start.row
344 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
345 {
346 range.end = range.end.max(entry.range.end);
347 continue;
348 }
349 }
350
351 let excerpt_start =
352 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
353 let excerpt_end = snapshot.clip_point(
354 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
355 Bias::Left,
356 );
357 let excerpt_id = excerpts
358 .insert_excerpts_after(
359 prev_excerpt_id,
360 buffer.clone(),
361 [ExcerptRange {
362 context: excerpt_start..excerpt_end,
363 primary: Some(range.clone()),
364 }],
365 excerpts_cx,
366 )
367 .pop()
368 .unwrap();
369
370 prev_excerpt_id = excerpt_id.clone();
371 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
372 group_state.excerpts.push(excerpt_id.clone());
373 let header_position = (excerpt_id.clone(), language::Anchor::MIN);
374
375 if is_first_excerpt_for_group {
376 is_first_excerpt_for_group = false;
377 let mut primary =
378 group.entries[group.primary_ix].diagnostic.clone();
379 primary.message =
380 primary.message.split('\n').next().unwrap().to_string();
381 group_state.block_count += 1;
382 blocks_to_add.push(BlockProperties {
383 position: header_position,
384 height: 2,
385 style: BlockStyle::Sticky,
386 render: diagnostic_header_renderer(primary),
387 disposition: BlockDisposition::Above,
388 });
389 }
390
391 for entry in &group.entries[*start_ix..ix] {
392 let mut diagnostic = entry.diagnostic.clone();
393 if diagnostic.is_primary {
394 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
395 diagnostic.message =
396 entry.diagnostic.message.split('\n').skip(1).collect();
397 }
398
399 if !diagnostic.message.is_empty() {
400 group_state.block_count += 1;
401 blocks_to_add.push(BlockProperties {
402 position: (excerpt_id.clone(), entry.range.start),
403 height: diagnostic.message.matches('\n').count() as u8 + 1,
404 style: BlockStyle::Fixed,
405 render: diagnostic_block_renderer(diagnostic, true),
406 disposition: BlockDisposition::Below,
407 });
408 }
409 }
410
411 pending_range.take();
412 }
413
414 if let Some(entry) = resolved_entry {
415 pending_range = Some((entry.range.clone(), ix));
416 }
417 }
418
419 groups_to_add.push(group_state);
420 } else if let Some((group_ix, group_state)) = to_remove {
421 excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
422 group_ixs_to_remove.push(group_ix);
423 blocks_to_remove.extend(group_state.blocks.iter().copied());
424 } else if let Some((_, group)) = to_keep {
425 prev_excerpt_id = group.excerpts.last().unwrap().clone();
426 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
427 }
428 }
429
430 excerpts.snapshot(excerpts_cx)
431 });
432
433 self.editor.update(cx, |editor, cx| {
434 editor.remove_blocks(blocks_to_remove, cx);
435 let block_ids = editor.insert_blocks(
436 blocks_to_add.into_iter().map(|block| {
437 let (excerpt_id, text_anchor) = block.position;
438 BlockProperties {
439 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
440 height: block.height,
441 style: block.style,
442 render: block.render,
443 disposition: block.disposition,
444 }
445 }),
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 git_diff_recalc(
614 &mut self,
615 project: ModelHandle<Project>,
616 cx: &mut ViewContext<Self>,
617 ) -> Task<Result<()>> {
618 self.editor
619 .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
620 }
621
622 fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
623 Editor::to_item_events(event)
624 }
625
626 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
627 self.editor.update(cx, |editor, _| {
628 editor.set_nav_history(Some(nav_history));
629 });
630 }
631
632 fn clone_on_split(
633 &self,
634 _workspace_id: workspace::WorkspaceId,
635 cx: &mut ViewContext<Self>,
636 ) -> Option<Self>
637 where
638 Self: Sized,
639 {
640 Some(ProjectDiagnosticsEditor::new(
641 self.project.clone(),
642 self.workspace.clone(),
643 cx,
644 ))
645 }
646
647 fn act_as_type<'a>(
648 &'a self,
649 type_id: TypeId,
650 self_handle: &'a ViewHandle<Self>,
651 _: &'a AppContext,
652 ) -> Option<&AnyViewHandle> {
653 if type_id == TypeId::of::<Self>() {
654 Some(self_handle)
655 } else if type_id == TypeId::of::<Editor>() {
656 Some(&self.editor)
657 } else {
658 None
659 }
660 }
661
662 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
663 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
664 }
665
666 fn serialized_item_kind() -> Option<&'static str> {
667 Some("diagnostics")
668 }
669
670 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
671 self.editor.breadcrumbs(theme, cx)
672 }
673
674 fn breadcrumb_location(&self) -> ToolbarItemLocation {
675 ToolbarItemLocation::PrimaryLeft { flex: None }
676 }
677
678 fn deserialize(
679 project: ModelHandle<Project>,
680 workspace: WeakViewHandle<Workspace>,
681 _workspace_id: workspace::WorkspaceId,
682 _item_id: workspace::ItemId,
683 cx: &mut ViewContext<Pane>,
684 ) -> Task<Result<ViewHandle<Self>>> {
685 Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
686 }
687}
688
689fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
690 let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
691 Arc::new(move |cx| {
692 let settings = settings::get::<ThemeSettings>(cx);
693 let theme = &settings.theme.editor;
694 let style = theme.diagnostic_header.clone();
695 let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
696 let icon_width = cx.em_width * style.icon_width_factor;
697 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
698 Svg::new("icons/circle_x_mark_12.svg")
699 .with_color(theme.error_diagnostic.message.text.color)
700 } else {
701 Svg::new("icons/triangle_exclamation_12.svg")
702 .with_color(theme.warning_diagnostic.message.text.color)
703 };
704
705 Flex::row()
706 .with_child(
707 icon.constrained()
708 .with_width(icon_width)
709 .aligned()
710 .contained()
711 .with_margin_right(cx.gutter_padding),
712 )
713 .with_children(diagnostic.source.as_ref().map(|source| {
714 Label::new(
715 format!("{source}: "),
716 style.source.label.clone().with_font_size(font_size),
717 )
718 .contained()
719 .with_style(style.message.container)
720 .aligned()
721 }))
722 .with_child(
723 Label::new(
724 message.clone(),
725 style.message.label.clone().with_font_size(font_size),
726 )
727 .with_highlights(highlights.clone())
728 .contained()
729 .with_style(style.message.container)
730 .aligned(),
731 )
732 .with_children(diagnostic.code.clone().map(|code| {
733 Label::new(code, style.code.text.clone().with_font_size(font_size))
734 .contained()
735 .with_style(style.code.container)
736 .aligned()
737 }))
738 .contained()
739 .with_style(style.container)
740 .with_padding_left(cx.gutter_padding)
741 .with_padding_right(cx.gutter_padding)
742 .expanded()
743 .into_any_named("diagnostic header")
744 })
745}
746
747pub(crate) fn render_summary<T: View>(
748 summary: &DiagnosticSummary,
749 text_style: &TextStyle,
750 theme: &theme::ProjectDiagnostics,
751) -> AnyElement<T> {
752 if summary.error_count == 0 && summary.warning_count == 0 {
753 Label::new("No problems", text_style.clone()).into_any()
754 } else {
755 let icon_width = theme.tab_icon_width;
756 let icon_spacing = theme.tab_icon_spacing;
757 let summary_spacing = theme.tab_summary_spacing;
758 Flex::row()
759 .with_child(
760 Svg::new("icons/circle_x_mark_12.svg")
761 .with_color(text_style.color)
762 .constrained()
763 .with_width(icon_width)
764 .aligned()
765 .contained()
766 .with_margin_right(icon_spacing),
767 )
768 .with_child(
769 Label::new(
770 summary.error_count.to_string(),
771 LabelStyle {
772 text: text_style.clone(),
773 highlight_text: None,
774 },
775 )
776 .aligned(),
777 )
778 .with_child(
779 Svg::new("icons/triangle_exclamation_12.svg")
780 .with_color(text_style.color)
781 .constrained()
782 .with_width(icon_width)
783 .aligned()
784 .contained()
785 .with_margin_left(summary_spacing)
786 .with_margin_right(icon_spacing),
787 )
788 .with_child(
789 Label::new(
790 summary.warning_count.to_string(),
791 LabelStyle {
792 text: text_style.clone(),
793 highlight_text: None,
794 },
795 )
796 .aligned(),
797 )
798 .into_any()
799 }
800}
801
802fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
803 lhs: &DiagnosticEntry<L>,
804 rhs: &DiagnosticEntry<R>,
805 snapshot: &language::BufferSnapshot,
806) -> Ordering {
807 lhs.range
808 .start
809 .to_offset(snapshot)
810 .cmp(&rhs.range.start.to_offset(snapshot))
811 .then_with(|| {
812 lhs.range
813 .end
814 .to_offset(snapshot)
815 .cmp(&rhs.range.end.to_offset(snapshot))
816 })
817 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823 use editor::{
824 display_map::{BlockContext, TransformBlock},
825 DisplayPoint,
826 };
827 use gpui::{TestAppContext, WindowContext};
828 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
829 use project::FakeFs;
830 use serde_json::json;
831 use settings::SettingsStore;
832 use unindent::Unindent as _;
833
834 #[gpui::test]
835 async fn test_diagnostics(cx: &mut TestAppContext) {
836 init_test(cx);
837
838 let fs = FakeFs::new(cx.background());
839 fs.insert_tree(
840 "/test",
841 json!({
842 "consts.rs": "
843 const a: i32 = 'a';
844 const b: i32 = c;
845 "
846 .unindent(),
847
848 "main.rs": "
849 fn main() {
850 let x = vec![];
851 let y = vec![];
852 a(x);
853 b(y);
854 // comment 1
855 // comment 2
856 c(y);
857 d(x);
858 }
859 "
860 .unindent(),
861 }),
862 )
863 .await;
864
865 let language_server_id = LanguageServerId(0);
866 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
867 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
868
869 // Create some diagnostics
870 project.update(cx, |project, cx| {
871 project
872 .update_diagnostic_entries(
873 language_server_id,
874 PathBuf::from("/test/main.rs"),
875 None,
876 vec![
877 DiagnosticEntry {
878 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
879 diagnostic: Diagnostic {
880 message:
881 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
882 .to_string(),
883 severity: DiagnosticSeverity::INFORMATION,
884 is_primary: false,
885 is_disk_based: true,
886 group_id: 1,
887 ..Default::default()
888 },
889 },
890 DiagnosticEntry {
891 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
892 diagnostic: Diagnostic {
893 message:
894 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
895 .to_string(),
896 severity: DiagnosticSeverity::INFORMATION,
897 is_primary: false,
898 is_disk_based: true,
899 group_id: 0,
900 ..Default::default()
901 },
902 },
903 DiagnosticEntry {
904 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
905 diagnostic: Diagnostic {
906 message: "value moved here".to_string(),
907 severity: DiagnosticSeverity::INFORMATION,
908 is_primary: false,
909 is_disk_based: true,
910 group_id: 1,
911 ..Default::default()
912 },
913 },
914 DiagnosticEntry {
915 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
916 diagnostic: Diagnostic {
917 message: "value moved here".to_string(),
918 severity: DiagnosticSeverity::INFORMATION,
919 is_primary: false,
920 is_disk_based: true,
921 group_id: 0,
922 ..Default::default()
923 },
924 },
925 DiagnosticEntry {
926 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
927 diagnostic: Diagnostic {
928 message: "use of moved value\nvalue used here after move".to_string(),
929 severity: DiagnosticSeverity::ERROR,
930 is_primary: true,
931 is_disk_based: true,
932 group_id: 0,
933 ..Default::default()
934 },
935 },
936 DiagnosticEntry {
937 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
938 diagnostic: Diagnostic {
939 message: "use of moved value\nvalue used here after move".to_string(),
940 severity: DiagnosticSeverity::ERROR,
941 is_primary: true,
942 is_disk_based: true,
943 group_id: 1,
944 ..Default::default()
945 },
946 },
947 ],
948 cx,
949 )
950 .unwrap();
951 });
952
953 // Open the project diagnostics view while there are already diagnostics.
954 let view = cx.add_view(window_id, |cx| {
955 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
956 });
957
958 view.next_notification(cx).await;
959 view.update(cx, |view, cx| {
960 assert_eq!(
961 editor_blocks(&view.editor, cx),
962 [
963 (0, "path header block".into()),
964 (2, "diagnostic header".into()),
965 (15, "collapsed context".into()),
966 (16, "diagnostic header".into()),
967 (25, "collapsed context".into()),
968 ]
969 );
970 assert_eq!(
971 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
972 concat!(
973 //
974 // main.rs
975 //
976 "\n", // filename
977 "\n", // padding
978 // diagnostic group 1
979 "\n", // primary message
980 "\n", // padding
981 " let x = vec![];\n",
982 " let y = vec![];\n",
983 "\n", // supporting diagnostic
984 " a(x);\n",
985 " b(y);\n",
986 "\n", // supporting diagnostic
987 " // comment 1\n",
988 " // comment 2\n",
989 " c(y);\n",
990 "\n", // supporting diagnostic
991 " d(x);\n",
992 "\n", // context ellipsis
993 // diagnostic group 2
994 "\n", // primary message
995 "\n", // padding
996 "fn main() {\n",
997 " let x = vec![];\n",
998 "\n", // supporting diagnostic
999 " let y = vec![];\n",
1000 " a(x);\n",
1001 "\n", // supporting diagnostic
1002 " b(y);\n",
1003 "\n", // context ellipsis
1004 " c(y);\n",
1005 " d(x);\n",
1006 "\n", // supporting diagnostic
1007 "}"
1008 )
1009 );
1010
1011 // Cursor is at the first diagnostic
1012 view.editor.update(cx, |editor, cx| {
1013 assert_eq!(
1014 editor.selections.display_ranges(cx),
1015 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1016 );
1017 });
1018 });
1019
1020 // Diagnostics are added for another earlier path.
1021 project.update(cx, |project, cx| {
1022 project.disk_based_diagnostics_started(language_server_id, cx);
1023 project
1024 .update_diagnostic_entries(
1025 language_server_id,
1026 PathBuf::from("/test/consts.rs"),
1027 None,
1028 vec![DiagnosticEntry {
1029 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1030 diagnostic: Diagnostic {
1031 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1032 severity: DiagnosticSeverity::ERROR,
1033 is_primary: true,
1034 is_disk_based: true,
1035 group_id: 0,
1036 ..Default::default()
1037 },
1038 }],
1039 cx,
1040 )
1041 .unwrap();
1042 project.disk_based_diagnostics_finished(language_server_id, cx);
1043 });
1044
1045 view.next_notification(cx).await;
1046 view.update(cx, |view, cx| {
1047 assert_eq!(
1048 editor_blocks(&view.editor, cx),
1049 [
1050 (0, "path header block".into()),
1051 (2, "diagnostic header".into()),
1052 (7, "path header block".into()),
1053 (9, "diagnostic header".into()),
1054 (22, "collapsed context".into()),
1055 (23, "diagnostic header".into()),
1056 (32, "collapsed context".into()),
1057 ]
1058 );
1059 assert_eq!(
1060 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1061 concat!(
1062 //
1063 // consts.rs
1064 //
1065 "\n", // filename
1066 "\n", // padding
1067 // diagnostic group 1
1068 "\n", // primary message
1069 "\n", // padding
1070 "const a: i32 = 'a';\n",
1071 "\n", // supporting diagnostic
1072 "const b: i32 = c;\n",
1073 //
1074 // main.rs
1075 //
1076 "\n", // filename
1077 "\n", // padding
1078 // diagnostic group 1
1079 "\n", // primary message
1080 "\n", // padding
1081 " let x = vec![];\n",
1082 " let y = vec![];\n",
1083 "\n", // supporting diagnostic
1084 " a(x);\n",
1085 " b(y);\n",
1086 "\n", // supporting diagnostic
1087 " // comment 1\n",
1088 " // comment 2\n",
1089 " c(y);\n",
1090 "\n", // supporting diagnostic
1091 " d(x);\n",
1092 "\n", // collapsed context
1093 // diagnostic group 2
1094 "\n", // primary message
1095 "\n", // filename
1096 "fn main() {\n",
1097 " let x = vec![];\n",
1098 "\n", // supporting diagnostic
1099 " let y = vec![];\n",
1100 " a(x);\n",
1101 "\n", // supporting diagnostic
1102 " b(y);\n",
1103 "\n", // context ellipsis
1104 " c(y);\n",
1105 " d(x);\n",
1106 "\n", // supporting diagnostic
1107 "}"
1108 )
1109 );
1110
1111 // Cursor keeps its position.
1112 view.editor.update(cx, |editor, cx| {
1113 assert_eq!(
1114 editor.selections.display_ranges(cx),
1115 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1116 );
1117 });
1118 });
1119
1120 // Diagnostics are added to the first path
1121 project.update(cx, |project, cx| {
1122 project.disk_based_diagnostics_started(language_server_id, cx);
1123 project
1124 .update_diagnostic_entries(
1125 language_server_id,
1126 PathBuf::from("/test/consts.rs"),
1127 None,
1128 vec![
1129 DiagnosticEntry {
1130 range: Unclipped(PointUtf16::new(0, 15))
1131 ..Unclipped(PointUtf16::new(0, 15)),
1132 diagnostic: Diagnostic {
1133 message: "mismatched types\nexpected `usize`, found `char`"
1134 .to_string(),
1135 severity: DiagnosticSeverity::ERROR,
1136 is_primary: true,
1137 is_disk_based: true,
1138 group_id: 0,
1139 ..Default::default()
1140 },
1141 },
1142 DiagnosticEntry {
1143 range: Unclipped(PointUtf16::new(1, 15))
1144 ..Unclipped(PointUtf16::new(1, 15)),
1145 diagnostic: Diagnostic {
1146 message: "unresolved name `c`".to_string(),
1147 severity: DiagnosticSeverity::ERROR,
1148 is_primary: true,
1149 is_disk_based: true,
1150 group_id: 1,
1151 ..Default::default()
1152 },
1153 },
1154 ],
1155 cx,
1156 )
1157 .unwrap();
1158 project.disk_based_diagnostics_finished(language_server_id, cx);
1159 });
1160
1161 view.next_notification(cx).await;
1162 view.update(cx, |view, cx| {
1163 assert_eq!(
1164 editor_blocks(&view.editor, cx),
1165 [
1166 (0, "path header block".into()),
1167 (2, "diagnostic header".into()),
1168 (7, "collapsed context".into()),
1169 (8, "diagnostic header".into()),
1170 (13, "path header block".into()),
1171 (15, "diagnostic header".into()),
1172 (28, "collapsed context".into()),
1173 (29, "diagnostic header".into()),
1174 (38, "collapsed context".into()),
1175 ]
1176 );
1177 assert_eq!(
1178 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1179 concat!(
1180 //
1181 // consts.rs
1182 //
1183 "\n", // filename
1184 "\n", // padding
1185 // diagnostic group 1
1186 "\n", // primary message
1187 "\n", // padding
1188 "const a: i32 = 'a';\n",
1189 "\n", // supporting diagnostic
1190 "const b: i32 = c;\n",
1191 "\n", // context ellipsis
1192 // diagnostic group 2
1193 "\n", // primary message
1194 "\n", // padding
1195 "const a: i32 = 'a';\n",
1196 "const b: i32 = c;\n",
1197 "\n", // supporting diagnostic
1198 //
1199 // main.rs
1200 //
1201 "\n", // filename
1202 "\n", // padding
1203 // diagnostic group 1
1204 "\n", // primary message
1205 "\n", // padding
1206 " let x = vec![];\n",
1207 " let y = vec![];\n",
1208 "\n", // supporting diagnostic
1209 " a(x);\n",
1210 " b(y);\n",
1211 "\n", // supporting diagnostic
1212 " // comment 1\n",
1213 " // comment 2\n",
1214 " c(y);\n",
1215 "\n", // supporting diagnostic
1216 " d(x);\n",
1217 "\n", // context ellipsis
1218 // diagnostic group 2
1219 "\n", // primary message
1220 "\n", // filename
1221 "fn main() {\n",
1222 " let x = vec![];\n",
1223 "\n", // supporting diagnostic
1224 " let y = vec![];\n",
1225 " a(x);\n",
1226 "\n", // supporting diagnostic
1227 " b(y);\n",
1228 "\n", // context ellipsis
1229 " c(y);\n",
1230 " d(x);\n",
1231 "\n", // supporting diagnostic
1232 "}"
1233 )
1234 );
1235 });
1236 }
1237
1238 #[gpui::test]
1239 async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1240 init_test(cx);
1241
1242 let fs = FakeFs::new(cx.background());
1243 fs.insert_tree(
1244 "/test",
1245 json!({
1246 "main.js": "
1247 a();
1248 b();
1249 c();
1250 d();
1251 e();
1252 ".unindent()
1253 }),
1254 )
1255 .await;
1256
1257 let server_id_1 = LanguageServerId(100);
1258 let server_id_2 = LanguageServerId(101);
1259 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1260 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1261
1262 let view = cx.add_view(window_id, |cx| {
1263 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1264 });
1265
1266 // Two language servers start updating diagnostics
1267 project.update(cx, |project, cx| {
1268 project.disk_based_diagnostics_started(server_id_1, cx);
1269 project.disk_based_diagnostics_started(server_id_2, cx);
1270 project
1271 .update_diagnostic_entries(
1272 server_id_1,
1273 PathBuf::from("/test/main.js"),
1274 None,
1275 vec![DiagnosticEntry {
1276 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1277 diagnostic: Diagnostic {
1278 message: "error 1".to_string(),
1279 severity: DiagnosticSeverity::WARNING,
1280 is_primary: true,
1281 is_disk_based: true,
1282 group_id: 1,
1283 ..Default::default()
1284 },
1285 }],
1286 cx,
1287 )
1288 .unwrap();
1289 project
1290 .update_diagnostic_entries(
1291 server_id_2,
1292 PathBuf::from("/test/main.js"),
1293 None,
1294 vec![DiagnosticEntry {
1295 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1296 diagnostic: Diagnostic {
1297 message: "warning 1".to_string(),
1298 severity: DiagnosticSeverity::ERROR,
1299 is_primary: true,
1300 is_disk_based: true,
1301 group_id: 2,
1302 ..Default::default()
1303 },
1304 }],
1305 cx,
1306 )
1307 .unwrap();
1308 });
1309
1310 // The first language server finishes
1311 project.update(cx, |project, cx| {
1312 project.disk_based_diagnostics_finished(server_id_1, cx);
1313 });
1314
1315 // Only the first language server's diagnostics are shown.
1316 cx.foreground().run_until_parked();
1317 view.update(cx, |view, cx| {
1318 assert_eq!(
1319 editor_blocks(&view.editor, cx),
1320 [
1321 (0, "path header block".into()),
1322 (2, "diagnostic header".into()),
1323 ]
1324 );
1325 assert_eq!(
1326 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1327 concat!(
1328 "\n", // filename
1329 "\n", // padding
1330 // diagnostic group 1
1331 "\n", // primary message
1332 "\n", // padding
1333 "a();\n", //
1334 "b();",
1335 )
1336 );
1337 });
1338
1339 // The second language server finishes
1340 project.update(cx, |project, cx| {
1341 project.disk_based_diagnostics_finished(server_id_2, cx);
1342 });
1343
1344 // Both language server's diagnostics are shown.
1345 cx.foreground().run_until_parked();
1346 view.update(cx, |view, cx| {
1347 assert_eq!(
1348 editor_blocks(&view.editor, cx),
1349 [
1350 (0, "path header block".into()),
1351 (2, "diagnostic header".into()),
1352 (6, "collapsed context".into()),
1353 (7, "diagnostic header".into()),
1354 ]
1355 );
1356 assert_eq!(
1357 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1358 concat!(
1359 "\n", // filename
1360 "\n", // padding
1361 // diagnostic group 1
1362 "\n", // primary message
1363 "\n", // padding
1364 "a();\n", // location
1365 "b();\n", //
1366 "\n", // collapsed context
1367 // diagnostic group 2
1368 "\n", // primary message
1369 "\n", // padding
1370 "a();\n", // context
1371 "b();\n", //
1372 "c();", // context
1373 )
1374 );
1375 });
1376
1377 // Both language servers start updating diagnostics, and the first server finishes.
1378 project.update(cx, |project, cx| {
1379 project.disk_based_diagnostics_started(server_id_1, cx);
1380 project.disk_based_diagnostics_started(server_id_2, cx);
1381 project
1382 .update_diagnostic_entries(
1383 server_id_1,
1384 PathBuf::from("/test/main.js"),
1385 None,
1386 vec![DiagnosticEntry {
1387 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1388 diagnostic: Diagnostic {
1389 message: "warning 2".to_string(),
1390 severity: DiagnosticSeverity::WARNING,
1391 is_primary: true,
1392 is_disk_based: true,
1393 group_id: 1,
1394 ..Default::default()
1395 },
1396 }],
1397 cx,
1398 )
1399 .unwrap();
1400 project
1401 .update_diagnostic_entries(
1402 server_id_2,
1403 PathBuf::from("/test/main.rs"),
1404 None,
1405 vec![],
1406 cx,
1407 )
1408 .unwrap();
1409 project.disk_based_diagnostics_finished(server_id_1, cx);
1410 });
1411
1412 // Only the first language server's diagnostics are updated.
1413 cx.foreground().run_until_parked();
1414 view.update(cx, |view, cx| {
1415 assert_eq!(
1416 editor_blocks(&view.editor, cx),
1417 [
1418 (0, "path header block".into()),
1419 (2, "diagnostic header".into()),
1420 (7, "collapsed context".into()),
1421 (8, "diagnostic header".into()),
1422 ]
1423 );
1424 assert_eq!(
1425 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1426 concat!(
1427 "\n", // filename
1428 "\n", // padding
1429 // diagnostic group 1
1430 "\n", // primary message
1431 "\n", // padding
1432 "a();\n", // location
1433 "b();\n", //
1434 "c();\n", // context
1435 "\n", // collapsed context
1436 // diagnostic group 2
1437 "\n", // primary message
1438 "\n", // padding
1439 "b();\n", // context
1440 "c();\n", //
1441 "d();", // context
1442 )
1443 );
1444 });
1445
1446 // The second language server finishes.
1447 project.update(cx, |project, cx| {
1448 project
1449 .update_diagnostic_entries(
1450 server_id_2,
1451 PathBuf::from("/test/main.js"),
1452 None,
1453 vec![DiagnosticEntry {
1454 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1455 diagnostic: Diagnostic {
1456 message: "warning 2".to_string(),
1457 severity: DiagnosticSeverity::WARNING,
1458 is_primary: true,
1459 is_disk_based: true,
1460 group_id: 1,
1461 ..Default::default()
1462 },
1463 }],
1464 cx,
1465 )
1466 .unwrap();
1467 project.disk_based_diagnostics_finished(server_id_2, cx);
1468 });
1469
1470 // Both language servers' diagnostics are updated.
1471 cx.foreground().run_until_parked();
1472 view.update(cx, |view, cx| {
1473 assert_eq!(
1474 editor_blocks(&view.editor, cx),
1475 [
1476 (0, "path header block".into()),
1477 (2, "diagnostic header".into()),
1478 (7, "collapsed context".into()),
1479 (8, "diagnostic header".into()),
1480 ]
1481 );
1482 assert_eq!(
1483 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1484 concat!(
1485 "\n", // filename
1486 "\n", // padding
1487 // diagnostic group 1
1488 "\n", // primary message
1489 "\n", // padding
1490 "b();\n", // location
1491 "c();\n", //
1492 "d();\n", // context
1493 "\n", // collapsed context
1494 // diagnostic group 2
1495 "\n", // primary message
1496 "\n", // padding
1497 "c();\n", // context
1498 "d();\n", //
1499 "e();", // context
1500 )
1501 );
1502 });
1503 }
1504
1505 fn init_test(cx: &mut TestAppContext) {
1506 cx.update(|cx| {
1507 cx.set_global(SettingsStore::test(cx));
1508 theme::init((), cx);
1509 language::init(cx);
1510 client::init_settings(cx);
1511 workspace::init_settings(cx);
1512 });
1513 }
1514
1515 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1516 editor.update(cx, |editor, cx| {
1517 let snapshot = editor.snapshot(cx);
1518 snapshot
1519 .blocks_in_range(0..snapshot.max_point().row())
1520 .filter_map(|(row, block)| {
1521 let name = match block {
1522 TransformBlock::Custom(block) => block
1523 .render(&mut BlockContext {
1524 view_context: cx,
1525 anchor_x: 0.,
1526 scroll_x: 0.,
1527 gutter_padding: 0.,
1528 gutter_width: 0.,
1529 line_height: 0.,
1530 em_width: 0.,
1531 })
1532 .name()?
1533 .to_string(),
1534 TransformBlock::ExcerptHeader {
1535 starts_new_buffer, ..
1536 } => {
1537 if *starts_new_buffer {
1538 "path header block".to_string()
1539 } else {
1540 "collapsed context".to_string()
1541 }
1542 }
1543 };
1544
1545 Some((row, name))
1546 })
1547 .collect()
1548 })
1549 }
1550}