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