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