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