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