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