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