1use crate::{
2 DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,
3 diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
4 toolbar_controls::DiagnosticsToolbarEditor,
5};
6use anyhow::Result;
7use collections::HashMap;
8use editor::{
9 Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
10 display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
11 multibuffer_context_lines,
12};
13use gpui::{
14 AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
15 InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
16 Task, WeakEntity, Window, actions, div,
17};
18use language::{Buffer, DiagnosticEntry, DiagnosticEntryRef, Point};
19use project::{
20 DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
21 project_settings::{DiagnosticSeverity, ProjectSettings},
22};
23use settings::Settings;
24use std::{
25 any::{Any, TypeId},
26 cmp::{self, Ordering},
27 sync::Arc,
28};
29use text::{Anchor, BufferSnapshot, OffsetRangeExt};
30use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
31use workspace::{
32 ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
33 item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
34};
35
36actions!(
37 diagnostics,
38 [
39 /// Opens the project diagnostics view for the currently focused file.
40 DeployCurrentFile,
41 ]
42);
43
44/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
45/// with diagnostics for a single buffer, as only the excerpts of the buffer
46/// where diagnostics are available are displayed.
47pub(crate) struct BufferDiagnosticsEditor {
48 pub project: Entity<Project>,
49 focus_handle: FocusHandle,
50 editor: Entity<Editor>,
51 /// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
52 /// allow quick comparison of updated diagnostics, to confirm if anything
53 /// has changed.
54 pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
55 /// The blocks used to display the diagnostics' content in the editor, next
56 /// to the excerpts where the diagnostic originated.
57 blocks: Vec<CustomBlockId>,
58 /// Multibuffer to contain all excerpts that contain diagnostics, which are
59 /// to be rendered in the editor.
60 multibuffer: Entity<MultiBuffer>,
61 /// The buffer for which the editor is displaying diagnostics and excerpts
62 /// for.
63 buffer: Option<Entity<Buffer>>,
64 /// The path for which the editor is displaying diagnostics for.
65 project_path: ProjectPath,
66 /// Summary of the number of warnings and errors for the path. Used to
67 /// display the number of warnings and errors in the tab's content.
68 summary: DiagnosticSummary,
69 /// Whether to include warnings in the list of diagnostics shown in the
70 /// editor.
71 pub(crate) include_warnings: bool,
72 /// Keeps track of whether there's a background task already running to
73 /// update the excerpts, in order to avoid firing multiple tasks for this purpose.
74 pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
75 /// The project's subscription, responsible for processing events related to
76 /// diagnostics.
77 _subscription: Subscription,
78}
79
80impl BufferDiagnosticsEditor {
81 /// Creates new instance of the `BufferDiagnosticsEditor` which can then be
82 /// displayed by adding it to a pane.
83 pub fn new(
84 project_path: ProjectPath,
85 project_handle: Entity<Project>,
86 buffer: Option<Entity<Buffer>>,
87 include_warnings: bool,
88 window: &mut Window,
89 cx: &mut Context<Self>,
90 ) -> Self {
91 // Subscribe to project events related to diagnostics so the
92 // `BufferDiagnosticsEditor` can update its state accordingly.
93 let project_event_subscription = cx.subscribe_in(
94 &project_handle,
95 window,
96 |buffer_diagnostics_editor, _project, event, window, cx| match event {
97 Event::DiskBasedDiagnosticsStarted { .. } => {
98 cx.notify();
99 }
100 Event::DiskBasedDiagnosticsFinished { .. } => {
101 buffer_diagnostics_editor.update_all_excerpts(window, cx);
102 }
103 Event::DiagnosticsUpdated {
104 paths,
105 language_server_id,
106 } => {
107 // When diagnostics have been updated, the
108 // `BufferDiagnosticsEditor` should update its state only if
109 // one of the paths matches its `project_path`, otherwise
110 // the event should be ignored.
111 if paths.contains(&buffer_diagnostics_editor.project_path) {
112 buffer_diagnostics_editor.update_diagnostic_summary(cx);
113
114 if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
115 log::debug!("diagnostics updated for server {language_server_id}. recording change");
116 } else {
117 log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
118 buffer_diagnostics_editor.update_all_excerpts(window, cx);
119 }
120 }
121 }
122 _ => {}
123 },
124 );
125
126 let focus_handle = cx.focus_handle();
127
128 cx.on_focus_in(
129 &focus_handle,
130 window,
131 |buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
132 )
133 .detach();
134
135 cx.on_focus_out(
136 &focus_handle,
137 window,
138 |buffer_diagnostics_editor, _event, window, cx| {
139 buffer_diagnostics_editor.focus_out(window, cx)
140 },
141 )
142 .detach();
143
144 let summary = project_handle
145 .read(cx)
146 .diagnostic_summary_for_path(&project_path, cx);
147
148 let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
149 let max_severity = Self::max_diagnostics_severity(include_warnings);
150 let editor = cx.new(|cx| {
151 let mut editor = Editor::for_multibuffer(
152 multibuffer.clone(),
153 Some(project_handle.clone()),
154 window,
155 cx,
156 );
157 editor.set_vertical_scroll_margin(5, cx);
158 editor.disable_inline_diagnostics();
159 editor.set_max_diagnostics_severity(max_severity, cx);
160 editor.set_all_diagnostics_active(cx);
161 editor
162 });
163
164 // Subscribe to events triggered by the editor in order to correctly
165 // update the buffer's excerpts.
166 cx.subscribe_in(
167 &editor,
168 window,
169 |buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
170 cx.emit(event.clone());
171
172 match event {
173 // If the user tries to focus on the editor but there's actually
174 // no excerpts for the buffer, focus back on the
175 // `BufferDiagnosticsEditor` instance.
176 EditorEvent::Focused => {
177 if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
178 window.focus(&buffer_diagnostics_editor.focus_handle);
179 }
180 }
181 EditorEvent::Blurred => {
182 buffer_diagnostics_editor.update_all_excerpts(window, cx)
183 }
184 _ => {}
185 }
186 },
187 )
188 .detach();
189
190 let diagnostics = vec![];
191 let update_excerpts_task = None;
192 let mut buffer_diagnostics_editor = Self {
193 project: project_handle,
194 focus_handle,
195 editor,
196 diagnostics,
197 blocks: Default::default(),
198 multibuffer,
199 buffer,
200 project_path,
201 summary,
202 include_warnings,
203 update_excerpts_task,
204 _subscription: project_event_subscription,
205 };
206
207 buffer_diagnostics_editor.update_all_diagnostics(window, cx);
208 buffer_diagnostics_editor
209 }
210
211 fn deploy(
212 workspace: &mut Workspace,
213 _: &DeployCurrentFile,
214 window: &mut Window,
215 cx: &mut Context<Workspace>,
216 ) {
217 // Determine the currently opened path by finding the active editor and
218 // finding the project path for the buffer.
219 // If there's no active editor with a project path, avoiding deploying
220 // the buffer diagnostics view.
221 if let Some(editor) = workspace.active_item_as::<Editor>(cx)
222 && let Some(project_path) = editor.project_path(cx)
223 {
224 // Check if there's already a `BufferDiagnosticsEditor` tab for this
225 // same path, and if so, focus on that one instead of creating a new
226 // one.
227 let existing_editor = workspace
228 .items_of_type::<BufferDiagnosticsEditor>(cx)
229 .find(|editor| editor.read(cx).project_path == project_path);
230
231 if let Some(editor) = existing_editor {
232 workspace.activate_item(&editor, true, true, window, cx);
233 } else {
234 let include_warnings = match cx.try_global::<IncludeWarnings>() {
235 Some(include_warnings) => include_warnings.0,
236 None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
237 };
238
239 let item = cx.new(|cx| {
240 Self::new(
241 project_path,
242 workspace.project().clone(),
243 editor.read(cx).buffer().read(cx).as_singleton(),
244 include_warnings,
245 window,
246 cx,
247 )
248 });
249
250 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
251 }
252 }
253 }
254
255 pub fn register(
256 workspace: &mut Workspace,
257 _window: Option<&mut Window>,
258 _: &mut Context<Workspace>,
259 ) {
260 workspace.register_action(Self::deploy);
261 }
262
263 fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
264 self.update_all_excerpts(window, cx);
265 }
266
267 fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
268 let project = self.project.read(cx);
269
270 self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
271 }
272
273 /// Enqueue an update to the excerpts and diagnostic blocks being shown in
274 /// the editor.
275 pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
276 // If there's already a task updating the excerpts, early return and let
277 // the other task finish.
278 if self.update_excerpts_task.is_some() {
279 return;
280 }
281
282 let buffer = self.buffer.clone();
283
284 self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
285 cx.background_executor()
286 .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
287 .await;
288
289 if let Some(buffer) = buffer {
290 editor
291 .update_in(cx, |editor, window, cx| {
292 editor.update_excerpts(buffer, window, cx)
293 })?
294 .await?;
295 };
296
297 let _ = editor.update(cx, |editor, cx| {
298 editor.update_excerpts_task = None;
299 cx.notify();
300 });
301
302 Ok(())
303 }));
304 }
305
306 /// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
307 /// buffer.
308 fn update_excerpts(
309 &mut self,
310 buffer: Entity<Buffer>,
311 window: &mut Window,
312 cx: &mut Context<Self>,
313 ) -> Task<Result<()>> {
314 let was_empty = self.multibuffer.read(cx).is_empty();
315 let multibuffer_context = multibuffer_context_lines(cx);
316 let buffer_snapshot = buffer.read(cx).snapshot();
317 let buffer_snapshot_max = buffer_snapshot.max_point();
318 let max_severity = Self::max_diagnostics_severity(self.include_warnings)
319 .into_lsp()
320 .unwrap_or(lsp::DiagnosticSeverity::WARNING);
321
322 cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
323 // Fetch the diagnostics for the whole of the buffer
324 // (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
325 // if the diagnostics changed, if it didn't, early return as there's
326 // nothing to update.
327 let diagnostics = buffer_snapshot
328 .diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
329 .collect::<Vec<_>>();
330
331 let unchanged =
332 buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
333 if buffer_diagnostics_editor
334 .diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
335 {
336 return true;
337 }
338
339 buffer_diagnostics_editor.set_diagnostics(&diagnostics);
340 return false;
341 })?;
342
343 if unchanged {
344 return Ok(());
345 }
346
347 // Mapping between the Group ID and a vector of DiagnosticEntry.
348 let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
349 for entry in diagnostics {
350 grouped
351 .entry(entry.diagnostic.group_id)
352 .or_default()
353 .push(DiagnosticEntryRef {
354 range: entry.range.to_point(&buffer_snapshot),
355 diagnostic: entry.diagnostic,
356 })
357 }
358
359 let mut blocks: Vec<DiagnosticBlock> = Vec::new();
360 for (_, group) in grouped {
361 // If the minimum severity of the group is higher than the
362 // maximum severity, or it doesn't even have severity, skip this
363 // group.
364 if group
365 .iter()
366 .map(|d| d.diagnostic.severity)
367 .min()
368 .is_none_or(|severity| severity > max_severity)
369 {
370 continue;
371 }
372
373 let languages = buffer_diagnostics_editor
374 .read_with(cx, |b, cx| b.project.read(cx).languages().clone())
375 .ok();
376
377 let diagnostic_blocks = cx.update(|_window, cx| {
378 DiagnosticRenderer::diagnostic_blocks_for_group(
379 group,
380 buffer_snapshot.remote_id(),
381 Some(Arc::new(buffer_diagnostics_editor.clone())),
382 languages,
383 cx,
384 )
385 })?;
386
387 // For each of the diagnostic blocks to be displayed in the
388 // editor, figure out its index in the list of blocks.
389 //
390 // The following rules are used to determine the order:
391 // 1. Blocks with a lower start position should come first.
392 // 2. If two blocks have the same start position, the one with
393 // the higher end position should come first.
394 for diagnostic_block in diagnostic_blocks {
395 let index = blocks.partition_point(|probe| {
396 match probe
397 .initial_range
398 .start
399 .cmp(&diagnostic_block.initial_range.start)
400 {
401 Ordering::Less => true,
402 Ordering::Greater => false,
403 Ordering::Equal => {
404 probe.initial_range.end > diagnostic_block.initial_range.end
405 }
406 }
407 });
408
409 blocks.insert(index, diagnostic_block);
410 }
411 }
412
413 // Build the excerpt ranges for this specific buffer's diagnostics,
414 // so those excerpts can later be used to update the excerpts shown
415 // in the editor.
416 // This is done by iterating over the list of diagnostic blocks and
417 // determine what range does the diagnostic block span.
418 let mut excerpt_ranges: Vec<ExcerptRange<_>> = Vec::new();
419
420 for diagnostic_block in blocks.iter() {
421 let excerpt_range = context_range_for_entry(
422 diagnostic_block.initial_range.clone(),
423 multibuffer_context,
424 buffer_snapshot.clone(),
425 &mut cx,
426 )
427 .await;
428 let initial_range = buffer_snapshot
429 .anchor_after(diagnostic_block.initial_range.start)
430 ..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);
431
432 let bin_search = |probe: &ExcerptRange<text::Anchor>| {
433 let context_start = || {
434 probe
435 .context
436 .start
437 .cmp(&excerpt_range.start, &buffer_snapshot)
438 };
439 let context_end =
440 || probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
441 let primary_start = || {
442 probe
443 .primary
444 .start
445 .cmp(&initial_range.start, &buffer_snapshot)
446 };
447 let primary_end =
448 || probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
449 context_start()
450 .then_with(context_end)
451 .then_with(primary_start)
452 .then_with(primary_end)
453 .then(cmp::Ordering::Greater)
454 };
455
456 let index = excerpt_ranges
457 .binary_search_by(bin_search)
458 .unwrap_or_else(|i| i);
459
460 excerpt_ranges.insert(
461 index,
462 ExcerptRange {
463 context: excerpt_range,
464 primary: initial_range,
465 },
466 )
467 }
468
469 // Finally, update the editor's content with the new excerpt ranges
470 // for this editor, as well as the diagnostic blocks.
471 buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
472 // Remove the list of `CustomBlockId` from the editor's display
473 // map, ensuring that if any diagnostics have been solved, the
474 // associated block stops being shown.
475 let block_ids = buffer_diagnostics_editor.blocks.clone();
476
477 buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
478 editor.display_map.update(cx, |display_map, cx| {
479 display_map.remove_blocks(block_ids.into_iter().collect(), cx);
480 })
481 });
482
483 let (anchor_ranges, _) =
484 buffer_diagnostics_editor
485 .multibuffer
486 .update(cx, |multibuffer, cx| {
487 let excerpt_ranges = excerpt_ranges
488 .into_iter()
489 .map(|range| ExcerptRange {
490 context: range.context.to_point(&buffer_snapshot),
491 primary: range.primary.to_point(&buffer_snapshot),
492 })
493 .collect();
494 multibuffer.set_excerpt_ranges_for_path(
495 PathKey::for_buffer(&buffer, cx),
496 buffer.clone(),
497 &buffer_snapshot,
498 excerpt_ranges,
499 cx,
500 )
501 });
502
503 if was_empty {
504 if let Some(anchor_range) = anchor_ranges.first() {
505 let range_to_select = anchor_range.start..anchor_range.start;
506
507 buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
508 editor.change_selections(Default::default(), window, cx, |selection| {
509 selection.select_anchor_ranges([range_to_select])
510 })
511 });
512
513 // If the `BufferDiagnosticsEditor` is currently
514 // focused, move focus to its editor.
515 if buffer_diagnostics_editor.focus_handle.is_focused(window) {
516 buffer_diagnostics_editor
517 .editor
518 .read(cx)
519 .focus_handle(cx)
520 .focus(window);
521 }
522 }
523 }
524
525 // Cloning the blocks before moving ownership so these can later
526 // be used to set the block contents for testing purposes.
527 #[cfg(test)]
528 let cloned_blocks = blocks.clone();
529
530 // Build new diagnostic blocks to be added to the editor's
531 // display map for the new diagnostics. Update the `blocks`
532 // property before finishing, to ensure the blocks are removed
533 // on the next execution.
534 let editor_blocks =
535 anchor_ranges
536 .into_iter()
537 .zip(blocks.into_iter())
538 .map(|(anchor, block)| {
539 let editor = buffer_diagnostics_editor.editor.downgrade();
540
541 BlockProperties {
542 placement: BlockPlacement::Near(anchor.start),
543 height: Some(1),
544 style: BlockStyle::Flex,
545 render: Arc::new(move |block_context| {
546 block.render_block(editor.clone(), block_context)
547 }),
548 priority: 1,
549 }
550 });
551
552 let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
553 editor.display_map.update(cx, |display_map, cx| {
554 display_map.insert_blocks(editor_blocks, cx)
555 })
556 });
557
558 // In order to be able to verify which diagnostic blocks are
559 // rendered in the editor, the `set_block_content_for_tests`
560 // function must be used, so that the
561 // `editor::test::editor_content_with_blocks` function can then
562 // be called to fetch these blocks.
563 #[cfg(test)]
564 {
565 for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
566 let markdown = block.markdown.clone();
567 editor::test::set_block_content_for_tests(
568 &buffer_diagnostics_editor.editor,
569 *block_id,
570 cx,
571 move |cx| {
572 markdown::MarkdownElement::rendered_text(
573 markdown.clone(),
574 cx,
575 editor::hover_popover::diagnostics_markdown_style,
576 )
577 },
578 );
579 }
580 }
581
582 buffer_diagnostics_editor.blocks = block_ids;
583 cx.notify()
584 })
585 })
586 }
587
588 fn set_diagnostics(&mut self, diagnostics: &[DiagnosticEntryRef<'_, Anchor>]) {
589 self.diagnostics = diagnostics
590 .iter()
591 .map(DiagnosticEntryRef::to_owned)
592 .collect();
593 }
594
595 fn diagnostics_are_unchanged(
596 &self,
597 diagnostics: &Vec<DiagnosticEntryRef<'_, Anchor>>,
598 snapshot: &BufferSnapshot,
599 ) -> bool {
600 if self.diagnostics.len() != diagnostics.len() {
601 return false;
602 }
603
604 self.diagnostics
605 .iter()
606 .zip(diagnostics.iter())
607 .all(|(existing, new)| {
608 existing.diagnostic.message == new.diagnostic.message
609 && existing.diagnostic.severity == new.diagnostic.severity
610 && existing.diagnostic.is_primary == new.diagnostic.is_primary
611 && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
612 })
613 }
614
615 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
616 // If the `BufferDiagnosticsEditor` is focused and the multibuffer is
617 // not empty, focus on the editor instead, which will allow the user to
618 // start interacting and editing the buffer's contents.
619 if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
620 self.editor.focus_handle(cx).focus(window)
621 }
622 }
623
624 fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
625 if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
626 {
627 self.update_all_excerpts(window, cx);
628 }
629 }
630
631 pub fn toggle_warnings(
632 &mut self,
633 _: &ToggleWarnings,
634 window: &mut Window,
635 cx: &mut Context<Self>,
636 ) {
637 let include_warnings = !self.include_warnings;
638 let max_severity = Self::max_diagnostics_severity(include_warnings);
639
640 self.editor.update(cx, |editor, cx| {
641 editor.set_max_diagnostics_severity(max_severity, cx);
642 });
643
644 self.include_warnings = include_warnings;
645 self.diagnostics.clear();
646 self.update_all_diagnostics(window, cx);
647 }
648
649 fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
650 match include_warnings {
651 true => DiagnosticSeverity::Warning,
652 false => DiagnosticSeverity::Error,
653 }
654 }
655
656 #[cfg(test)]
657 pub fn editor(&self) -> &Entity<Editor> {
658 &self.editor
659 }
660
661 #[cfg(test)]
662 pub fn summary(&self) -> &DiagnosticSummary {
663 &self.summary
664 }
665}
666
667impl Focusable for BufferDiagnosticsEditor {
668 fn focus_handle(&self, _: &App) -> FocusHandle {
669 self.focus_handle.clone()
670 }
671}
672
673impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
674
675impl Item for BufferDiagnosticsEditor {
676 type Event = EditorEvent;
677
678 fn act_as_type<'a>(
679 &'a self,
680 type_id: std::any::TypeId,
681 self_handle: &'a Entity<Self>,
682 _: &'a App,
683 ) -> Option<gpui::AnyView> {
684 if type_id == TypeId::of::<Self>() {
685 Some(self_handle.to_any())
686 } else if type_id == TypeId::of::<Editor>() {
687 Some(self.editor.to_any())
688 } else {
689 None
690 }
691 }
692
693 fn added_to_workspace(
694 &mut self,
695 workspace: &mut Workspace,
696 window: &mut Window,
697 cx: &mut Context<Self>,
698 ) {
699 self.editor.update(cx, |editor, cx| {
700 editor.added_to_workspace(workspace, window, cx)
701 });
702 }
703
704 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
705 ToolbarItemLocation::PrimaryLeft
706 }
707
708 fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
709 self.editor.breadcrumbs(theme, cx)
710 }
711
712 fn can_save(&self, _cx: &App) -> bool {
713 true
714 }
715
716 fn can_split(&self) -> bool {
717 true
718 }
719
720 fn clone_on_split(
721 &self,
722 _workspace_id: Option<workspace::WorkspaceId>,
723 window: &mut Window,
724 cx: &mut Context<Self>,
725 ) -> Task<Option<Entity<Self>>>
726 where
727 Self: Sized,
728 {
729 Task::ready(Some(cx.new(|cx| {
730 BufferDiagnosticsEditor::new(
731 self.project_path.clone(),
732 self.project.clone(),
733 self.buffer.clone(),
734 self.include_warnings,
735 window,
736 cx,
737 )
738 })))
739 }
740
741 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
742 self.editor
743 .update(cx, |editor, cx| editor.deactivated(window, cx));
744 }
745
746 fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
747 self.editor.for_each_project_item(cx, f);
748 }
749
750 fn has_conflict(&self, cx: &App) -> bool {
751 self.multibuffer.read(cx).has_conflict(cx)
752 }
753
754 fn has_deleted_file(&self, cx: &App) -> bool {
755 self.multibuffer.read(cx).has_deleted_file(cx)
756 }
757
758 fn is_dirty(&self, cx: &App) -> bool {
759 self.multibuffer.read(cx).is_dirty(cx)
760 }
761
762 fn navigate(
763 &mut self,
764 data: Box<dyn Any>,
765 window: &mut Window,
766 cx: &mut Context<Self>,
767 ) -> bool {
768 self.editor
769 .update(cx, |editor, cx| editor.navigate(data, window, cx))
770 }
771
772 fn reload(
773 &mut self,
774 project: Entity<Project>,
775 window: &mut Window,
776 cx: &mut Context<Self>,
777 ) -> Task<Result<()>> {
778 self.editor.reload(project, window, cx)
779 }
780
781 fn save(
782 &mut self,
783 options: workspace::item::SaveOptions,
784 project: Entity<Project>,
785 window: &mut Window,
786 cx: &mut Context<Self>,
787 ) -> Task<Result<()>> {
788 self.editor.save(options, project, window, cx)
789 }
790
791 fn save_as(
792 &mut self,
793 _project: Entity<Project>,
794 _path: ProjectPath,
795 _window: &mut Window,
796 _cx: &mut Context<Self>,
797 ) -> Task<Result<()>> {
798 unreachable!()
799 }
800
801 fn set_nav_history(
802 &mut self,
803 nav_history: ItemNavHistory,
804 _window: &mut Window,
805 cx: &mut Context<Self>,
806 ) {
807 self.editor.update(cx, |editor, _| {
808 editor.set_nav_history(Some(nav_history));
809 })
810 }
811
812 // Builds the content to be displayed in the tab.
813 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
814 let path_style = self.project.read(cx).path_style(cx);
815 let error_count = self.summary.error_count;
816 let warning_count = self.summary.warning_count;
817 let label = Label::new(
818 self.project_path
819 .path
820 .file_name()
821 .map(|s| s.to_string())
822 .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
823 );
824
825 h_flex()
826 .gap_1()
827 .child(label)
828 .when(error_count == 0 && warning_count == 0, |parent| {
829 parent.child(
830 h_flex()
831 .gap_1()
832 .child(Icon::new(IconName::Check).color(Color::Success)),
833 )
834 })
835 .when(error_count > 0, |parent| {
836 parent.child(
837 h_flex()
838 .gap_1()
839 .child(Icon::new(IconName::XCircle).color(Color::Error))
840 .child(Label::new(error_count.to_string()).color(params.text_color())),
841 )
842 })
843 .when(warning_count > 0, |parent| {
844 parent.child(
845 h_flex()
846 .gap_1()
847 .child(Icon::new(IconName::Warning).color(Color::Warning))
848 .child(Label::new(warning_count.to_string()).color(params.text_color())),
849 )
850 })
851 .into_any_element()
852 }
853
854 fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
855 "Buffer Diagnostics".into()
856 }
857
858 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
859 let path_style = self.project.read(cx).path_style(cx);
860 Some(
861 format!(
862 "Buffer Diagnostics - {}",
863 self.project_path.path.display(path_style)
864 )
865 .into(),
866 )
867 }
868
869 fn telemetry_event_text(&self) -> Option<&'static str> {
870 Some("Buffer Diagnostics Opened")
871 }
872
873 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
874 Editor::to_item_events(event, f)
875 }
876}
877
878impl Render for BufferDiagnosticsEditor {
879 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
880 let path_style = self.project.read(cx).path_style(cx);
881 let filename = self.project_path.path.display(path_style).to_string();
882 let error_count = self.summary.error_count;
883 let warning_count = match self.include_warnings {
884 true => self.summary.warning_count,
885 false => 0,
886 };
887
888 let child = if error_count + warning_count == 0 {
889 let label = match warning_count {
890 0 => "No problems in",
891 _ => "No errors in",
892 };
893
894 v_flex()
895 .key_context("EmptyPane")
896 .size_full()
897 .gap_1()
898 .justify_center()
899 .items_center()
900 .text_center()
901 .bg(cx.theme().colors().editor_background)
902 .child(
903 div()
904 .h_flex()
905 .child(Label::new(label).color(Color::Muted))
906 .child(
907 Button::new("open-file", filename)
908 .style(ButtonStyle::Transparent)
909 .tooltip(Tooltip::text("Open File"))
910 .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
911 if let Some(workspace) = window.root::<Workspace>().flatten() {
912 workspace.update(cx, |workspace, cx| {
913 workspace
914 .open_path(
915 buffer_diagnostics.project_path.clone(),
916 None,
917 true,
918 window,
919 cx,
920 )
921 .detach_and_log_err(cx);
922 })
923 }
924 })),
925 ),
926 )
927 .when(self.summary.warning_count > 0, |div| {
928 let label = match self.summary.warning_count {
929 1 => "Show 1 warning".into(),
930 warning_count => format!("Show {} warnings", warning_count),
931 };
932
933 div.child(
934 Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
935 |buffer_diagnostics_editor, _, window, cx| {
936 buffer_diagnostics_editor.toggle_warnings(
937 &Default::default(),
938 window,
939 cx,
940 );
941 cx.notify();
942 },
943 )),
944 )
945 })
946 } else {
947 div().size_full().child(self.editor.clone())
948 };
949
950 div()
951 .key_context("Diagnostics")
952 .track_focus(&self.focus_handle(cx))
953 .size_full()
954 .child(child)
955 }
956}
957
958impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
959 fn include_warnings(&self, cx: &App) -> bool {
960 self.read_with(cx, |buffer_diagnostics_editor, _cx| {
961 buffer_diagnostics_editor.include_warnings
962 })
963 .unwrap_or(false)
964 }
965
966 fn is_updating(&self, cx: &App) -> bool {
967 self.read_with(cx, |buffer_diagnostics_editor, cx| {
968 buffer_diagnostics_editor.update_excerpts_task.is_some()
969 || buffer_diagnostics_editor
970 .project
971 .read(cx)
972 .language_servers_running_disk_based_diagnostics(cx)
973 .next()
974 .is_some()
975 })
976 .unwrap_or(false)
977 }
978
979 fn stop_updating(&self, cx: &mut App) {
980 let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
981 buffer_diagnostics_editor.update_excerpts_task = None;
982 cx.notify();
983 });
984 }
985
986 fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
987 let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
988 buffer_diagnostics_editor.update_all_excerpts(window, cx);
989 });
990 }
991
992 fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
993 let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
994 buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
995 });
996 }
997
998 fn get_diagnostics_for_buffer(
999 &self,
1000 _buffer_id: text::BufferId,
1001 cx: &App,
1002 ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
1003 self.read_with(cx, |buffer_diagnostics_editor, _cx| {
1004 buffer_diagnostics_editor.diagnostics.clone()
1005 })
1006 .unwrap_or_default()
1007 }
1008}