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, Component, Context, Div, EventEmitter,
17 FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent,
18 Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
19 VisualContext, WeakView,
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};
37pub use toolbar_controls::ToolbarControls;
38use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor};
39use util::TryFutureExt;
40use workspace::{
41 item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
42 ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
43};
44
45actions!(Deploy, ToggleWarnings);
46
47const CONTEXT_LINE_COUNT: u32 = 1;
48
49pub fn init(cx: &mut AppContext) {
50 ProjectDiagnosticsSettings::register(cx);
51 cx.observe_new_views(ProjectDiagnosticsEditor::register)
52 .detach();
53}
54
55struct ProjectDiagnosticsEditor {
56 project: Model<Project>,
57 workspace: WeakView<Workspace>,
58 focus_handle: FocusHandle,
59 editor: View<Editor>,
60 summary: DiagnosticSummary,
61 excerpts: Model<MultiBuffer>,
62 path_states: Vec<PathState>,
63 paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
64 current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
65 include_warnings: bool,
66 _subscriptions: Vec<Subscription>,
67}
68
69struct PathState {
70 path: ProjectPath,
71 diagnostic_groups: Vec<DiagnosticGroupState>,
72}
73
74#[derive(Clone, Debug, PartialEq)]
75struct Jump {
76 path: ProjectPath,
77 position: Point,
78 anchor: Anchor,
79}
80
81struct DiagnosticGroupState {
82 language_server_id: LanguageServerId,
83 primary_diagnostic: DiagnosticEntry<language::Anchor>,
84 primary_excerpt_ix: usize,
85 excerpts: Vec<ExcerptId>,
86 blocks: HashSet<BlockId>,
87 block_count: usize,
88}
89
90impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
91
92impl Render for ProjectDiagnosticsEditor {
93 type Element = Focusable<Self, Div<Self>>;
94
95 fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
96 let child = if self.path_states.is_empty() {
97 div()
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_focus_in(Self::focus_in)
111 .on_action(Self::toggle_warnings)
112 .child(child)
113 }
114}
115
116impl ProjectDiagnosticsEditor {
117 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
118 workspace.register_action(Self::deploy);
119 }
120
121 fn new(
122 project_handle: Model<Project>,
123 workspace: WeakView<Workspace>,
124 cx: &mut ViewContext<Self>,
125 ) -> Self {
126 let project_event_subscription =
127 cx.subscribe(&project_handle, |this, _, event, cx| match event {
128 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
129 log::debug!("Disk based diagnostics finished for server {language_server_id}");
130 this.update_excerpts(Some(*language_server_id), cx);
131 }
132 project::Event::DiagnosticsUpdated {
133 language_server_id,
134 path,
135 } => {
136 log::debug!("Adding path {path:?} to update for server {language_server_id}");
137 this.paths_to_update
138 .entry(*language_server_id)
139 .or_default()
140 .insert(path.clone());
141 if this.editor.read(cx).selections.all::<usize>(cx).is_empty()
142 && !this.is_dirty(cx)
143 {
144 this.update_excerpts(Some(*language_server_id), cx);
145 }
146 }
147 _ => {}
148 });
149
150 let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
151 let editor = cx.build_view(|cx| {
152 let mut editor =
153 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
154 editor.set_vertical_scroll_margin(5, cx);
155 editor
156 });
157 let editor_event_subscription =
158 cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
159 Self::emit_item_event_for_editor_event(event, cx);
160 if event == &EditorEvent::Focused && this.path_states.is_empty() {
161 cx.focus(&this.focus_handle);
162 }
163 });
164
165 let project = project_handle.read(cx);
166 let summary = project.diagnostic_summary(cx);
167 let mut this = Self {
168 project: project_handle,
169 summary,
170 workspace,
171 excerpts,
172 focus_handle: cx.focus_handle(),
173 editor,
174 path_states: Default::default(),
175 paths_to_update: HashMap::default(),
176 include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
177 current_diagnostics: HashMap::default(),
178 _subscriptions: vec![project_event_subscription, editor_event_subscription],
179 };
180 this.update_excerpts(None, cx);
181 this
182 }
183
184 fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
185 match event {
186 EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
187
188 EditorEvent::Saved | EditorEvent::TitleChanged => {
189 cx.emit(ItemEvent::UpdateTab);
190 cx.emit(ItemEvent::UpdateBreadcrumbs);
191 }
192
193 EditorEvent::Reparsed => {
194 cx.emit(ItemEvent::UpdateBreadcrumbs);
195 }
196
197 EditorEvent::SelectionsChanged { local } if *local => {
198 cx.emit(ItemEvent::UpdateBreadcrumbs);
199 }
200
201 EditorEvent::DirtyChanged => {
202 cx.emit(ItemEvent::UpdateTab);
203 }
204
205 EditorEvent::BufferEdited => {
206 cx.emit(ItemEvent::Edit);
207 cx.emit(ItemEvent::UpdateBreadcrumbs);
208 }
209
210 EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
211 cx.emit(ItemEvent::Edit);
212 }
213
214 _ => {}
215 }
216 }
217
218 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
219 if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
220 workspace.activate_item(&existing, cx);
221 } else {
222 let workspace_handle = cx.view().downgrade();
223 let diagnostics = cx.build_view(|cx| {
224 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
225 });
226 workspace.add_item(Box::new(diagnostics), cx);
227 }
228 }
229
230 fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
231 self.include_warnings = !self.include_warnings;
232 self.paths_to_update = self.current_diagnostics.clone();
233 self.update_excerpts(None, cx);
234 cx.notify();
235 }
236
237 fn focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext<Self>) {
238 if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
239 self.editor.focus_handle(cx).focus(cx)
240 }
241 }
242
243 fn update_excerpts(
244 &mut self,
245 language_server_id: Option<LanguageServerId>,
246 cx: &mut ViewContext<Self>,
247 ) {
248 log::debug!("Updating excerpts for server {language_server_id:?}");
249 let mut paths_to_recheck = HashSet::default();
250 let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
251 .project
252 .read(cx)
253 .diagnostic_summaries(cx)
254 .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
255 summaries.entry(server_id).or_default().insert(path);
256 summaries
257 });
258 let mut old_diagnostics = if let Some(language_server_id) = language_server_id {
259 new_summaries.retain(|server_id, _| server_id == &language_server_id);
260 self.paths_to_update.retain(|server_id, paths| {
261 if server_id == &language_server_id {
262 paths_to_recheck.extend(paths.drain());
263 false
264 } else {
265 true
266 }
267 });
268 let mut old_diagnostics = HashMap::default();
269 if let Some(new_paths) = new_summaries.get(&language_server_id) {
270 if let Some(old_paths) = self
271 .current_diagnostics
272 .insert(language_server_id, new_paths.clone())
273 {
274 old_diagnostics.insert(language_server_id, old_paths);
275 }
276 } else {
277 if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) {
278 old_diagnostics.insert(language_server_id, old_paths);
279 }
280 }
281 old_diagnostics
282 } else {
283 paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths));
284 mem::replace(&mut self.current_diagnostics, new_summaries.clone())
285 };
286 for (server_id, new_paths) in new_summaries {
287 match old_diagnostics.remove(&server_id) {
288 Some(mut old_paths) => {
289 paths_to_recheck.extend(
290 new_paths
291 .into_iter()
292 .filter(|new_path| !old_paths.remove(new_path)),
293 );
294 paths_to_recheck.extend(old_paths);
295 }
296 None => paths_to_recheck.extend(new_paths),
297 }
298 }
299 paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths));
300
301 if paths_to_recheck.is_empty() {
302 log::debug!("No paths to recheck for language server {language_server_id:?}");
303 return;
304 }
305 log::debug!(
306 "Rechecking {} paths for language server {:?}",
307 paths_to_recheck.len(),
308 language_server_id
309 );
310 let project = self.project.clone();
311 cx.spawn(|this, mut cx| {
312 async move {
313 let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
314 let mut cx = cx.clone();
315 let project = project.clone();
316 let this = this.clone();
317 async move {
318 let buffer = project
319 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
320 .await
321 .with_context(|| format!("opening buffer for path {path:?}"))?;
322 this.update(&mut cx, |this, cx| {
323 this.populate_excerpts(path, language_server_id, buffer, cx);
324 })
325 .context("missing project")?;
326 anyhow::Ok(())
327 }
328 }))
329 .await
330 .context("rechecking diagnostics for paths")?;
331
332 this.update(&mut cx, |this, cx| {
333 this.summary = this.project.read(cx).diagnostic_summary(cx);
334 cx.emit(ItemEvent::UpdateTab);
335 cx.emit(ItemEvent::UpdateBreadcrumbs);
336 })?;
337 anyhow::Ok(())
338 }
339 .log_err()
340 })
341 .detach();
342 }
343
344 fn populate_excerpts(
345 &mut self,
346 path: ProjectPath,
347 language_server_id: Option<LanguageServerId>,
348 buffer: Model<Buffer>,
349 cx: &mut ViewContext<Self>,
350 ) {
351 let was_empty = self.path_states.is_empty();
352 let snapshot = buffer.read(cx).snapshot();
353 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
354 Ok(ix) => ix,
355 Err(ix) => {
356 self.path_states.insert(
357 ix,
358 PathState {
359 path: path.clone(),
360 diagnostic_groups: Default::default(),
361 },
362 );
363 ix
364 }
365 };
366
367 let mut prev_excerpt_id = if path_ix > 0 {
368 let prev_path_last_group = &self.path_states[path_ix - 1]
369 .diagnostic_groups
370 .last()
371 .unwrap();
372 prev_path_last_group.excerpts.last().unwrap().clone()
373 } else {
374 ExcerptId::min()
375 };
376
377 let path_state = &mut self.path_states[path_ix];
378 let mut groups_to_add = Vec::new();
379 let mut group_ixs_to_remove = Vec::new();
380 let mut blocks_to_add = Vec::new();
381 let mut blocks_to_remove = HashSet::default();
382 let mut first_excerpt_id = None;
383 let max_severity = if self.include_warnings {
384 DiagnosticSeverity::WARNING
385 } else {
386 DiagnosticSeverity::ERROR
387 };
388 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
389 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
390 let mut new_groups = snapshot
391 .diagnostic_groups(language_server_id)
392 .into_iter()
393 .filter(|(_, group)| {
394 group.entries[group.primary_ix].diagnostic.severity <= max_severity
395 })
396 .peekable();
397 loop {
398 let mut to_insert = None;
399 let mut to_remove = None;
400 let mut to_keep = None;
401 match (old_groups.peek(), new_groups.peek()) {
402 (None, None) => break,
403 (None, Some(_)) => to_insert = new_groups.next(),
404 (Some((_, old_group)), None) => {
405 if language_server_id.map_or(true, |id| id == old_group.language_server_id)
406 {
407 to_remove = old_groups.next();
408 } else {
409 to_keep = old_groups.next();
410 }
411 }
412 (Some((_, old_group)), Some((_, new_group))) => {
413 let old_primary = &old_group.primary_diagnostic;
414 let new_primary = &new_group.entries[new_group.primary_ix];
415 match compare_diagnostics(old_primary, new_primary, &snapshot) {
416 Ordering::Less => {
417 if language_server_id
418 .map_or(true, |id| id == old_group.language_server_id)
419 {
420 to_remove = old_groups.next();
421 } else {
422 to_keep = old_groups.next();
423 }
424 }
425 Ordering::Equal => {
426 to_keep = old_groups.next();
427 new_groups.next();
428 }
429 Ordering::Greater => to_insert = new_groups.next(),
430 }
431 }
432 }
433
434 if let Some((language_server_id, group)) = to_insert {
435 let mut group_state = DiagnosticGroupState {
436 language_server_id,
437 primary_diagnostic: group.entries[group.primary_ix].clone(),
438 primary_excerpt_ix: 0,
439 excerpts: Default::default(),
440 blocks: Default::default(),
441 block_count: 0,
442 };
443 let mut pending_range: Option<(Range<Point>, usize)> = None;
444 let mut is_first_excerpt_for_group = true;
445 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
446 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
447 if let Some((range, start_ix)) = &mut pending_range {
448 if let Some(entry) = resolved_entry.as_ref() {
449 if entry.range.start.row
450 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
451 {
452 range.end = range.end.max(entry.range.end);
453 continue;
454 }
455 }
456
457 let excerpt_start =
458 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
459 let excerpt_end = snapshot.clip_point(
460 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
461 Bias::Left,
462 );
463 let excerpt_id = excerpts
464 .insert_excerpts_after(
465 prev_excerpt_id,
466 buffer.clone(),
467 [ExcerptRange {
468 context: excerpt_start..excerpt_end,
469 primary: Some(range.clone()),
470 }],
471 excerpts_cx,
472 )
473 .pop()
474 .unwrap();
475
476 prev_excerpt_id = excerpt_id.clone();
477 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
478 group_state.excerpts.push(excerpt_id.clone());
479 let header_position = (excerpt_id.clone(), language::Anchor::MIN);
480
481 if is_first_excerpt_for_group {
482 is_first_excerpt_for_group = false;
483 let mut primary =
484 group.entries[group.primary_ix].diagnostic.clone();
485 primary.message =
486 primary.message.split('\n').next().unwrap().to_string();
487 group_state.block_count += 1;
488 blocks_to_add.push(BlockProperties {
489 position: header_position,
490 height: 2,
491 style: BlockStyle::Sticky,
492 render: diagnostic_header_renderer(primary),
493 disposition: BlockDisposition::Above,
494 });
495 }
496
497 for entry in &group.entries[*start_ix..ix] {
498 let mut diagnostic = entry.diagnostic.clone();
499 if diagnostic.is_primary {
500 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
501 diagnostic.message =
502 entry.diagnostic.message.split('\n').skip(1).collect();
503 }
504
505 if !diagnostic.message.is_empty() {
506 group_state.block_count += 1;
507 blocks_to_add.push(BlockProperties {
508 position: (excerpt_id.clone(), entry.range.start),
509 height: diagnostic.message.matches('\n').count() as u8 + 1,
510 style: BlockStyle::Fixed,
511 render: diagnostic_block_renderer(diagnostic, true),
512 disposition: BlockDisposition::Below,
513 });
514 }
515 }
516
517 pending_range.take();
518 }
519
520 if let Some(entry) = resolved_entry {
521 pending_range = Some((entry.range.clone(), ix));
522 }
523 }
524
525 groups_to_add.push(group_state);
526 } else if let Some((group_ix, group_state)) = to_remove {
527 excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
528 group_ixs_to_remove.push(group_ix);
529 blocks_to_remove.extend(group_state.blocks.iter().copied());
530 } else if let Some((_, group)) = to_keep {
531 prev_excerpt_id = group.excerpts.last().unwrap().clone();
532 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
533 }
534 }
535
536 excerpts.snapshot(excerpts_cx)
537 });
538
539 self.editor.update(cx, |editor, cx| {
540 editor.remove_blocks(blocks_to_remove, None, cx);
541 let block_ids = editor.insert_blocks(
542 blocks_to_add.into_iter().map(|block| {
543 let (excerpt_id, text_anchor) = block.position;
544 BlockProperties {
545 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
546 height: block.height,
547 style: block.style,
548 render: block.render,
549 disposition: block.disposition,
550 }
551 }),
552 Some(Autoscroll::fit()),
553 cx,
554 );
555
556 let mut block_ids = block_ids.into_iter();
557 for group_state in &mut groups_to_add {
558 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
559 }
560 });
561
562 for ix in group_ixs_to_remove.into_iter().rev() {
563 path_state.diagnostic_groups.remove(ix);
564 }
565 path_state.diagnostic_groups.extend(groups_to_add);
566 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
567 let range_a = &a.primary_diagnostic.range;
568 let range_b = &b.primary_diagnostic.range;
569 range_a
570 .start
571 .cmp(&range_b.start, &snapshot)
572 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
573 });
574
575 if path_state.diagnostic_groups.is_empty() {
576 self.path_states.remove(path_ix);
577 }
578
579 self.editor.update(cx, |editor, cx| {
580 let groups;
581 let mut selections;
582 let new_excerpt_ids_by_selection_id;
583 if was_empty {
584 groups = self.path_states.first()?.diagnostic_groups.as_slice();
585 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
586 selections = vec![Selection {
587 id: 0,
588 start: 0,
589 end: 0,
590 reversed: false,
591 goal: SelectionGoal::None,
592 }];
593 } else {
594 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
595 new_excerpt_ids_by_selection_id =
596 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
597 selections = editor.selections.all::<usize>(cx);
598 }
599
600 // If any selection has lost its position, move it to start of the next primary diagnostic.
601 let snapshot = editor.snapshot(cx);
602 for selection in &mut selections {
603 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
604 let group_ix = match groups.binary_search_by(|probe| {
605 probe
606 .excerpts
607 .last()
608 .unwrap()
609 .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
610 }) {
611 Ok(ix) | Err(ix) => ix,
612 };
613 if let Some(group) = groups.get(group_ix) {
614 let offset = excerpts_snapshot
615 .anchor_in_excerpt(
616 group.excerpts[group.primary_excerpt_ix].clone(),
617 group.primary_diagnostic.range.start,
618 )
619 .to_offset(&excerpts_snapshot);
620 selection.start = offset;
621 selection.end = offset;
622 }
623 }
624 }
625 editor.change_selections(None, cx, |s| {
626 s.select(selections);
627 });
628 Some(())
629 });
630
631 if self.path_states.is_empty() {
632 if self.editor.focus_handle(cx).is_focused(cx) {
633 cx.focus(&self.focus_handle);
634 }
635 } else if self.focus_handle.is_focused(cx) {
636 let focus_handle = self.editor.focus_handle(cx);
637 cx.focus(&focus_handle);
638 }
639 cx.notify();
640 }
641}
642
643impl FocusableView for ProjectDiagnosticsEditor {
644 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
645 self.focus_handle.clone()
646 }
647}
648
649impl Item for ProjectDiagnosticsEditor {
650 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
651 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
652 }
653
654 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
655 self.editor
656 .update(cx, |editor, cx| editor.navigate(data, cx))
657 }
658
659 fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
660 Some("Project Diagnostics".into())
661 }
662
663 fn tab_content<T: 'static>(&self, _detail: Option<usize>, _: &AppContext) -> AnyElement<T> {
664 render_summary(&self.summary)
665 }
666
667 fn for_each_project_item(
668 &self,
669 cx: &AppContext,
670 f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
671 ) {
672 self.editor.for_each_project_item(cx, f)
673 }
674
675 fn is_singleton(&self, _: &AppContext) -> bool {
676 false
677 }
678
679 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
680 self.editor.update(cx, |editor, _| {
681 editor.set_nav_history(Some(nav_history));
682 });
683 }
684
685 fn clone_on_split(
686 &self,
687 _workspace_id: workspace::WorkspaceId,
688 cx: &mut ViewContext<Self>,
689 ) -> Option<View<Self>>
690 where
691 Self: Sized,
692 {
693 Some(cx.build_view(|cx| {
694 ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
695 }))
696 }
697
698 fn is_dirty(&self, cx: &AppContext) -> bool {
699 self.excerpts.read(cx).is_dirty(cx)
700 }
701
702 fn has_conflict(&self, cx: &AppContext) -> bool {
703 self.excerpts.read(cx).has_conflict(cx)
704 }
705
706 fn can_save(&self, _: &AppContext) -> bool {
707 true
708 }
709
710 fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
711 self.editor.save(project, cx)
712 }
713
714 fn save_as(
715 &mut self,
716 _: Model<Project>,
717 _: PathBuf,
718 _: &mut ViewContext<Self>,
719 ) -> Task<Result<()>> {
720 unreachable!()
721 }
722
723 fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
724 self.editor.reload(project, cx)
725 }
726
727 fn act_as_type<'a>(
728 &'a self,
729 type_id: TypeId,
730 self_handle: &'a View<Self>,
731 _: &'a AppContext,
732 ) -> Option<AnyView> {
733 if type_id == TypeId::of::<Self>() {
734 Some(self_handle.to_any())
735 } else if type_id == TypeId::of::<Editor>() {
736 Some(self.editor.to_any())
737 } else {
738 None
739 }
740 }
741
742 fn breadcrumb_location(&self) -> ToolbarItemLocation {
743 ToolbarItemLocation::PrimaryLeft { flex: None }
744 }
745
746 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
747 self.editor.breadcrumbs(theme, cx)
748 }
749
750 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
751 self.editor
752 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
753 }
754
755 fn serialized_item_kind() -> Option<&'static str> {
756 Some("diagnostics")
757 }
758
759 fn deserialize(
760 project: Model<Project>,
761 workspace: WeakView<Workspace>,
762 _workspace_id: workspace::WorkspaceId,
763 _item_id: workspace::ItemId,
764 cx: &mut ViewContext<Pane>,
765 ) -> Task<Result<View<Self>>> {
766 Task::ready(Ok(cx.build_view(|cx| Self::new(project, workspace, cx))))
767 }
768}
769
770fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
771 let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
772 Arc::new(move |_| {
773 h_stack()
774 .id("diagnostic header")
775 .gap_3()
776 .bg(gpui::red())
777 .map(|stack| {
778 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
779 IconElement::new(Icon::XCircle).color(TextColor::Error)
780 } else {
781 IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)
782 };
783
784 stack.child(div().pl_8().child(icon))
785 })
786 .when_some(diagnostic.source.as_ref(), |stack, source| {
787 stack.child(Label::new(format!("{source}:")).color(TextColor::Accent))
788 })
789 .child(HighlightedLabel::new(message.clone(), highlights.clone()))
790 .when_some(diagnostic.code.as_ref(), |stack, code| {
791 stack.child(Label::new(code.clone()))
792 })
793 .render()
794 })
795}
796
797pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElement<T> {
798 if summary.error_count == 0 && summary.warning_count == 0 {
799 Label::new("No problems").render()
800 } else {
801 h_stack()
802 .bg(gpui::red())
803 .child(IconElement::new(Icon::XCircle))
804 .child(Label::new(summary.error_count.to_string()))
805 .child(IconElement::new(Icon::ExclamationTriangle))
806 .child(Label::new(summary.warning_count.to_string()))
807 .render()
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 .element_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}