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