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