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