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