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}