diagnostics.rs

  1pub mod items;
  2mod toolbar_controls;
  3
  4mod diagnostic_renderer;
  5
  6#[cfg(test)]
  7mod diagnostics_tests;
  8
  9use anyhow::Result;
 10use collections::{BTreeSet, HashMap};
 11use diagnostic_renderer::DiagnosticBlock;
 12use editor::{
 13    DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
 14    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
 15    scroll::Autoscroll,
 16};
 17use gpui::{
 18    AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
 19    Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
 20    Subscription, Task, WeakEntity, Window, actions, div,
 21};
 22use language::{
 23    Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
 24};
 25use lsp::DiagnosticSeverity;
 26
 27use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
 28use settings::Settings;
 29use std::{
 30    any::{Any, TypeId},
 31    cmp,
 32    cmp::Ordering,
 33    ops::{Range, RangeInclusive},
 34    sync::Arc,
 35    time::Duration,
 36};
 37use text::{BufferId, OffsetRangeExt};
 38use theme::ActiveTheme;
 39pub use toolbar_controls::ToolbarControls;
 40use ui::{Icon, IconName, Label, h_flex, prelude::*};
 41use util::ResultExt;
 42use workspace::{
 43    ItemNavHistory, ToolbarItemLocation, Workspace,
 44    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
 45    searchable::SearchableItemHandle,
 46};
 47
 48actions!(diagnostics, [Deploy, ToggleWarnings]);
 49
 50#[derive(Default)]
 51pub(crate) struct IncludeWarnings(bool);
 52impl Global for IncludeWarnings {}
 53
 54pub fn init(cx: &mut App) {
 55    editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
 56    cx.observe_new(ProjectDiagnosticsEditor::register).detach();
 57}
 58
 59pub(crate) struct ProjectDiagnosticsEditor {
 60    project: Entity<Project>,
 61    workspace: WeakEntity<Workspace>,
 62    focus_handle: FocusHandle,
 63    editor: Entity<Editor>,
 64    diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,
 65    blocks: HashMap<BufferId, Vec<CustomBlockId>>,
 66    summary: DiagnosticSummary,
 67    multibuffer: Entity<MultiBuffer>,
 68    paths_to_update: BTreeSet<ProjectPath>,
 69    include_warnings: bool,
 70    update_excerpts_task: Option<Task<Result<()>>>,
 71    _subscription: Subscription,
 72}
 73
 74impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
 75
 76const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
 77
 78impl Render for ProjectDiagnosticsEditor {
 79    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 80        let warning_count = if self.include_warnings {
 81            self.summary.warning_count
 82        } else {
 83            0
 84        };
 85
 86        let child = if warning_count + self.summary.error_count == 0 {
 87            let label = if self.summary.warning_count == 0 {
 88                SharedString::new_static("No problems in workspace")
 89            } else {
 90                SharedString::new_static("No errors in workspace")
 91            };
 92            v_flex()
 93                .key_context("EmptyPane")
 94                .size_full()
 95                .gap_1()
 96                .justify_center()
 97                .items_center()
 98                .text_center()
 99                .bg(cx.theme().colors().editor_background)
100                .child(Label::new(label).color(Color::Muted))
101                .when(self.summary.warning_count > 0, |this| {
102                    let plural_suffix = if self.summary.warning_count > 1 {
103                        "s"
104                    } else {
105                        ""
106                    };
107                    let label = format!(
108                        "Show {} warning{}",
109                        self.summary.warning_count, plural_suffix
110                    );
111                    this.child(
112                        Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
113                            |this, _, window, cx| {
114                                this.toggle_warnings(&Default::default(), window, cx);
115                                cx.notify();
116                            },
117                        )),
118                    )
119                })
120        } else {
121            div().size_full().child(self.editor.clone())
122        };
123
124        div()
125            .key_context("Diagnostics")
126            .track_focus(&self.focus_handle(cx))
127            .size_full()
128            .on_action(cx.listener(Self::toggle_warnings))
129            .child(child)
130    }
131}
132
133impl ProjectDiagnosticsEditor {
134    fn register(
135        workspace: &mut Workspace,
136        _window: Option<&mut Window>,
137        _: &mut Context<Workspace>,
138    ) {
139        workspace.register_action(Self::deploy);
140    }
141
142    fn new(
143        include_warnings: bool,
144        project_handle: Entity<Project>,
145        workspace: WeakEntity<Workspace>,
146        window: &mut Window,
147        cx: &mut Context<Self>,
148    ) -> Self {
149        let project_event_subscription =
150            cx.subscribe_in(&project_handle, window, |this, project, event, window, cx| match event {
151                project::Event::DiskBasedDiagnosticsStarted { .. } => {
152                    cx.notify();
153                }
154                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
155                    log::debug!("disk based diagnostics finished for server {language_server_id}");
156                    this.update_stale_excerpts(window, cx);
157                }
158                project::Event::DiagnosticsUpdated {
159                    language_server_id,
160                    path,
161                } => {
162                    this.paths_to_update.insert(path.clone());
163                    this.summary = project.read(cx).diagnostic_summary(false, cx);
164                    cx.emit(EditorEvent::TitleChanged);
165
166                    if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
167                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
168                    } else {
169                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
170                        this.update_stale_excerpts(window, cx);
171                    }
172                }
173                _ => {}
174            });
175
176        let focus_handle = cx.focus_handle();
177        cx.on_focus_in(&focus_handle, window, |this, window, cx| {
178            this.focus_in(window, cx)
179        })
180        .detach();
181        cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
182            this.focus_out(window, cx)
183        })
184        .detach();
185
186        let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
187        let editor = cx.new(|cx| {
188            let mut editor =
189                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
190            editor.set_vertical_scroll_margin(5, cx);
191            editor.disable_inline_diagnostics();
192            editor.set_all_diagnostics_active(cx);
193            editor
194        });
195        cx.subscribe_in(
196            &editor,
197            window,
198            |this, _editor, event: &EditorEvent, window, cx| {
199                cx.emit(event.clone());
200                match event {
201                    EditorEvent::Focused => {
202                        if this.multibuffer.read(cx).is_empty() {
203                            window.focus(&this.focus_handle);
204                        }
205                    }
206                    EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
207                    _ => {}
208                }
209            },
210        )
211        .detach();
212        cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
213            this.include_warnings = cx.global::<IncludeWarnings>().0;
214            this.diagnostics.clear();
215            this.update_all_excerpts(window, cx);
216        })
217        .detach();
218
219        let project = project_handle.read(cx);
220        let mut this = Self {
221            project: project_handle.clone(),
222            summary: project.diagnostic_summary(false, cx),
223            diagnostics: Default::default(),
224            blocks: Default::default(),
225            include_warnings,
226            workspace,
227            multibuffer: excerpts,
228            focus_handle,
229            editor,
230            paths_to_update: Default::default(),
231            update_excerpts_task: None,
232            _subscription: project_event_subscription,
233        };
234        this.update_all_excerpts(window, cx);
235        this
236    }
237
238    fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
239        if self.update_excerpts_task.is_some() {
240            return;
241        }
242        let project_handle = self.project.clone();
243        self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
244            cx.background_executor()
245                .timer(DIAGNOSTICS_UPDATE_DELAY)
246                .await;
247            loop {
248                let Some(path) = this.update(cx, |this, _| {
249                    let Some(path) = this.paths_to_update.pop_first() else {
250                        this.update_excerpts_task.take();
251                        return None;
252                    };
253                    Some(path)
254                })?
255                else {
256                    break;
257                };
258
259                if let Some(buffer) = project_handle
260                    .update(cx, |project, cx| project.open_buffer(path.clone(), cx))?
261                    .await
262                    .log_err()
263                {
264                    this.update_in(cx, |this, window, cx| {
265                        this.update_excerpts(buffer, window, cx)
266                    })?
267                    .await?;
268                }
269            }
270            Ok(())
271        }));
272    }
273
274    fn deploy(
275        workspace: &mut Workspace,
276        _: &Deploy,
277        window: &mut Window,
278        cx: &mut Context<Workspace>,
279    ) {
280        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
281            let is_active = workspace
282                .active_item(cx)
283                .is_some_and(|item| item.item_id() == existing.item_id());
284            workspace.activate_item(&existing, true, !is_active, window, cx);
285        } else {
286            let workspace_handle = cx.entity().downgrade();
287
288            let include_warnings = match cx.try_global::<IncludeWarnings>() {
289                Some(include_warnings) => include_warnings.0,
290                None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
291            };
292
293            let diagnostics = cx.new(|cx| {
294                ProjectDiagnosticsEditor::new(
295                    include_warnings,
296                    workspace.project().clone(),
297                    workspace_handle,
298                    window,
299                    cx,
300                )
301            });
302            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, window, cx);
303        }
304    }
305
306    fn toggle_warnings(&mut self, _: &ToggleWarnings, _: &mut Window, cx: &mut Context<Self>) {
307        cx.set_global(IncludeWarnings(!self.include_warnings));
308    }
309
310    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
311        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
312            self.editor.focus_handle(cx).focus(window)
313        }
314    }
315
316    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
317        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
318        {
319            self.update_stale_excerpts(window, cx);
320        }
321    }
322
323    /// Enqueue an update of all excerpts. Updates all paths that either
324    /// currently have diagnostics or are currently present in this view.
325    fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
326        self.project.update(cx, |project, cx| {
327            let mut paths = project
328                .diagnostic_summaries(false, cx)
329                .map(|(path, _, _)| path)
330                .collect::<BTreeSet<_>>();
331            self.multibuffer.update(cx, |multibuffer, cx| {
332                for buffer in multibuffer.all_buffers() {
333                    if let Some(file) = buffer.read(cx).file() {
334                        paths.insert(ProjectPath {
335                            path: file.path().clone(),
336                            worktree_id: file.worktree_id(cx),
337                        });
338                    }
339                }
340            });
341            self.paths_to_update = paths;
342        });
343        self.update_stale_excerpts(window, cx);
344    }
345
346    fn diagnostics_are_unchanged(
347        &self,
348        existing: &Vec<DiagnosticEntry<text::Anchor>>,
349        new: &Vec<DiagnosticEntry<text::Anchor>>,
350        snapshot: &BufferSnapshot,
351    ) -> bool {
352        if existing.len() != new.len() {
353            return false;
354        }
355        existing.iter().zip(new.iter()).all(|(existing, new)| {
356            existing.diagnostic.message == new.diagnostic.message
357                && existing.diagnostic.severity == new.diagnostic.severity
358                && existing.diagnostic.is_primary == new.diagnostic.is_primary
359                && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
360        })
361    }
362
363    fn update_excerpts(
364        &mut self,
365        buffer: Entity<Buffer>,
366        window: &mut Window,
367        cx: &mut Context<Self>,
368    ) -> Task<Result<()>> {
369        let was_empty = self.multibuffer.read(cx).is_empty();
370        let buffer_snapshot = buffer.read(cx).snapshot();
371        let buffer_id = buffer_snapshot.remote_id();
372        let max_severity = if self.include_warnings {
373            DiagnosticSeverity::WARNING
374        } else {
375            DiagnosticSeverity::ERROR
376        };
377
378        cx.spawn_in(window, async move |this, mut cx| {
379            let diagnostics = buffer_snapshot
380                .diagnostics_in_range::<_, text::Anchor>(
381                    Point::zero()..buffer_snapshot.max_point(),
382                    false,
383                )
384                .collect::<Vec<_>>();
385            let unchanged = this.update(cx, |this, _| {
386                if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
387                    this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
388                }) {
389                    return true;
390                }
391                this.diagnostics.insert(buffer_id, diagnostics.clone());
392                return false;
393            })?;
394            if unchanged {
395                return Ok(());
396            }
397
398            let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
399            for entry in diagnostics {
400                grouped
401                    .entry(entry.diagnostic.group_id)
402                    .or_default()
403                    .push(DiagnosticEntry {
404                        range: entry.range.to_point(&buffer_snapshot),
405                        diagnostic: entry.diagnostic,
406                    })
407            }
408            let mut blocks: Vec<DiagnosticBlock> = Vec::new();
409
410            for (_, group) in grouped {
411                let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
412                if group_severity.is_none_or(|s| s > max_severity) {
413                    continue;
414                }
415                let more = cx.update(|_, cx| {
416                    crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
417                        group,
418                        buffer_snapshot.remote_id(),
419                        Some(this.clone()),
420                        cx,
421                    )
422                })?;
423
424                for item in more {
425                    let insert_pos = blocks
426                        .binary_search_by(|existing| {
427                            match existing.initial_range.start.cmp(&item.initial_range.start) {
428                                Ordering::Equal => item
429                                    .initial_range
430                                    .end
431                                    .cmp(&existing.initial_range.end)
432                                    .reverse(),
433                                other => other,
434                            }
435                        })
436                        .unwrap_or_else(|pos| pos);
437
438                    blocks.insert(insert_pos, item);
439                }
440            }
441
442            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
443            for b in blocks.iter() {
444                let excerpt_range = context_range_for_entry(
445                    b.initial_range.clone(),
446                    DEFAULT_MULTIBUFFER_CONTEXT,
447                    buffer_snapshot.clone(),
448                    &mut cx,
449                )
450                .await;
451                excerpt_ranges.push(ExcerptRange {
452                    context: excerpt_range,
453                    primary: b.initial_range.clone(),
454                })
455            }
456
457            this.update_in(cx, |this, window, cx| {
458                if let Some(block_ids) = this.blocks.remove(&buffer_id) {
459                    this.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, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
466                    multi_buffer.set_excerpt_ranges_for_path(
467                        PathKey::for_buffer(&buffer, cx),
468                        buffer.clone(),
469                        &buffer_snapshot,
470                        excerpt_ranges,
471                        cx,
472                    )
473                });
474                #[cfg(test)]
475                let cloned_blocks = blocks.clone();
476
477                if was_empty {
478                    if let Some(anchor_range) = anchor_ranges.first() {
479                        let range_to_select = anchor_range.start..anchor_range.start;
480                        this.editor.update(cx, |editor, cx| {
481                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
482                                s.select_anchor_ranges([range_to_select]);
483                            })
484                        });
485                        if this.focus_handle.is_focused(window) {
486                            this.editor.read(cx).focus_handle(cx).focus(window);
487                        }
488                    }
489                }
490
491                let editor_blocks =
492                    anchor_ranges
493                        .into_iter()
494                        .zip(blocks.into_iter())
495                        .map(|(anchor, block)| {
496                            let editor = this.editor.downgrade();
497                            BlockProperties {
498                                placement: BlockPlacement::Near(anchor.start),
499                                height: Some(1),
500                                style: BlockStyle::Flex,
501                                render: Arc::new(move |bcx| {
502                                    block.render_block(editor.clone(), bcx)
503                                }),
504                                priority: 1,
505                            }
506                        });
507                let block_ids = this.editor.update(cx, |editor, cx| {
508                    editor.display_map.update(cx, |display_map, cx| {
509                        display_map.insert_blocks(editor_blocks, cx)
510                    })
511                });
512
513                #[cfg(test)]
514                {
515                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
516                        let markdown = block.markdown.clone();
517                        editor::test::set_block_content_for_tests(
518                            &this.editor,
519                            *block_id,
520                            cx,
521                            move |cx| {
522                                markdown::MarkdownElement::rendered_text(
523                                    markdown.clone(),
524                                    cx,
525                                    editor::hover_popover::diagnostics_markdown_style,
526                                )
527                            },
528                        );
529                    }
530                }
531
532                this.blocks.insert(buffer_id, block_ids);
533                cx.notify()
534            })
535        })
536    }
537}
538
539impl Focusable for ProjectDiagnosticsEditor {
540    fn focus_handle(&self, _: &App) -> FocusHandle {
541        self.focus_handle.clone()
542    }
543}
544
545impl Item for ProjectDiagnosticsEditor {
546    type Event = EditorEvent;
547
548    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
549        Editor::to_item_events(event, f)
550    }
551
552    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
553        self.editor
554            .update(cx, |editor, cx| editor.deactivated(window, cx));
555    }
556
557    fn navigate(
558        &mut self,
559        data: Box<dyn Any>,
560        window: &mut Window,
561        cx: &mut Context<Self>,
562    ) -> bool {
563        self.editor
564            .update(cx, |editor, cx| editor.navigate(data, window, cx))
565    }
566
567    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
568        Some("Project Diagnostics".into())
569    }
570
571    fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
572        "Diagnostics".into()
573    }
574
575    fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement {
576        h_flex()
577            .gap_1()
578            .when(
579                self.summary.error_count == 0 && self.summary.warning_count == 0,
580                |then| {
581                    then.child(
582                        h_flex()
583                            .gap_1()
584                            .child(Icon::new(IconName::Check).color(Color::Success))
585                            .child(Label::new("No problems").color(params.text_color())),
586                    )
587                },
588            )
589            .when(self.summary.error_count > 0, |then| {
590                then.child(
591                    h_flex()
592                        .gap_1()
593                        .child(Icon::new(IconName::XCircle).color(Color::Error))
594                        .child(
595                            Label::new(self.summary.error_count.to_string())
596                                .color(params.text_color()),
597                        ),
598                )
599            })
600            .when(self.summary.warning_count > 0, |then| {
601                then.child(
602                    h_flex()
603                        .gap_1()
604                        .child(Icon::new(IconName::Warning).color(Color::Warning))
605                        .child(
606                            Label::new(self.summary.warning_count.to_string())
607                                .color(params.text_color()),
608                        ),
609                )
610            })
611            .into_any_element()
612    }
613
614    fn telemetry_event_text(&self) -> Option<&'static str> {
615        Some("Project Diagnostics Opened")
616    }
617
618    fn for_each_project_item(
619        &self,
620        cx: &App,
621        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
622    ) {
623        self.editor.for_each_project_item(cx, f)
624    }
625
626    fn is_singleton(&self, _: &App) -> bool {
627        false
628    }
629
630    fn set_nav_history(
631        &mut self,
632        nav_history: ItemNavHistory,
633        _: &mut Window,
634        cx: &mut Context<Self>,
635    ) {
636        self.editor.update(cx, |editor, _| {
637            editor.set_nav_history(Some(nav_history));
638        });
639    }
640
641    fn clone_on_split(
642        &self,
643        _workspace_id: Option<workspace::WorkspaceId>,
644        window: &mut Window,
645        cx: &mut Context<Self>,
646    ) -> Option<Entity<Self>>
647    where
648        Self: Sized,
649    {
650        Some(cx.new(|cx| {
651            ProjectDiagnosticsEditor::new(
652                self.include_warnings,
653                self.project.clone(),
654                self.workspace.clone(),
655                window,
656                cx,
657            )
658        }))
659    }
660
661    fn is_dirty(&self, cx: &App) -> bool {
662        self.multibuffer.read(cx).is_dirty(cx)
663    }
664
665    fn has_deleted_file(&self, cx: &App) -> bool {
666        self.multibuffer.read(cx).has_deleted_file(cx)
667    }
668
669    fn has_conflict(&self, cx: &App) -> bool {
670        self.multibuffer.read(cx).has_conflict(cx)
671    }
672
673    fn can_save(&self, _: &App) -> bool {
674        true
675    }
676
677    fn save(
678        &mut self,
679        format: bool,
680        project: Entity<Project>,
681        window: &mut Window,
682        cx: &mut Context<Self>,
683    ) -> Task<Result<()>> {
684        self.editor.save(format, project, window, cx)
685    }
686
687    fn save_as(
688        &mut self,
689        _: Entity<Project>,
690        _: ProjectPath,
691        _window: &mut Window,
692        _: &mut Context<Self>,
693    ) -> Task<Result<()>> {
694        unreachable!()
695    }
696
697    fn reload(
698        &mut self,
699        project: Entity<Project>,
700        window: &mut Window,
701        cx: &mut Context<Self>,
702    ) -> Task<Result<()>> {
703        self.editor.reload(project, window, cx)
704    }
705
706    fn act_as_type<'a>(
707        &'a self,
708        type_id: TypeId,
709        self_handle: &'a Entity<Self>,
710        _: &'a App,
711    ) -> Option<AnyView> {
712        if type_id == TypeId::of::<Self>() {
713            Some(self_handle.to_any())
714        } else if type_id == TypeId::of::<Editor>() {
715            Some(self.editor.to_any())
716        } else {
717            None
718        }
719    }
720
721    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
722        Some(Box::new(self.editor.clone()))
723    }
724
725    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
726        ToolbarItemLocation::PrimaryLeft
727    }
728
729    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
730        self.editor.breadcrumbs(theme, cx)
731    }
732
733    fn added_to_workspace(
734        &mut self,
735        workspace: &mut Workspace,
736        window: &mut Window,
737        cx: &mut Context<Self>,
738    ) {
739        self.editor.update(cx, |editor, cx| {
740            editor.added_to_workspace(workspace, window, cx)
741        });
742    }
743}
744
745const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
746
747async fn context_range_for_entry(
748    range: Range<Point>,
749    context: u32,
750    snapshot: BufferSnapshot,
751    cx: &mut AsyncApp,
752) -> Range<Point> {
753    if let Some(rows) = heuristic_syntactic_expand(
754        range.clone(),
755        DIAGNOSTIC_EXPANSION_ROW_LIMIT,
756        snapshot.clone(),
757        cx,
758    )
759    .await
760    {
761        return Range {
762            start: Point::new(*rows.start(), 0),
763            end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
764        };
765    }
766    Range {
767        start: Point::new(range.start.row.saturating_sub(context), 0),
768        end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
769    }
770}
771
772/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
773/// to the specified `max_row_count`.
774///
775/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
776/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
777async fn heuristic_syntactic_expand(
778    input_range: Range<Point>,
779    max_row_count: u32,
780    snapshot: BufferSnapshot,
781    cx: &mut AsyncApp,
782) -> Option<RangeInclusive<BufferRow>> {
783    let input_row_count = input_range.end.row - input_range.start.row;
784    if input_row_count > max_row_count {
785        return None;
786    }
787
788    // If the outline node contains the diagnostic and is small enough, just use that.
789    let outline_range = snapshot.outline_range_containing(input_range.clone());
790    if let Some(outline_range) = outline_range.clone() {
791        // Remove blank lines from start and end
792        if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
793            .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
794        {
795            if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
796                .rev()
797                .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
798            {
799                let row_count = end_row.saturating_sub(start_row);
800                if row_count <= max_row_count {
801                    return Some(RangeInclusive::new(
802                        outline_range.start.row,
803                        outline_range.end.row,
804                    ));
805                }
806            }
807        }
808    }
809
810    let mut node = snapshot.syntax_ancestor(input_range.clone())?;
811
812    loop {
813        let node_start = Point::from_ts_point(node.start_position());
814        let node_end = Point::from_ts_point(node.end_position());
815        let node_range = node_start..node_end;
816        let row_count = node_end.row - node_start.row + 1;
817        let mut ancestor_range = None;
818        let reached_outline_node = cx.background_executor().scoped({
819                 let node_range = node_range.clone();
820                 let outline_range = outline_range.clone();
821                 let ancestor_range =  &mut ancestor_range;
822                |scope| {scope.spawn(async move {
823                    // Stop if we've exceeded the row count or reached an outline node. Then, find the interval
824                    // of node children which contains the query range. For example, this allows just returning
825                    // the header of a declaration rather than the entire declaration.
826                    if row_count > max_row_count || outline_range == Some(node_range.clone()) {
827                        let mut cursor = node.walk();
828                        let mut included_child_start = None;
829                        let mut included_child_end = None;
830                        let mut previous_end = node_start;
831                        if cursor.goto_first_child() {
832                            loop {
833                                let child_node = cursor.node();
834                                let child_range = previous_end..Point::from_ts_point(child_node.end_position());
835                                if included_child_start.is_none() && child_range.contains(&input_range.start) {
836                                    included_child_start = Some(child_range.start);
837                                }
838                                if child_range.contains(&input_range.end) {
839                                    included_child_end = Some(child_range.end);
840                                }
841                                previous_end = child_range.end;
842                                if !cursor.goto_next_sibling() {
843                                    break;
844                                }
845                            }
846                        }
847                        let end = included_child_end.unwrap_or(node_range.end);
848                        if let Some(start) = included_child_start {
849                            let row_count = end.row - start.row;
850                            if row_count < max_row_count {
851                                *ancestor_range = Some(Some(RangeInclusive::new(start.row, end.row)));
852                                return;
853                            }
854                        }
855
856                        log::info!(
857                            "Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
858                            node.grammar_name()
859                        );
860                        *ancestor_range = Some(None);
861                    }
862                })
863            }});
864        reached_outline_node.await;
865        if let Some(node) = ancestor_range {
866            return node;
867        }
868
869        let node_name = node.grammar_name();
870        let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
871        if node_name.ends_with("block") {
872            return Some(node_row_range);
873        } else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
874            // Expand to the nearest dedent or blank line for statements and declarations.
875            let tab_size = cx
876                .update(|cx| snapshot.settings_at(node_range.start, cx).tab_size.get())
877                .ok()?;
878            let indent_level = snapshot
879                .line_indent_for_row(node_range.start.row)
880                .len(tab_size);
881            let rows_remaining = max_row_count.saturating_sub(row_count);
882            let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
883                ..node_range.start.row)
884                .rev()
885                .find(|row| {
886                    is_line_blank_or_indented_less(indent_level, *row, tab_size, &snapshot.clone())
887                })
888            else {
889                return Some(node_row_range);
890            };
891            let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
892            let Some(end_row) = (node_range.end.row + 1
893                ..cmp::min(
894                    node_range.end.row + rows_remaining + 1,
895                    snapshot.row_count(),
896                ))
897                .find(|row| {
898                    is_line_blank_or_indented_less(indent_level, *row, tab_size, &snapshot.clone())
899                })
900            else {
901                return Some(node_row_range);
902            };
903            return Some(RangeInclusive::new(start_row, end_row));
904        }
905
906        // TODO: doing this instead of walking a cursor as that doesn't work - why?
907        let Some(parent) = node.parent() else {
908            log::info!(
909                "Expanding to ancestor reached the top node, so using default context line count.",
910            );
911            return None;
912        };
913        node = parent;
914    }
915}
916
917fn is_line_blank_or_indented_less(
918    indent_level: u32,
919    row: u32,
920    tab_size: u32,
921    snapshot: &BufferSnapshot,
922) -> bool {
923    let line_indent = snapshot.line_indent_for_row(row);
924    line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
925}