1pub mod items;
2mod project_diagnostics_settings;
3mod toolbar_controls;
4
5#[cfg(test)]
6mod diagnostics_tests;
7
8use anyhow::Result;
9use collections::{BTreeSet, HashSet};
10use editor::{
11 diagnostic_block_renderer,
12 display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
13 highlight_diagnostic_message,
14 scroll::Autoscroll,
15 Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
16};
17use futures::{
18 channel::mpsc::{self, UnboundedSender},
19 StreamExt as _,
20};
21use gpui::{
22 actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
23 FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
24 SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
25 WeakView, WindowContext,
26};
27use language::{
28 Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
29};
30use lsp::LanguageServerId;
31use project::{DiagnosticSummary, Project, ProjectPath};
32use project_diagnostics_settings::ProjectDiagnosticsSettings;
33use settings::Settings;
34use std::{
35 any::{Any, TypeId},
36 cmp::Ordering,
37 mem,
38 ops::Range,
39};
40use theme::ActiveTheme;
41pub use toolbar_controls::ToolbarControls;
42use ui::{h_flex, prelude::*, Icon, IconName, Label};
43use util::ResultExt;
44use workspace::{
45 item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
46 ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
47};
48
49actions!(diagnostics, [Deploy, ToggleWarnings]);
50
51pub fn init(cx: &mut AppContext) {
52 ProjectDiagnosticsSettings::register(cx);
53 cx.observe_new_views(ProjectDiagnosticsEditor::register)
54 .detach();
55}
56
57struct ProjectDiagnosticsEditor {
58 project: Model<Project>,
59 workspace: WeakView<Workspace>,
60 focus_handle: FocusHandle,
61 editor: View<Editor>,
62 summary: DiagnosticSummary,
63 excerpts: Model<MultiBuffer>,
64 path_states: Vec<PathState>,
65 paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
66 include_warnings: bool,
67 context: u32,
68 update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
69 _update_excerpts_task: Task<Result<()>>,
70 _subscription: Subscription,
71}
72
73struct PathState {
74 path: ProjectPath,
75 diagnostic_groups: Vec<DiagnosticGroupState>,
76}
77
78struct DiagnosticGroupState {
79 language_server_id: LanguageServerId,
80 primary_diagnostic: DiagnosticEntry<language::Anchor>,
81 primary_excerpt_ix: usize,
82 excerpts: Vec<ExcerptId>,
83 blocks: HashSet<BlockId>,
84 block_count: usize,
85}
86
87impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
88
89impl Render for ProjectDiagnosticsEditor {
90 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
91 let child = if self.path_states.is_empty() {
92 div()
93 .bg(cx.theme().colors().editor_background)
94 .flex()
95 .items_center()
96 .justify_center()
97 .size_full()
98 .child(Label::new("No problems in workspace"))
99 } else {
100 div().size_full().child(self.editor.clone())
101 };
102
103 div()
104 .track_focus(&self.focus_handle)
105 .size_full()
106 .on_action(cx.listener(Self::toggle_warnings))
107 .child(child)
108 }
109}
110
111impl ProjectDiagnosticsEditor {
112 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
113 workspace.register_action(Self::deploy);
114 }
115
116 fn new_with_context(
117 context: u32,
118 project_handle: Model<Project>,
119 workspace: WeakView<Workspace>,
120 cx: &mut ViewContext<Self>,
121 ) -> Self {
122 let project_event_subscription =
123 cx.subscribe(&project_handle, |this, project, event, cx| match event {
124 project::Event::DiskBasedDiagnosticsStarted { .. } => {
125 cx.notify();
126 }
127 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
128 log::debug!("disk based diagnostics finished for server {language_server_id}");
129 this.enqueue_update_stale_excerpts(Some(*language_server_id));
130 }
131 project::Event::DiagnosticsUpdated {
132 language_server_id,
133 path,
134 } => {
135 this.paths_to_update
136 .insert((path.clone(), *language_server_id));
137 this.summary = project.read(cx).diagnostic_summary(false, cx);
138 cx.emit(EditorEvent::TitleChanged);
139
140 if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
141 log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
142 } else {
143 log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
144 this.enqueue_update_stale_excerpts(Some(*language_server_id));
145 }
146 }
147 _ => {}
148 });
149
150 let focus_handle = cx.focus_handle();
151 cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
152 .detach();
153 cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
154 .detach();
155
156 let excerpts = cx.new_model(|cx| {
157 MultiBuffer::new(
158 project_handle.read(cx).replica_id(),
159 project_handle.read(cx).capability(),
160 )
161 });
162 let editor = cx.new_view(|cx| {
163 let mut editor =
164 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
165 editor.set_vertical_scroll_margin(5, cx);
166 editor
167 });
168 cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
169 cx.emit(event.clone());
170 match event {
171 EditorEvent::Focused => {
172 if this.path_states.is_empty() {
173 cx.focus(&this.focus_handle);
174 }
175 }
176 EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),
177 _ => {}
178 }
179 })
180 .detach();
181
182 let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
183
184 let project = project_handle.read(cx);
185 let mut this = Self {
186 project: project_handle.clone(),
187 context,
188 summary: project.diagnostic_summary(false, cx),
189 workspace,
190 excerpts,
191 focus_handle,
192 editor,
193 path_states: Default::default(),
194 paths_to_update: Default::default(),
195 include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
196 update_paths_tx: update_excerpts_tx,
197 _update_excerpts_task: cx.spawn(move |this, mut cx| async move {
198 while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
199 if let Some(buffer) = project_handle
200 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
201 .await
202 .log_err()
203 {
204 this.update(&mut cx, |this, cx| {
205 this.update_excerpts(path, language_server_id, buffer, cx);
206 })?;
207 }
208 }
209 anyhow::Ok(())
210 }),
211 _subscription: project_event_subscription,
212 };
213 this.enqueue_update_all_excerpts(cx);
214 this
215 }
216
217 fn new(
218 project_handle: Model<Project>,
219 workspace: WeakView<Workspace>,
220 cx: &mut ViewContext<Self>,
221 ) -> Self {
222 Self::new_with_context(
223 editor::DEFAULT_MULTIBUFFER_CONTEXT,
224 project_handle,
225 workspace,
226 cx,
227 )
228 }
229
230 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
231 if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
232 workspace.activate_item(&existing, cx);
233 } else {
234 let workspace_handle = cx.view().downgrade();
235 let diagnostics = cx.new_view(|cx| {
236 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
237 });
238 workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx);
239 }
240 }
241
242 fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
243 self.include_warnings = !self.include_warnings;
244 self.enqueue_update_all_excerpts(cx);
245 cx.notify();
246 }
247
248 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
249 if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
250 self.editor.focus_handle(cx).focus(cx)
251 }
252 }
253
254 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
255 if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
256 self.enqueue_update_stale_excerpts(None);
257 }
258 }
259
260 /// Enqueue an update of all excerpts. Updates all paths that either
261 /// currently have diagnostics or are currently present in this view.
262 fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
263 self.project.update(cx, |project, cx| {
264 let mut paths = project
265 .diagnostic_summaries(false, cx)
266 .map(|(path, _, _)| path)
267 .collect::<BTreeSet<_>>();
268 paths.extend(self.path_states.iter().map(|state| state.path.clone()));
269 for path in paths {
270 self.update_paths_tx.unbounded_send((path, None)).unwrap();
271 }
272 });
273 }
274
275 /// Enqueue an update of the excerpts for any path whose diagnostics are known
276 /// to have changed. If a language server id is passed, then only the excerpts for
277 /// that language server's diagnostics will be updated. Otherwise, all stale excerpts
278 /// will be refreshed.
279 fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
280 for (path, server_id) in &self.paths_to_update {
281 if language_server_id.map_or(true, |id| id == *server_id) {
282 self.update_paths_tx
283 .unbounded_send((path.clone(), Some(*server_id)))
284 .unwrap();
285 }
286 }
287 }
288
289 fn update_excerpts(
290 &mut self,
291 path_to_update: ProjectPath,
292 server_to_update: Option<LanguageServerId>,
293 buffer: Model<Buffer>,
294 cx: &mut ViewContext<Self>,
295 ) {
296 self.paths_to_update.retain(|(path, server_id)| {
297 *path != path_to_update
298 || server_to_update.map_or(false, |to_update| *server_id != to_update)
299 });
300
301 let was_empty = self.path_states.is_empty();
302 let snapshot = buffer.read(cx).snapshot();
303 let path_ix = match self
304 .path_states
305 .binary_search_by_key(&&path_to_update, |e| &e.path)
306 {
307 Ok(ix) => ix,
308 Err(ix) => {
309 self.path_states.insert(
310 ix,
311 PathState {
312 path: path_to_update.clone(),
313 diagnostic_groups: Default::default(),
314 },
315 );
316 ix
317 }
318 };
319
320 let mut prev_excerpt_id = if path_ix > 0 {
321 let prev_path_last_group = &self.path_states[path_ix - 1]
322 .diagnostic_groups
323 .last()
324 .unwrap();
325 *prev_path_last_group.excerpts.last().unwrap()
326 } else {
327 ExcerptId::min()
328 };
329
330 let path_state = &mut self.path_states[path_ix];
331 let mut new_group_ixs = Vec::new();
332 let mut blocks_to_add = Vec::new();
333 let mut blocks_to_remove = HashSet::default();
334 let mut first_excerpt_id = None;
335 let max_severity = if self.include_warnings {
336 DiagnosticSeverity::WARNING
337 } else {
338 DiagnosticSeverity::ERROR
339 };
340 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
341 let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
342 .into_iter()
343 .enumerate()
344 .peekable();
345 let mut new_groups = snapshot
346 .diagnostic_groups(server_to_update)
347 .into_iter()
348 .filter(|(_, group)| {
349 group.entries[group.primary_ix].diagnostic.severity <= max_severity
350 })
351 .peekable();
352 loop {
353 let mut to_insert = None;
354 let mut to_remove = None;
355 let mut to_keep = None;
356 match (old_groups.peek(), new_groups.peek()) {
357 (None, None) => break,
358 (None, Some(_)) => to_insert = new_groups.next(),
359 (Some((_, old_group)), None) => {
360 if server_to_update.map_or(true, |id| id == old_group.language_server_id) {
361 to_remove = old_groups.next();
362 } else {
363 to_keep = old_groups.next();
364 }
365 }
366 (Some((_, old_group)), Some((new_language_server_id, new_group))) => {
367 let old_primary = &old_group.primary_diagnostic;
368 let new_primary = &new_group.entries[new_group.primary_ix];
369 match compare_diagnostics(old_primary, new_primary, &snapshot)
370 .then_with(|| old_group.language_server_id.cmp(new_language_server_id))
371 {
372 Ordering::Less => {
373 if server_to_update
374 .map_or(true, |id| id == old_group.language_server_id)
375 {
376 to_remove = old_groups.next();
377 } else {
378 to_keep = old_groups.next();
379 }
380 }
381 Ordering::Equal => {
382 to_keep = old_groups.next();
383 new_groups.next();
384 }
385 Ordering::Greater => to_insert = new_groups.next(),
386 }
387 }
388 }
389
390 if let Some((language_server_id, group)) = to_insert {
391 let mut group_state = DiagnosticGroupState {
392 language_server_id,
393 primary_diagnostic: group.entries[group.primary_ix].clone(),
394 primary_excerpt_ix: 0,
395 excerpts: Default::default(),
396 blocks: Default::default(),
397 block_count: 0,
398 };
399 let mut pending_range: Option<(Range<Point>, usize)> = None;
400 let mut is_first_excerpt_for_group = true;
401 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
402 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
403 if let Some((range, start_ix)) = &mut pending_range {
404 if let Some(entry) = resolved_entry.as_ref() {
405 if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
406 range.end = range.end.max(entry.range.end);
407 continue;
408 }
409 }
410
411 let excerpt_start =
412 Point::new(range.start.row.saturating_sub(self.context), 0);
413 let excerpt_end = snapshot.clip_point(
414 Point::new(range.end.row + self.context, u32::MAX),
415 Bias::Left,
416 );
417
418 let excerpt_id = excerpts
419 .insert_excerpts_after(
420 prev_excerpt_id,
421 buffer.clone(),
422 [ExcerptRange {
423 context: excerpt_start..excerpt_end,
424 primary: Some(range.clone()),
425 }],
426 cx,
427 )
428 .pop()
429 .unwrap();
430
431 prev_excerpt_id = excerpt_id;
432 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id);
433 group_state.excerpts.push(excerpt_id);
434 let header_position = (excerpt_id, language::Anchor::MIN);
435
436 if is_first_excerpt_for_group {
437 is_first_excerpt_for_group = false;
438 let mut primary =
439 group.entries[group.primary_ix].diagnostic.clone();
440 primary.message =
441 primary.message.split('\n').next().unwrap().to_string();
442 group_state.block_count += 1;
443 blocks_to_add.push(BlockProperties {
444 position: header_position,
445 height: 2,
446 style: BlockStyle::Sticky,
447 render: diagnostic_header_renderer(primary),
448 disposition: BlockDisposition::Above,
449 });
450 }
451
452 for entry in &group.entries[*start_ix..ix] {
453 let mut diagnostic = entry.diagnostic.clone();
454 if diagnostic.is_primary {
455 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
456 diagnostic.message =
457 entry.diagnostic.message.split('\n').skip(1).collect();
458 }
459
460 if !diagnostic.message.is_empty() {
461 group_state.block_count += 1;
462 blocks_to_add.push(BlockProperties {
463 position: (excerpt_id, entry.range.start),
464 height: diagnostic.message.matches('\n').count() as u8 + 1,
465 style: BlockStyle::Fixed,
466 render: diagnostic_block_renderer(diagnostic, true),
467 disposition: BlockDisposition::Below,
468 });
469 }
470 }
471
472 pending_range.take();
473 }
474
475 if let Some(entry) = resolved_entry {
476 pending_range = Some((entry.range.clone(), ix));
477 }
478 }
479
480 new_group_ixs.push(path_state.diagnostic_groups.len());
481 path_state.diagnostic_groups.push(group_state);
482 } else if let Some((_, group_state)) = to_remove {
483 excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx);
484 blocks_to_remove.extend(group_state.blocks.iter().copied());
485 } else if let Some((_, group_state)) = to_keep {
486 prev_excerpt_id = *group_state.excerpts.last().unwrap();
487 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id);
488 path_state.diagnostic_groups.push(group_state);
489 }
490 }
491
492 excerpts.snapshot(cx)
493 });
494
495 self.editor.update(cx, |editor, cx| {
496 editor.remove_blocks(blocks_to_remove, None, cx);
497 let block_ids = editor.insert_blocks(
498 blocks_to_add.into_iter().flat_map(|block| {
499 let (excerpt_id, text_anchor) = block.position;
500 Some(BlockProperties {
501 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
502 height: block.height,
503 style: block.style,
504 render: block.render,
505 disposition: block.disposition,
506 })
507 }),
508 Some(Autoscroll::fit()),
509 cx,
510 );
511
512 let mut block_ids = block_ids.into_iter();
513 for ix in new_group_ixs {
514 let group_state = &mut path_state.diagnostic_groups[ix];
515 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
516 }
517 });
518
519 if path_state.diagnostic_groups.is_empty() {
520 self.path_states.remove(path_ix);
521 }
522
523 self.editor.update(cx, |editor, cx| {
524 let groups;
525 let mut selections;
526 let new_excerpt_ids_by_selection_id;
527 if was_empty {
528 groups = self.path_states.first()?.diagnostic_groups.as_slice();
529 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
530 selections = vec![Selection {
531 id: 0,
532 start: 0,
533 end: 0,
534 reversed: false,
535 goal: SelectionGoal::None,
536 }];
537 } else {
538 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
539 new_excerpt_ids_by_selection_id =
540 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
541 selections = editor.selections.all::<usize>(cx);
542 }
543
544 // If any selection has lost its position, move it to start of the next primary diagnostic.
545 let snapshot = editor.snapshot(cx);
546 for selection in &mut selections {
547 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
548 let group_ix = match groups.binary_search_by(|probe| {
549 probe
550 .excerpts
551 .last()
552 .unwrap()
553 .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
554 }) {
555 Ok(ix) | Err(ix) => ix,
556 };
557 if let Some(group) = groups.get(group_ix) {
558 if let Some(offset) = excerpts_snapshot
559 .anchor_in_excerpt(
560 group.excerpts[group.primary_excerpt_ix],
561 group.primary_diagnostic.range.start,
562 )
563 .map(|anchor| anchor.to_offset(&excerpts_snapshot))
564 {
565 selection.start = offset;
566 selection.end = offset;
567 }
568 }
569 }
570 }
571 editor.change_selections(None, cx, |s| {
572 s.select(selections);
573 });
574 Some(())
575 });
576
577 if self.path_states.is_empty() {
578 if self.editor.focus_handle(cx).is_focused(cx) {
579 cx.focus(&self.focus_handle);
580 }
581 } else if self.focus_handle.is_focused(cx) {
582 let focus_handle = self.editor.focus_handle(cx);
583 cx.focus(&focus_handle);
584 }
585
586 #[cfg(test)]
587 self.check_invariants(cx);
588
589 cx.notify();
590 }
591
592 #[cfg(test)]
593 fn check_invariants(&self, cx: &mut ViewContext<Self>) {
594 let mut excerpts = Vec::new();
595 for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
596 if let Some(file) = buffer.file() {
597 excerpts.push((id, file.path().clone()));
598 }
599 }
600
601 let mut prev_path = None;
602 for (_, path) in &excerpts {
603 if let Some(prev_path) = prev_path {
604 if path < prev_path {
605 panic!("excerpts are not sorted by path {:?}", excerpts);
606 }
607 }
608 prev_path = Some(path);
609 }
610 }
611}
612
613impl FocusableView for ProjectDiagnosticsEditor {
614 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
615 self.focus_handle.clone()
616 }
617}
618
619impl Item for ProjectDiagnosticsEditor {
620 type Event = EditorEvent;
621
622 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
623 Editor::to_item_events(event, f)
624 }
625
626 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
627 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
628 }
629
630 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
631 self.editor
632 .update(cx, |editor, cx| editor.navigate(data, cx))
633 }
634
635 fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
636 Some("Project Diagnostics".into())
637 }
638
639 fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
640 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
641 Label::new("No problems")
642 .color(if params.selected {
643 Color::Default
644 } else {
645 Color::Muted
646 })
647 .into_any_element()
648 } else {
649 h_flex()
650 .gap_1()
651 .when(self.summary.error_count > 0, |then| {
652 then.child(
653 h_flex()
654 .gap_1()
655 .child(Icon::new(IconName::XCircle).color(Color::Error))
656 .child(Label::new(self.summary.error_count.to_string()).color(
657 if params.selected {
658 Color::Default
659 } else {
660 Color::Muted
661 },
662 )),
663 )
664 })
665 .when(self.summary.warning_count > 0, |then| {
666 then.child(
667 h_flex()
668 .gap_1()
669 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
670 .child(Label::new(self.summary.warning_count.to_string()).color(
671 if params.selected {
672 Color::Default
673 } else {
674 Color::Muted
675 },
676 )),
677 )
678 })
679 .into_any_element()
680 }
681 }
682
683 fn telemetry_event_text(&self) -> Option<&'static str> {
684 Some("project diagnostics")
685 }
686
687 fn for_each_project_item(
688 &self,
689 cx: &AppContext,
690 f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
691 ) {
692 self.editor.for_each_project_item(cx, f)
693 }
694
695 fn is_singleton(&self, _: &AppContext) -> bool {
696 false
697 }
698
699 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
700 self.editor.update(cx, |editor, _| {
701 editor.set_nav_history(Some(nav_history));
702 });
703 }
704
705 fn clone_on_split(
706 &self,
707 _workspace_id: Option<workspace::WorkspaceId>,
708 cx: &mut ViewContext<Self>,
709 ) -> Option<View<Self>>
710 where
711 Self: Sized,
712 {
713 Some(cx.new_view(|cx| {
714 ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
715 }))
716 }
717
718 fn is_dirty(&self, cx: &AppContext) -> bool {
719 self.excerpts.read(cx).is_dirty(cx)
720 }
721
722 fn has_conflict(&self, cx: &AppContext) -> bool {
723 self.excerpts.read(cx).has_conflict(cx)
724 }
725
726 fn can_save(&self, _: &AppContext) -> bool {
727 true
728 }
729
730 fn save(
731 &mut self,
732 format: bool,
733 project: Model<Project>,
734 cx: &mut ViewContext<Self>,
735 ) -> Task<Result<()>> {
736 self.editor.save(format, project, cx)
737 }
738
739 fn save_as(
740 &mut self,
741 _: Model<Project>,
742 _: ProjectPath,
743 _: &mut ViewContext<Self>,
744 ) -> Task<Result<()>> {
745 unreachable!()
746 }
747
748 fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
749 self.editor.reload(project, cx)
750 }
751
752 fn act_as_type<'a>(
753 &'a self,
754 type_id: TypeId,
755 self_handle: &'a View<Self>,
756 _: &'a AppContext,
757 ) -> Option<AnyView> {
758 if type_id == TypeId::of::<Self>() {
759 Some(self_handle.to_any())
760 } else if type_id == TypeId::of::<Editor>() {
761 Some(self.editor.to_any())
762 } else {
763 None
764 }
765 }
766
767 fn breadcrumb_location(&self) -> ToolbarItemLocation {
768 ToolbarItemLocation::PrimaryLeft
769 }
770
771 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
772 self.editor.breadcrumbs(theme, cx)
773 }
774
775 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
776 self.editor
777 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
778 }
779
780 fn serialized_item_kind() -> Option<&'static str> {
781 Some("diagnostics")
782 }
783
784 fn deserialize(
785 project: Model<Project>,
786 workspace: WeakView<Workspace>,
787 _workspace_id: workspace::WorkspaceId,
788 _item_id: workspace::ItemId,
789 cx: &mut ViewContext<Pane>,
790 ) -> Task<Result<View<Self>>> {
791 Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
792 }
793}
794
795const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
796
797fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
798 let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
799 let message: SharedString = message;
800 Box::new(move |cx| {
801 let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
802 h_flex()
803 .id(DIAGNOSTIC_HEADER)
804 .py_2()
805 .pl_10()
806 .pr_5()
807 .w_full()
808 .justify_between()
809 .gap_2()
810 .child(
811 h_flex()
812 .gap_3()
813 .map(|stack| {
814 stack.child(
815 svg()
816 .size(cx.text_style().font_size)
817 .flex_none()
818 .map(|icon| {
819 if diagnostic.severity == DiagnosticSeverity::ERROR {
820 icon.path(IconName::XCircle.path())
821 .text_color(Color::Error.color(cx))
822 } else {
823 icon.path(IconName::ExclamationTriangle.path())
824 .text_color(Color::Warning.color(cx))
825 }
826 }),
827 )
828 })
829 .child(
830 h_flex()
831 .gap_1()
832 .child(
833 StyledText::new(message.clone()).with_highlights(
834 &cx.text_style(),
835 code_ranges
836 .iter()
837 .map(|range| (range.clone(), highlight_style)),
838 ),
839 )
840 .when_some(diagnostic.code.as_ref(), |stack, code| {
841 stack.child(
842 div()
843 .child(SharedString::from(format!("({code})")))
844 .text_color(cx.theme().colors().text_muted),
845 )
846 }),
847 ),
848 )
849 .child(
850 h_flex()
851 .gap_1()
852 .when_some(diagnostic.source.as_ref(), |stack, source| {
853 stack.child(
854 div()
855 .child(SharedString::from(source.clone()))
856 .text_color(cx.theme().colors().text_muted),
857 )
858 }),
859 )
860 .into_any_element()
861 })
862}
863
864fn compare_diagnostics(
865 old: &DiagnosticEntry<language::Anchor>,
866 new: &DiagnosticEntry<language::Anchor>,
867 snapshot: &language::BufferSnapshot,
868) -> Ordering {
869 use language::ToOffset;
870
871 // The diagnostics may point to a previously open Buffer for this file.
872 if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
873 return Ordering::Greater;
874 }
875
876 old.range
877 .start
878 .to_offset(snapshot)
879 .cmp(&new.range.start.to_offset(snapshot))
880 .then_with(|| {
881 old.range
882 .end
883 .to_offset(snapshot)
884 .cmp(&new.range.end.to_offset(snapshot))
885 })
886 .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
887}