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