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