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