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                        cx,
420                    )
421                })?;
422
423                for item in more {
424                    let insert_pos = blocks
425                        .binary_search_by(|existing| {
426                            match existing.initial_range.start.cmp(&item.initial_range.start) {
427                                Ordering::Equal => item
428                                    .initial_range
429                                    .end
430                                    .cmp(&existing.initial_range.end)
431                                    .reverse(),
432                                other => other,
433                            }
434                        })
435                        .unwrap_or_else(|pos| pos);
436
437                    blocks.insert(insert_pos, item);
438                }
439            }
440
441            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
442            for b in blocks.iter() {
443                let excerpt_range = context_range_for_entry(
444                    b.initial_range.clone(),
445                    DEFAULT_MULTIBUFFER_CONTEXT,
446                    buffer_snapshot.clone(),
447                    &mut cx,
448                )
449                .await;
450                excerpt_ranges.push(ExcerptRange {
451                    context: excerpt_range,
452                    primary: b.initial_range.clone(),
453                })
454            }
455
456            this.update_in(cx, |this, window, cx| {
457                if let Some(block_ids) = this.blocks.remove(&buffer_id) {
458                    this.editor.update(cx, |editor, cx| {
459                        editor.display_map.update(cx, |display_map, cx| {
460                            display_map.remove_blocks(block_ids.into_iter().collect(), cx)
461                        });
462                    })
463                }
464                let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
465                    multi_buffer.set_excerpt_ranges_for_path(
466                        PathKey::for_buffer(&buffer, cx),
467                        buffer.clone(),
468                        &buffer_snapshot,
469                        excerpt_ranges,
470                        cx,
471                    )
472                });
473                #[cfg(test)]
474                let cloned_blocks = blocks.clone();
475
476                if was_empty {
477                    if let Some(anchor_range) = anchor_ranges.first() {
478                        let range_to_select = anchor_range.start..anchor_range.start;
479                        this.editor.update(cx, |editor, cx| {
480                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
481                                s.select_anchor_ranges([range_to_select]);
482                            })
483                        });
484                        if this.focus_handle.is_focused(window) {
485                            this.editor.read(cx).focus_handle(cx).focus(window);
486                        }
487                    }
488                }
489
490                let editor_blocks =
491                    anchor_ranges
492                        .into_iter()
493                        .zip(blocks.into_iter())
494                        .map(|(anchor, block)| {
495                            let editor = this.editor.downgrade();
496                            BlockProperties {
497                                placement: BlockPlacement::Near(anchor.start),
498                                height: Some(1),
499                                style: BlockStyle::Flex,
500                                render: Arc::new(move |bcx| {
501                                    block.render_block(editor.clone(), bcx)
502                                }),
503                                priority: 1,
504                            }
505                        });
506                let block_ids = this.editor.update(cx, |editor, cx| {
507                    editor.display_map.update(cx, |display_map, cx| {
508                        display_map.insert_blocks(editor_blocks, cx)
509                    })
510                });
511
512                #[cfg(test)]
513                {
514                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
515                        let markdown = block.markdown.clone();
516                        editor::test::set_block_content_for_tests(
517                            &this.editor,
518                            *block_id,
519                            cx,
520                            move |cx| {
521                                markdown::MarkdownElement::rendered_text(
522                                    markdown.clone(),
523                                    cx,
524                                    editor::hover_markdown_style,
525                                )
526                            },
527                        );
528                    }
529                }
530
531                this.blocks.insert(buffer_id, block_ids);
532                cx.notify()
533            })
534        })
535    }
536}
537
538impl Focusable for ProjectDiagnosticsEditor {
539    fn focus_handle(&self, _: &App) -> FocusHandle {
540        self.focus_handle.clone()
541    }
542}
543
544impl Item for ProjectDiagnosticsEditor {
545    type Event = EditorEvent;
546
547    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
548        Editor::to_item_events(event, f)
549    }
550
551    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
552        self.editor
553            .update(cx, |editor, cx| editor.deactivated(window, cx));
554    }
555
556    fn navigate(
557        &mut self,
558        data: Box<dyn Any>,
559        window: &mut Window,
560        cx: &mut Context<Self>,
561    ) -> bool {
562        self.editor
563            .update(cx, |editor, cx| editor.navigate(data, window, cx))
564    }
565
566    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
567        Some("Project Diagnostics".into())
568    }
569
570    fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement {
571        h_flex()
572            .gap_1()
573            .when(
574                self.summary.error_count == 0 && self.summary.warning_count == 0,
575                |then| {
576                    then.child(
577                        h_flex()
578                            .gap_1()
579                            .child(Icon::new(IconName::Check).color(Color::Success))
580                            .child(Label::new("No problems").color(params.text_color())),
581                    )
582                },
583            )
584            .when(self.summary.error_count > 0, |then| {
585                then.child(
586                    h_flex()
587                        .gap_1()
588                        .child(Icon::new(IconName::XCircle).color(Color::Error))
589                        .child(
590                            Label::new(self.summary.error_count.to_string())
591                                .color(params.text_color()),
592                        ),
593                )
594            })
595            .when(self.summary.warning_count > 0, |then| {
596                then.child(
597                    h_flex()
598                        .gap_1()
599                        .child(Icon::new(IconName::Warning).color(Color::Warning))
600                        .child(
601                            Label::new(self.summary.warning_count.to_string())
602                                .color(params.text_color()),
603                        ),
604                )
605            })
606            .into_any_element()
607    }
608
609    fn telemetry_event_text(&self) -> Option<&'static str> {
610        Some("Project Diagnostics Opened")
611    }
612
613    fn for_each_project_item(
614        &self,
615        cx: &App,
616        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
617    ) {
618        self.editor.for_each_project_item(cx, f)
619    }
620
621    fn is_singleton(&self, _: &App) -> bool {
622        false
623    }
624
625    fn set_nav_history(
626        &mut self,
627        nav_history: ItemNavHistory,
628        _: &mut Window,
629        cx: &mut Context<Self>,
630    ) {
631        self.editor.update(cx, |editor, _| {
632            editor.set_nav_history(Some(nav_history));
633        });
634    }
635
636    fn clone_on_split(
637        &self,
638        _workspace_id: Option<workspace::WorkspaceId>,
639        window: &mut Window,
640        cx: &mut Context<Self>,
641    ) -> Option<Entity<Self>>
642    where
643        Self: Sized,
644    {
645        Some(cx.new(|cx| {
646            ProjectDiagnosticsEditor::new(
647                self.include_warnings,
648                self.project.clone(),
649                self.workspace.clone(),
650                window,
651                cx,
652            )
653        }))
654    }
655
656    fn is_dirty(&self, cx: &App) -> bool {
657        self.multibuffer.read(cx).is_dirty(cx)
658    }
659
660    fn has_deleted_file(&self, cx: &App) -> bool {
661        self.multibuffer.read(cx).has_deleted_file(cx)
662    }
663
664    fn has_conflict(&self, cx: &App) -> bool {
665        self.multibuffer.read(cx).has_conflict(cx)
666    }
667
668    fn can_save(&self, _: &App) -> bool {
669        true
670    }
671
672    fn save(
673        &mut self,
674        format: bool,
675        project: Entity<Project>,
676        window: &mut Window,
677        cx: &mut Context<Self>,
678    ) -> Task<Result<()>> {
679        self.editor.save(format, project, window, cx)
680    }
681
682    fn save_as(
683        &mut self,
684        _: Entity<Project>,
685        _: ProjectPath,
686        _window: &mut Window,
687        _: &mut Context<Self>,
688    ) -> Task<Result<()>> {
689        unreachable!()
690    }
691
692    fn reload(
693        &mut self,
694        project: Entity<Project>,
695        window: &mut Window,
696        cx: &mut Context<Self>,
697    ) -> Task<Result<()>> {
698        self.editor.reload(project, window, cx)
699    }
700
701    fn act_as_type<'a>(
702        &'a self,
703        type_id: TypeId,
704        self_handle: &'a Entity<Self>,
705        _: &'a App,
706    ) -> Option<AnyView> {
707        if type_id == TypeId::of::<Self>() {
708            Some(self_handle.to_any())
709        } else if type_id == TypeId::of::<Editor>() {
710            Some(self.editor.to_any())
711        } else {
712            None
713        }
714    }
715
716    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
717        Some(Box::new(self.editor.clone()))
718    }
719
720    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
721        ToolbarItemLocation::PrimaryLeft
722    }
723
724    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
725        self.editor.breadcrumbs(theme, cx)
726    }
727
728    fn added_to_workspace(
729        &mut self,
730        workspace: &mut Workspace,
731        window: &mut Window,
732        cx: &mut Context<Self>,
733    ) {
734        self.editor.update(cx, |editor, cx| {
735            editor.added_to_workspace(workspace, window, cx)
736        });
737    }
738}
739
740const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
741
742async fn context_range_for_entry(
743    range: Range<Point>,
744    context: u32,
745    snapshot: BufferSnapshot,
746    cx: &mut AsyncApp,
747) -> Range<Point> {
748    if let Some(rows) = heuristic_syntactic_expand(
749        range.clone(),
750        DIAGNOSTIC_EXPANSION_ROW_LIMIT,
751        snapshot.clone(),
752        cx,
753    )
754    .await
755    {
756        return Range {
757            start: Point::new(*rows.start(), 0),
758            end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
759        };
760    }
761    Range {
762        start: Point::new(range.start.row.saturating_sub(context), 0),
763        end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
764    }
765}
766
767/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
768/// to the specified `max_row_count`.
769///
770/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
771/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
772async fn heuristic_syntactic_expand(
773    input_range: Range<Point>,
774    max_row_count: u32,
775    snapshot: BufferSnapshot,
776    cx: &mut AsyncApp,
777) -> Option<RangeInclusive<BufferRow>> {
778    let input_row_count = input_range.end.row - input_range.start.row;
779    if input_row_count > max_row_count {
780        return None;
781    }
782
783    // If the outline node contains the diagnostic and is small enough, just use that.
784    let outline_range = snapshot.outline_range_containing(input_range.clone());
785    if let Some(outline_range) = outline_range.clone() {
786        // Remove blank lines from start and end
787        if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
788            .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
789        {
790            if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
791                .rev()
792                .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
793            {
794                let row_count = end_row.saturating_sub(start_row);
795                if row_count <= max_row_count {
796                    return Some(RangeInclusive::new(
797                        outline_range.start.row,
798                        outline_range.end.row,
799                    ));
800                }
801            }
802        }
803    }
804
805    let mut node = snapshot.syntax_ancestor(input_range.clone())?;
806
807    loop {
808        let node_start = Point::from_ts_point(node.start_position());
809        let node_end = Point::from_ts_point(node.end_position());
810        let node_range = node_start..node_end;
811        let row_count = node_end.row - node_start.row + 1;
812        let mut ancestor_range = None;
813        let reached_outline_node = cx.background_executor().scoped({
814                 let node_range = node_range.clone();
815                 let outline_range = outline_range.clone();
816                 let ancestor_range =  &mut ancestor_range;
817                |scope| {scope.spawn(async move {
818                    // Stop if we've exceeded the row count or reached an outline node. Then, find the interval
819                    // of node children which contains the query range. For example, this allows just returning
820                    // the header of a declaration rather than the entire declaration.
821                    if row_count > max_row_count || outline_range == Some(node_range.clone()) {
822                        let mut cursor = node.walk();
823                        let mut included_child_start = None;
824                        let mut included_child_end = None;
825                        let mut previous_end = node_start;
826                        if cursor.goto_first_child() {
827                            loop {
828                                let child_node = cursor.node();
829                                let child_range = previous_end..Point::from_ts_point(child_node.end_position());
830                                if included_child_start.is_none() && child_range.contains(&input_range.start) {
831                                    included_child_start = Some(child_range.start);
832                                }
833                                if child_range.contains(&input_range.end) {
834                                    included_child_end = Some(child_range.end);
835                                }
836                                previous_end = child_range.end;
837                                if !cursor.goto_next_sibling() {
838                                    break;
839                                }
840                            }
841                        }
842                        let end = included_child_end.unwrap_or(node_range.end);
843                        if let Some(start) = included_child_start {
844                            let row_count = end.row - start.row;
845                            if row_count < max_row_count {
846                                *ancestor_range = Some(Some(RangeInclusive::new(start.row, end.row)));
847                                return;
848                            }
849                        }
850
851                        log::info!(
852                            "Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
853                            node.grammar_name()
854                        );
855                        *ancestor_range = Some(None);
856                    }
857                })
858            }});
859        reached_outline_node.await;
860        if let Some(node) = ancestor_range {
861            return node;
862        }
863
864        let node_name = node.grammar_name();
865        let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
866        if node_name.ends_with("block") {
867            return Some(node_row_range);
868        } else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
869            // Expand to the nearest dedent or blank line for statements and declarations.
870            let tab_size = cx
871                .update(|cx| snapshot.settings_at(node_range.start, cx).tab_size.get())
872                .ok()?;
873            let indent_level = snapshot
874                .line_indent_for_row(node_range.start.row)
875                .len(tab_size);
876            let rows_remaining = max_row_count.saturating_sub(row_count);
877            let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
878                ..node_range.start.row)
879                .rev()
880                .find(|row| {
881                    is_line_blank_or_indented_less(indent_level, *row, tab_size, &snapshot.clone())
882                })
883            else {
884                return Some(node_row_range);
885            };
886            let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
887            let Some(end_row) = (node_range.end.row + 1
888                ..cmp::min(
889                    node_range.end.row + rows_remaining + 1,
890                    snapshot.row_count(),
891                ))
892                .find(|row| {
893                    is_line_blank_or_indented_less(indent_level, *row, tab_size, &snapshot.clone())
894                })
895            else {
896                return Some(node_row_range);
897            };
898            return Some(RangeInclusive::new(start_row, end_row));
899        }
900
901        // TODO: doing this instead of walking a cursor as that doesn't work - why?
902        let Some(parent) = node.parent() else {
903            log::info!(
904                "Expanding to ancestor reached the top node, so using default context line count.",
905            );
906            return None;
907        };
908        node = parent;
909    }
910}
911
912fn is_line_blank_or_indented_less(
913    indent_level: u32,
914    row: u32,
915    tab_size: u32,
916    snapshot: &BufferSnapshot,
917) -> bool {
918    let line_indent = snapshot.line_indent_for_row(row);
919    line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
920}