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