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