1pub mod items;
2mod project_diagnostics_settings;
3mod toolbar_controls;
4
5#[cfg(test)]
6mod diagnostics_tests;
7pub(crate) mod grouped_diagnostics;
8
9use anyhow::Result;
10use collections::{BTreeSet, HashSet};
11use editor::{
12 diagnostic_block_renderer,
13 display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, 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, 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<CustomBlockId>,
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, true, true, 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, true, 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(params.text_color())
653 .into_any_element()
654 } else {
655 h_flex()
656 .gap_1()
657 .when(self.summary.error_count > 0, |then| {
658 then.child(
659 h_flex()
660 .gap_1()
661 .child(Icon::new(IconName::XCircle).color(Color::Error))
662 .child(
663 Label::new(self.summary.error_count.to_string())
664 .color(params.text_color()),
665 ),
666 )
667 })
668 .when(self.summary.warning_count > 0, |then| {
669 then.child(
670 h_flex()
671 .gap_1()
672 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
673 .child(
674 Label::new(self.summary.warning_count.to_string())
675 .color(params.text_color()),
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
781const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
782
783fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
784 let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
785 let message: SharedString = message;
786 Box::new(move |cx| {
787 let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
788 h_flex()
789 .id(DIAGNOSTIC_HEADER)
790 .py_2()
791 .pl_10()
792 .pr_5()
793 .w_full()
794 .justify_between()
795 .gap_2()
796 .child(
797 h_flex()
798 .gap_3()
799 .map(|stack| {
800 stack.child(
801 svg()
802 .size(cx.text_style().font_size)
803 .flex_none()
804 .map(|icon| {
805 if diagnostic.severity == DiagnosticSeverity::ERROR {
806 icon.path(IconName::XCircle.path())
807 .text_color(Color::Error.color(cx))
808 } else {
809 icon.path(IconName::ExclamationTriangle.path())
810 .text_color(Color::Warning.color(cx))
811 }
812 }),
813 )
814 })
815 .child(
816 h_flex()
817 .gap_1()
818 .child(
819 StyledText::new(message.clone()).with_highlights(
820 &cx.text_style(),
821 code_ranges
822 .iter()
823 .map(|range| (range.clone(), highlight_style)),
824 ),
825 )
826 .when_some(diagnostic.code.as_ref(), |stack, code| {
827 stack.child(
828 div()
829 .child(SharedString::from(format!("({code})")))
830 .text_color(cx.theme().colors().text_muted),
831 )
832 }),
833 ),
834 )
835 .child(
836 h_flex()
837 .gap_1()
838 .when_some(diagnostic.source.as_ref(), |stack, source| {
839 stack.child(
840 div()
841 .child(SharedString::from(source.clone()))
842 .text_color(cx.theme().colors().text_muted),
843 )
844 }),
845 )
846 .into_any_element()
847 })
848}
849
850fn compare_diagnostics(
851 old: &DiagnosticEntry<language::Anchor>,
852 new: &DiagnosticEntry<language::Anchor>,
853 snapshot: &language::BufferSnapshot,
854) -> Ordering {
855 use language::ToOffset;
856
857 // The diagnostics may point to a previously open Buffer for this file.
858 if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
859 return Ordering::Greater;
860 }
861
862 old.range
863 .start
864 .to_offset(snapshot)
865 .cmp(&new.range.start.to_offset(snapshot))
866 .then_with(|| {
867 old.range
868 .end
869 .to_offset(snapshot)
870 .cmp(&new.range.end.to_offset(snapshot))
871 })
872 .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
873}