Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:31:46.707Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

buffer_diagnostics.rs

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