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