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