buffer_diagnostics.rs

  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::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 diagnostic_blocks = cx.update(|_window, cx| {
374                    DiagnosticRenderer::diagnostic_blocks_for_group(
375                        group,
376                        buffer_snapshot.remote_id(),
377                        Some(Arc::new(buffer_diagnostics_editor.clone())),
378                        cx,
379                    )
380                })?;
381
382                // For each of the diagnostic blocks to be displayed in the
383                // editor, figure out its index in the list of blocks.
384                //
385                // The following rules are used to determine the order:
386                // 1. Blocks with a lower start position should come first.
387                // 2. If two blocks have the same start position, the one with
388                // the higher end position should come first.
389                for diagnostic_block in diagnostic_blocks {
390                    let index = blocks.partition_point(|probe| {
391                        match probe
392                            .initial_range
393                            .start
394                            .cmp(&diagnostic_block.initial_range.start)
395                        {
396                            Ordering::Less => true,
397                            Ordering::Greater => false,
398                            Ordering::Equal => {
399                                probe.initial_range.end > diagnostic_block.initial_range.end
400                            }
401                        }
402                    });
403
404                    blocks.insert(index, diagnostic_block);
405                }
406            }
407
408            // Build the excerpt ranges for this specific buffer's diagnostics,
409            // so those excerpts can later be used to update the excerpts shown
410            // in the editor.
411            // This is done by iterating over the list of diagnostic blocks and
412            // determine what range does the diagnostic block span.
413            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
414
415            for diagnostic_block in blocks.iter() {
416                let excerpt_range = context_range_for_entry(
417                    diagnostic_block.initial_range.clone(),
418                    multibuffer_context,
419                    buffer_snapshot.clone(),
420                    &mut cx,
421                )
422                .await;
423
424                let index = excerpt_ranges
425                    .binary_search_by(|probe| {
426                        probe
427                            .context
428                            .start
429                            .cmp(&excerpt_range.start)
430                            .then(probe.context.end.cmp(&excerpt_range.end))
431                            .then(
432                                probe
433                                    .primary
434                                    .start
435                                    .cmp(&diagnostic_block.initial_range.start),
436                            )
437                            .then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
438                            .then(Ordering::Greater)
439                    })
440                    .unwrap_or_else(|index| index);
441
442                excerpt_ranges.insert(
443                    index,
444                    ExcerptRange {
445                        context: excerpt_range,
446                        primary: diagnostic_block.initial_range.clone(),
447                    },
448                )
449            }
450
451            // Finally, update the editor's content with the new excerpt ranges
452            // for this editor, as well as the diagnostic blocks.
453            buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
454                // Remove the list of `CustomBlockId` from the editor's display
455                // map, ensuring that if any diagnostics have been solved, the
456                // associated block stops being shown.
457                let block_ids = buffer_diagnostics_editor.blocks.clone();
458
459                buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
460                    editor.display_map.update(cx, |display_map, cx| {
461                        display_map.remove_blocks(block_ids.into_iter().collect(), cx);
462                    })
463                });
464
465                let (anchor_ranges, _) =
466                    buffer_diagnostics_editor
467                        .multibuffer
468                        .update(cx, |multibuffer, cx| {
469                            multibuffer.set_excerpt_ranges_for_path(
470                                PathKey::for_buffer(&buffer, cx),
471                                buffer.clone(),
472                                &buffer_snapshot,
473                                excerpt_ranges,
474                                cx,
475                            )
476                        });
477
478                if was_empty {
479                    if let Some(anchor_range) = anchor_ranges.first() {
480                        let range_to_select = anchor_range.start..anchor_range.start;
481
482                        buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
483                            editor.change_selections(Default::default(), window, cx, |selection| {
484                                selection.select_anchor_ranges([range_to_select])
485                            })
486                        });
487
488                        // If the `BufferDiagnosticsEditor` is currently
489                        // focused, move focus to its editor.
490                        if buffer_diagnostics_editor.focus_handle.is_focused(window) {
491                            buffer_diagnostics_editor
492                                .editor
493                                .read(cx)
494                                .focus_handle(cx)
495                                .focus(window);
496                        }
497                    }
498                }
499
500                // Cloning the blocks before moving ownership so these can later
501                // be used to set the block contents for testing purposes.
502                #[cfg(test)]
503                let cloned_blocks = blocks.clone();
504
505                // Build new diagnostic blocks to be added to the editor's
506                // display map for the new diagnostics. Update the `blocks`
507                // property before finishing, to ensure the blocks are removed
508                // on the next execution.
509                let editor_blocks =
510                    anchor_ranges
511                        .into_iter()
512                        .zip(blocks.into_iter())
513                        .map(|(anchor, block)| {
514                            let editor = buffer_diagnostics_editor.editor.downgrade();
515
516                            BlockProperties {
517                                placement: BlockPlacement::Near(anchor.start),
518                                height: Some(1),
519                                style: BlockStyle::Flex,
520                                render: Arc::new(move |block_context| {
521                                    block.render_block(editor.clone(), block_context)
522                                }),
523                                priority: 1,
524                            }
525                        });
526
527                let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
528                    editor.display_map.update(cx, |display_map, cx| {
529                        display_map.insert_blocks(editor_blocks, cx)
530                    })
531                });
532
533                // In order to be able to verify which diagnostic blocks are
534                // rendered in the editor, the `set_block_content_for_tests`
535                // function must be used, so that the
536                // `editor::test::editor_content_with_blocks` function can then
537                // be called to fetch these blocks.
538                #[cfg(test)]
539                {
540                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
541                        let markdown = block.markdown.clone();
542                        editor::test::set_block_content_for_tests(
543                            &buffer_diagnostics_editor.editor,
544                            *block_id,
545                            cx,
546                            move |cx| {
547                                markdown::MarkdownElement::rendered_text(
548                                    markdown.clone(),
549                                    cx,
550                                    editor::hover_popover::diagnostics_markdown_style,
551                                )
552                            },
553                        );
554                    }
555                }
556
557                buffer_diagnostics_editor.blocks = block_ids;
558                cx.notify()
559            })
560        })
561    }
562
563    fn set_diagnostics(&mut self, diagnostics: &[DiagnosticEntryRef<'_, Anchor>]) {
564        self.diagnostics = diagnostics
565            .iter()
566            .map(DiagnosticEntryRef::to_owned)
567            .collect();
568    }
569
570    fn diagnostics_are_unchanged(
571        &self,
572        diagnostics: &Vec<DiagnosticEntryRef<'_, Anchor>>,
573        snapshot: &BufferSnapshot,
574    ) -> bool {
575        if self.diagnostics.len() != diagnostics.len() {
576            return false;
577        }
578
579        self.diagnostics
580            .iter()
581            .zip(diagnostics.iter())
582            .all(|(existing, new)| {
583                existing.diagnostic.message == new.diagnostic.message
584                    && existing.diagnostic.severity == new.diagnostic.severity
585                    && existing.diagnostic.is_primary == new.diagnostic.is_primary
586                    && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
587            })
588    }
589
590    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
591        // If the `BufferDiagnosticsEditor` is focused and the multibuffer is
592        // not empty, focus on the editor instead, which will allow the user to
593        // start interacting and editing the buffer's contents.
594        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
595            self.editor.focus_handle(cx).focus(window)
596        }
597    }
598
599    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
600        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
601        {
602            self.update_all_excerpts(window, cx);
603        }
604    }
605
606    pub fn toggle_warnings(
607        &mut self,
608        _: &ToggleWarnings,
609        window: &mut Window,
610        cx: &mut Context<Self>,
611    ) {
612        let include_warnings = !self.include_warnings;
613        let max_severity = Self::max_diagnostics_severity(include_warnings);
614
615        self.editor.update(cx, |editor, cx| {
616            editor.set_max_diagnostics_severity(max_severity, cx);
617        });
618
619        self.include_warnings = include_warnings;
620        self.diagnostics.clear();
621        self.update_all_diagnostics(window, cx);
622    }
623
624    fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
625        match include_warnings {
626            true => DiagnosticSeverity::Warning,
627            false => DiagnosticSeverity::Error,
628        }
629    }
630
631    #[cfg(test)]
632    pub fn editor(&self) -> &Entity<Editor> {
633        &self.editor
634    }
635
636    #[cfg(test)]
637    pub fn summary(&self) -> &DiagnosticSummary {
638        &self.summary
639    }
640}
641
642impl Focusable for BufferDiagnosticsEditor {
643    fn focus_handle(&self, _: &App) -> FocusHandle {
644        self.focus_handle.clone()
645    }
646}
647
648impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
649
650impl Item for BufferDiagnosticsEditor {
651    type Event = EditorEvent;
652
653    fn act_as_type<'a>(
654        &'a self,
655        type_id: std::any::TypeId,
656        self_handle: &'a Entity<Self>,
657        _: &'a App,
658    ) -> Option<gpui::AnyView> {
659        if type_id == TypeId::of::<Self>() {
660            Some(self_handle.to_any())
661        } else if type_id == TypeId::of::<Editor>() {
662            Some(self.editor.to_any())
663        } else {
664            None
665        }
666    }
667
668    fn added_to_workspace(
669        &mut self,
670        workspace: &mut Workspace,
671        window: &mut Window,
672        cx: &mut Context<Self>,
673    ) {
674        self.editor.update(cx, |editor, cx| {
675            editor.added_to_workspace(workspace, window, cx)
676        });
677    }
678
679    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
680        ToolbarItemLocation::PrimaryLeft
681    }
682
683    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
684        self.editor.breadcrumbs(theme, cx)
685    }
686
687    fn can_save(&self, _cx: &App) -> bool {
688        true
689    }
690
691    fn can_split(&self) -> bool {
692        true
693    }
694
695    fn clone_on_split(
696        &self,
697        _workspace_id: Option<workspace::WorkspaceId>,
698        window: &mut Window,
699        cx: &mut Context<Self>,
700    ) -> Task<Option<Entity<Self>>>
701    where
702        Self: Sized,
703    {
704        Task::ready(Some(cx.new(|cx| {
705            BufferDiagnosticsEditor::new(
706                self.project_path.clone(),
707                self.project.clone(),
708                self.buffer.clone(),
709                self.include_warnings,
710                window,
711                cx,
712            )
713        })))
714    }
715
716    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
717        self.editor
718            .update(cx, |editor, cx| editor.deactivated(window, cx));
719    }
720
721    fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
722        self.editor.for_each_project_item(cx, f);
723    }
724
725    fn has_conflict(&self, cx: &App) -> bool {
726        self.multibuffer.read(cx).has_conflict(cx)
727    }
728
729    fn has_deleted_file(&self, cx: &App) -> bool {
730        self.multibuffer.read(cx).has_deleted_file(cx)
731    }
732
733    fn is_dirty(&self, cx: &App) -> bool {
734        self.multibuffer.read(cx).is_dirty(cx)
735    }
736
737    fn navigate(
738        &mut self,
739        data: Box<dyn Any>,
740        window: &mut Window,
741        cx: &mut Context<Self>,
742    ) -> bool {
743        self.editor
744            .update(cx, |editor, cx| editor.navigate(data, window, cx))
745    }
746
747    fn reload(
748        &mut self,
749        project: Entity<Project>,
750        window: &mut Window,
751        cx: &mut Context<Self>,
752    ) -> Task<Result<()>> {
753        self.editor.reload(project, window, cx)
754    }
755
756    fn save(
757        &mut self,
758        options: workspace::item::SaveOptions,
759        project: Entity<Project>,
760        window: &mut Window,
761        cx: &mut Context<Self>,
762    ) -> Task<Result<()>> {
763        self.editor.save(options, project, window, cx)
764    }
765
766    fn save_as(
767        &mut self,
768        _project: Entity<Project>,
769        _path: ProjectPath,
770        _window: &mut Window,
771        _cx: &mut Context<Self>,
772    ) -> Task<Result<()>> {
773        unreachable!()
774    }
775
776    fn set_nav_history(
777        &mut self,
778        nav_history: ItemNavHistory,
779        _window: &mut Window,
780        cx: &mut Context<Self>,
781    ) {
782        self.editor.update(cx, |editor, _| {
783            editor.set_nav_history(Some(nav_history));
784        })
785    }
786
787    // Builds the content to be displayed in the tab.
788    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
789        let path_style = self.project.read(cx).path_style(cx);
790        let error_count = self.summary.error_count;
791        let warning_count = self.summary.warning_count;
792        let label = Label::new(
793            self.project_path
794                .path
795                .file_name()
796                .map(|s| s.to_string())
797                .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
798        );
799
800        h_flex()
801            .gap_1()
802            .child(label)
803            .when(error_count == 0 && warning_count == 0, |parent| {
804                parent.child(
805                    h_flex()
806                        .gap_1()
807                        .child(Icon::new(IconName::Check).color(Color::Success)),
808                )
809            })
810            .when(error_count > 0, |parent| {
811                parent.child(
812                    h_flex()
813                        .gap_1()
814                        .child(Icon::new(IconName::XCircle).color(Color::Error))
815                        .child(Label::new(error_count.to_string()).color(params.text_color())),
816                )
817            })
818            .when(warning_count > 0, |parent| {
819                parent.child(
820                    h_flex()
821                        .gap_1()
822                        .child(Icon::new(IconName::Warning).color(Color::Warning))
823                        .child(Label::new(warning_count.to_string()).color(params.text_color())),
824                )
825            })
826            .into_any_element()
827    }
828
829    fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
830        "Buffer Diagnostics".into()
831    }
832
833    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
834        let path_style = self.project.read(cx).path_style(cx);
835        Some(
836            format!(
837                "Buffer Diagnostics - {}",
838                self.project_path.path.display(path_style)
839            )
840            .into(),
841        )
842    }
843
844    fn telemetry_event_text(&self) -> Option<&'static str> {
845        Some("Buffer Diagnostics Opened")
846    }
847
848    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
849        Editor::to_item_events(event, f)
850    }
851}
852
853impl Render for BufferDiagnosticsEditor {
854    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
855        let path_style = self.project.read(cx).path_style(cx);
856        let filename = self.project_path.path.display(path_style).to_string();
857        let error_count = self.summary.error_count;
858        let warning_count = match self.include_warnings {
859            true => self.summary.warning_count,
860            false => 0,
861        };
862
863        let child = if error_count + warning_count == 0 {
864            let label = match warning_count {
865                0 => "No problems in",
866                _ => "No errors in",
867            };
868
869            v_flex()
870                .key_context("EmptyPane")
871                .size_full()
872                .gap_1()
873                .justify_center()
874                .items_center()
875                .text_center()
876                .bg(cx.theme().colors().editor_background)
877                .child(
878                    div()
879                        .h_flex()
880                        .child(Label::new(label).color(Color::Muted))
881                        .child(
882                            Button::new("open-file", filename)
883                                .style(ButtonStyle::Transparent)
884                                .tooltip(Tooltip::text("Open File"))
885                                .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
886                                    if let Some(workspace) = window.root::<Workspace>().flatten() {
887                                        workspace.update(cx, |workspace, cx| {
888                                            workspace
889                                                .open_path(
890                                                    buffer_diagnostics.project_path.clone(),
891                                                    None,
892                                                    true,
893                                                    window,
894                                                    cx,
895                                                )
896                                                .detach_and_log_err(cx);
897                                        })
898                                    }
899                                })),
900                        ),
901                )
902                .when(self.summary.warning_count > 0, |div| {
903                    let label = match self.summary.warning_count {
904                        1 => "Show 1 warning".into(),
905                        warning_count => format!("Show {} warnings", warning_count),
906                    };
907
908                    div.child(
909                        Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
910                            |buffer_diagnostics_editor, _, window, cx| {
911                                buffer_diagnostics_editor.toggle_warnings(
912                                    &Default::default(),
913                                    window,
914                                    cx,
915                                );
916                                cx.notify();
917                            },
918                        )),
919                    )
920                })
921        } else {
922            div().size_full().child(self.editor.clone())
923        };
924
925        div()
926            .key_context("Diagnostics")
927            .track_focus(&self.focus_handle(cx))
928            .size_full()
929            .child(child)
930    }
931}
932
933impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
934    fn include_warnings(&self, cx: &App) -> bool {
935        self.read_with(cx, |buffer_diagnostics_editor, _cx| {
936            buffer_diagnostics_editor.include_warnings
937        })
938        .unwrap_or(false)
939    }
940
941    fn has_stale_excerpts(&self, _cx: &App) -> bool {
942        false
943    }
944
945    fn is_updating(&self, cx: &App) -> bool {
946        self.read_with(cx, |buffer_diagnostics_editor, cx| {
947            buffer_diagnostics_editor.update_excerpts_task.is_some()
948                || buffer_diagnostics_editor
949                    .project
950                    .read(cx)
951                    .language_servers_running_disk_based_diagnostics(cx)
952                    .next()
953                    .is_some()
954        })
955        .unwrap_or(false)
956    }
957
958    fn stop_updating(&self, cx: &mut App) {
959        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
960            buffer_diagnostics_editor.update_excerpts_task = None;
961            cx.notify();
962        });
963    }
964
965    fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
966        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
967            buffer_diagnostics_editor.update_all_excerpts(window, cx);
968        });
969    }
970
971    fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
972        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
973            buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
974        });
975    }
976
977    fn get_diagnostics_for_buffer(
978        &self,
979        _buffer_id: text::BufferId,
980        cx: &App,
981    ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
982        self.read_with(cx, |buffer_diagnostics_editor, _cx| {
983            buffer_diagnostics_editor.diagnostics.clone()
984        })
985        .unwrap_or_default()
986    }
987}