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