buffer_diagnostics.rs

  1use crate::{
  2    DIAGNOSTICS_UPDATE_DELAY, 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, 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_DELAY)
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(DiagnosticEntry {
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: &Vec<DiagnosticEntry<Anchor>>) {
564        self.diagnostics = diagnostics.clone();
565    }
566
567    fn diagnostics_are_unchanged(
568        &self,
569        diagnostics: &Vec<DiagnosticEntry<Anchor>>,
570        snapshot: &BufferSnapshot,
571    ) -> bool {
572        if self.diagnostics.len() != diagnostics.len() {
573            return false;
574        }
575
576        self.diagnostics
577            .iter()
578            .zip(diagnostics.iter())
579            .all(|(existing, new)| {
580                existing.diagnostic.message == new.diagnostic.message
581                    && existing.diagnostic.severity == new.diagnostic.severity
582                    && existing.diagnostic.is_primary == new.diagnostic.is_primary
583                    && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
584            })
585    }
586
587    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
588        // If the `BufferDiagnosticsEditor` is focused and the multibuffer is
589        // not empty, focus on the editor instead, which will allow the user to
590        // start interacting and editing the buffer's contents.
591        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
592            self.editor.focus_handle(cx).focus(window)
593        }
594    }
595
596    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
597        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
598        {
599            self.update_all_excerpts(window, cx);
600        }
601    }
602
603    pub fn toggle_warnings(
604        &mut self,
605        _: &ToggleWarnings,
606        window: &mut Window,
607        cx: &mut Context<Self>,
608    ) {
609        let include_warnings = !self.include_warnings;
610        let max_severity = Self::max_diagnostics_severity(include_warnings);
611
612        self.editor.update(cx, |editor, cx| {
613            editor.set_max_diagnostics_severity(max_severity, cx);
614        });
615
616        self.include_warnings = include_warnings;
617        self.diagnostics.clear();
618        self.update_all_diagnostics(window, cx);
619    }
620
621    fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
622        match include_warnings {
623            true => DiagnosticSeverity::Warning,
624            false => DiagnosticSeverity::Error,
625        }
626    }
627
628    #[cfg(test)]
629    pub fn editor(&self) -> &Entity<Editor> {
630        &self.editor
631    }
632
633    #[cfg(test)]
634    pub fn summary(&self) -> &DiagnosticSummary {
635        &self.summary
636    }
637}
638
639impl Focusable for BufferDiagnosticsEditor {
640    fn focus_handle(&self, _: &App) -> FocusHandle {
641        self.focus_handle.clone()
642    }
643}
644
645impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
646
647impl Item for BufferDiagnosticsEditor {
648    type Event = EditorEvent;
649
650    fn act_as_type<'a>(
651        &'a self,
652        type_id: std::any::TypeId,
653        self_handle: &'a Entity<Self>,
654        _: &'a App,
655    ) -> Option<gpui::AnyView> {
656        if type_id == TypeId::of::<Self>() {
657            Some(self_handle.to_any())
658        } else if type_id == TypeId::of::<Editor>() {
659            Some(self.editor.to_any())
660        } else {
661            None
662        }
663    }
664
665    fn added_to_workspace(
666        &mut self,
667        workspace: &mut Workspace,
668        window: &mut Window,
669        cx: &mut Context<Self>,
670    ) {
671        self.editor.update(cx, |editor, cx| {
672            editor.added_to_workspace(workspace, window, cx)
673        });
674    }
675
676    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
677        ToolbarItemLocation::PrimaryLeft
678    }
679
680    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
681        self.editor.breadcrumbs(theme, cx)
682    }
683
684    fn can_save(&self, _cx: &App) -> bool {
685        true
686    }
687
688    fn clone_on_split(
689        &self,
690        _workspace_id: Option<workspace::WorkspaceId>,
691        window: &mut Window,
692        cx: &mut Context<Self>,
693    ) -> Option<Entity<Self>>
694    where
695        Self: Sized,
696    {
697        Some(cx.new(|cx| {
698            BufferDiagnosticsEditor::new(
699                self.project_path.clone(),
700                self.project.clone(),
701                self.buffer.clone(),
702                self.include_warnings,
703                window,
704                cx,
705            )
706        }))
707    }
708
709    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
710        self.editor
711            .update(cx, |editor, cx| editor.deactivated(window, cx));
712    }
713
714    fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
715        self.editor.for_each_project_item(cx, f);
716    }
717
718    fn has_conflict(&self, cx: &App) -> bool {
719        self.multibuffer.read(cx).has_conflict(cx)
720    }
721
722    fn has_deleted_file(&self, cx: &App) -> bool {
723        self.multibuffer.read(cx).has_deleted_file(cx)
724    }
725
726    fn is_dirty(&self, cx: &App) -> bool {
727        self.multibuffer.read(cx).is_dirty(cx)
728    }
729
730    fn is_singleton(&self, _cx: &App) -> bool {
731        false
732    }
733
734    fn navigate(
735        &mut self,
736        data: Box<dyn Any>,
737        window: &mut Window,
738        cx: &mut Context<Self>,
739    ) -> bool {
740        self.editor
741            .update(cx, |editor, cx| editor.navigate(data, window, cx))
742    }
743
744    fn reload(
745        &mut self,
746        project: Entity<Project>,
747        window: &mut Window,
748        cx: &mut Context<Self>,
749    ) -> Task<Result<()>> {
750        self.editor.reload(project, window, cx)
751    }
752
753    fn save(
754        &mut self,
755        options: workspace::item::SaveOptions,
756        project: Entity<Project>,
757        window: &mut Window,
758        cx: &mut Context<Self>,
759    ) -> Task<Result<()>> {
760        self.editor.save(options, project, window, cx)
761    }
762
763    fn save_as(
764        &mut self,
765        _project: Entity<Project>,
766        _path: ProjectPath,
767        _window: &mut Window,
768        _cx: &mut Context<Self>,
769    ) -> Task<Result<()>> {
770        unreachable!()
771    }
772
773    fn set_nav_history(
774        &mut self,
775        nav_history: ItemNavHistory,
776        _window: &mut Window,
777        cx: &mut Context<Self>,
778    ) {
779        self.editor.update(cx, |editor, _| {
780            editor.set_nav_history(Some(nav_history));
781        })
782    }
783
784    // Builds the content to be displayed in the tab.
785    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
786        let path_style = self.project.read(cx).path_style(cx);
787        let error_count = self.summary.error_count;
788        let warning_count = self.summary.warning_count;
789        let label = Label::new(
790            self.project_path
791                .path
792                .file_name()
793                .map(|s| s.to_string())
794                .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
795        );
796
797        h_flex()
798            .gap_1()
799            .child(label)
800            .when(error_count == 0 && warning_count == 0, |parent| {
801                parent.child(
802                    h_flex()
803                        .gap_1()
804                        .child(Icon::new(IconName::Check).color(Color::Success)),
805                )
806            })
807            .when(error_count > 0, |parent| {
808                parent.child(
809                    h_flex()
810                        .gap_1()
811                        .child(Icon::new(IconName::XCircle).color(Color::Error))
812                        .child(Label::new(error_count.to_string()).color(params.text_color())),
813                )
814            })
815            .when(warning_count > 0, |parent| {
816                parent.child(
817                    h_flex()
818                        .gap_1()
819                        .child(Icon::new(IconName::Warning).color(Color::Warning))
820                        .child(Label::new(warning_count.to_string()).color(params.text_color())),
821                )
822            })
823            .into_any_element()
824    }
825
826    fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
827        "Buffer Diagnostics".into()
828    }
829
830    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
831        let path_style = self.project.read(cx).path_style(cx);
832        Some(
833            format!(
834                "Buffer Diagnostics - {}",
835                self.project_path.path.display(path_style)
836            )
837            .into(),
838        )
839    }
840
841    fn telemetry_event_text(&self) -> Option<&'static str> {
842        Some("Buffer Diagnostics Opened")
843    }
844
845    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
846        Editor::to_item_events(event, f)
847    }
848}
849
850impl Render for BufferDiagnosticsEditor {
851    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
852        let path_style = self.project.read(cx).path_style(cx);
853        let filename = self.project_path.path.display(path_style).to_string();
854        let error_count = self.summary.error_count;
855        let warning_count = match self.include_warnings {
856            true => self.summary.warning_count,
857            false => 0,
858        };
859
860        let child = if error_count + warning_count == 0 {
861            let label = match warning_count {
862                0 => "No problems in",
863                _ => "No errors in",
864            };
865
866            v_flex()
867                .key_context("EmptyPane")
868                .size_full()
869                .gap_1()
870                .justify_center()
871                .items_center()
872                .text_center()
873                .bg(cx.theme().colors().editor_background)
874                .child(
875                    div()
876                        .h_flex()
877                        .child(Label::new(label).color(Color::Muted))
878                        .child(
879                            Button::new("open-file", filename)
880                                .style(ButtonStyle::Transparent)
881                                .tooltip(Tooltip::text("Open File"))
882                                .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
883                                    if let Some(workspace) = window.root::<Workspace>().flatten() {
884                                        workspace.update(cx, |workspace, cx| {
885                                            workspace
886                                                .open_path(
887                                                    buffer_diagnostics.project_path.clone(),
888                                                    None,
889                                                    true,
890                                                    window,
891                                                    cx,
892                                                )
893                                                .detach_and_log_err(cx);
894                                        })
895                                    }
896                                })),
897                        ),
898                )
899                .when(self.summary.warning_count > 0, |div| {
900                    let label = match self.summary.warning_count {
901                        1 => "Show 1 warning".into(),
902                        warning_count => format!("Show {} warnings", warning_count),
903                    };
904
905                    div.child(
906                        Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
907                            |buffer_diagnostics_editor, _, window, cx| {
908                                buffer_diagnostics_editor.toggle_warnings(
909                                    &Default::default(),
910                                    window,
911                                    cx,
912                                );
913                                cx.notify();
914                            },
915                        )),
916                    )
917                })
918        } else {
919            div().size_full().child(self.editor.clone())
920        };
921
922        div()
923            .key_context("Diagnostics")
924            .track_focus(&self.focus_handle(cx))
925            .size_full()
926            .child(child)
927    }
928}
929
930impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
931    fn include_warnings(&self, cx: &App) -> bool {
932        self.read_with(cx, |buffer_diagnostics_editor, _cx| {
933            buffer_diagnostics_editor.include_warnings
934        })
935        .unwrap_or(false)
936    }
937
938    fn has_stale_excerpts(&self, _cx: &App) -> bool {
939        false
940    }
941
942    fn is_updating(&self, cx: &App) -> bool {
943        self.read_with(cx, |buffer_diagnostics_editor, cx| {
944            buffer_diagnostics_editor.update_excerpts_task.is_some()
945                || buffer_diagnostics_editor
946                    .project
947                    .read(cx)
948                    .language_servers_running_disk_based_diagnostics(cx)
949                    .next()
950                    .is_some()
951        })
952        .unwrap_or(false)
953    }
954
955    fn stop_updating(&self, cx: &mut App) {
956        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
957            buffer_diagnostics_editor.update_excerpts_task = None;
958            cx.notify();
959        });
960    }
961
962    fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
963        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
964            buffer_diagnostics_editor.update_all_excerpts(window, cx);
965        });
966    }
967
968    fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
969        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
970            buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
971        });
972    }
973
974    fn get_diagnostics_for_buffer(
975        &self,
976        _buffer_id: text::BufferId,
977        cx: &App,
978    ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
979        self.read_with(cx, |buffer_diagnostics_editor, _cx| {
980            buffer_diagnostics_editor.diagnostics.clone()
981        })
982        .unwrap_or_default()
983    }
984}