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