Skip to repository content1019 lines · 38.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:31:34.900Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
buffer_diagnostics.rs
1use crate::{
2 DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,
3 diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
4 toolbar_controls::DiagnosticsToolbarEditor,
5};
6use anyhow::Result;
7use collections::HashMap;
8use editor::{
9 Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
10 display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
11 multibuffer_context_lines,
12};
13use gpui::{
14 AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
15 InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
16 Task, TaskExt, WeakEntity, Window, actions, div,
17};
18use language::{Buffer, Capability, DiagnosticEntry, DiagnosticEntryRef, Point};
19use project::{
20 DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
21 project_settings::{DiagnosticSeverity, ProjectSettings},
22};
23use settings::Settings;
24use std::{
25 any::{Any, TypeId},
26 cmp::{self, Ordering},
27 ops::Range,
28 sync::Arc,
29};
30use text::{Anchor, BufferSnapshot, OffsetRangeExt};
31use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
32use workspace::{
33 ItemHandle, ItemNavHistory, Workspace,
34 item::{Item, ItemEvent, TabContentParams},
35};
36
37actions!(
38 diagnostics,
39 [
40 /// Opens the project diagnostics view for the currently focused file.
41 DeployCurrentFile,
42 ]
43);
44
45/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
46/// with diagnostics for a single buffer, as only the excerpts of the buffer
47/// where diagnostics are available are displayed.
48pub(crate) struct BufferDiagnosticsEditor {
49 pub project: Entity<Project>,
50 focus_handle: FocusHandle,
51 editor: Entity<Editor>,
52 /// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
53 /// allow quick comparison of updated diagnostics, to confirm if anything
54 /// has changed.
55 pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
56 /// The blocks used to display the diagnostics' content in the editor, next
57 /// to the excerpts where the diagnostic originated.
58 blocks: Vec<CustomBlockId>,
59 /// Multibuffer to contain all excerpts that contain diagnostics, which are
60 /// to be rendered in the editor.
61 multibuffer: Entity<MultiBuffer>,
62 /// The buffer for which the editor is displaying diagnostics and excerpts
63 /// for.
64 buffer: Option<Entity<Buffer>>,
65 /// The path for which the editor is displaying diagnostics for.
66 project_path: ProjectPath,
67 /// Summary of the number of warnings and errors for the path. Used to
68 /// display the number of warnings and errors in the tab's content.
69 summary: DiagnosticSummary,
70 /// Whether to include warnings in the list of diagnostics shown in the
71 /// editor.
72 pub(crate) include_warnings: bool,
73 /// Keeps track of whether there's a background task already running to
74 /// update the excerpts, in order to avoid firing multiple tasks for this purpose.
75 pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
76 /// The project's subscription, responsible for processing events related to
77 /// diagnostics.
78 _subscription: Subscription,
79}
80
81impl BufferDiagnosticsEditor {
82 /// Creates new instance of the `BufferDiagnosticsEditor` which can then be
83 /// displayed by adding it to a pane.
84 pub fn new(
85 project_path: ProjectPath,
86 project_handle: Entity<Project>,
87 buffer: Option<Entity<Buffer>>,
88 include_warnings: bool,
89 window: &mut Window,
90 cx: &mut Context<Self>,
91 ) -> Self {
92 // Subscribe to project events related to diagnostics so the
93 // `BufferDiagnosticsEditor` can update its state accordingly.
94 let project_event_subscription = cx.subscribe_in(
95 &project_handle,
96 window,
97 |buffer_diagnostics_editor, _project, event, window, cx| match event {
98 Event::DiskBasedDiagnosticsStarted { .. } => {
99 cx.notify();
100 }
101 Event::DiskBasedDiagnosticsFinished { .. } => {
102 buffer_diagnostics_editor.update_all_excerpts(window, cx);
103 }
104 Event::DiagnosticsUpdated {
105 paths,
106 language_server_id,
107 } => {
108 // When diagnostics have been updated, the
109 // `BufferDiagnosticsEditor` should update its state only if
110 // one of the paths matches its `project_path`, otherwise
111 // the event should be ignored.
112 if paths.contains(&buffer_diagnostics_editor.project_path) {
113 buffer_diagnostics_editor.update_diagnostic_summary(cx);
114
115 if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
116 log::debug!("diagnostics updated for server {language_server_id}. recording change");
117 } else {
118 log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
119 buffer_diagnostics_editor.update_all_excerpts(window, cx);
120 }
121 }
122 }
123 _ => {}
124 },
125 );
126
127 let focus_handle = cx.focus_handle();
128
129 cx.on_focus_in(
130 &focus_handle,
131 window,
132 |buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
133 )
134 .detach();
135
136 cx.on_focus_out(
137 &focus_handle,
138 window,
139 |buffer_diagnostics_editor, _event, window, cx| {
140 buffer_diagnostics_editor.focus_out(window, cx)
141 },
142 )
143 .detach();
144
145 let summary = project_handle
146 .read(cx)
147 .diagnostic_summary_for_path(&project_path, cx);
148
149 let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
150 let max_severity = Self::max_diagnostics_severity(include_warnings);
151 let editor = cx.new(|cx| {
152 let mut editor = Editor::for_multibuffer(
153 multibuffer.clone(),
154 Some(project_handle.clone()),
155 window,
156 cx,
157 );
158 editor.set_vertical_scroll_margin(5, cx);
159 editor.disable_inline_diagnostics();
160 editor.set_max_diagnostics_severity(max_severity, cx);
161 editor.set_all_diagnostics_active(cx);
162 editor
163 });
164
165 // Subscribe to events triggered by the editor in order to correctly
166 // update the buffer's excerpts.
167 cx.subscribe_in(
168 &editor,
169 window,
170 |buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
171 cx.emit(event.clone());
172
173 match event {
174 // If the user tries to focus on the editor but there's actually
175 // no excerpts for the buffer, focus back on the
176 // `BufferDiagnosticsEditor` instance.
177 EditorEvent::Focused => {
178 if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
179 window.focus(&buffer_diagnostics_editor.focus_handle, cx);
180 }
181 }
182 EditorEvent::Blurred => {
183 buffer_diagnostics_editor.update_all_excerpts(window, cx)
184 }
185 _ => {}
186 }
187 },
188 )
189 .detach();
190
191 let diagnostics = vec![];
192 let update_excerpts_task = None;
193 let mut buffer_diagnostics_editor = Self {
194 project: project_handle,
195 focus_handle,
196 editor,
197 diagnostics,
198 blocks: Default::default(),
199 multibuffer,
200 buffer,
201 project_path,
202 summary,
203 include_warnings,
204 update_excerpts_task,
205 _subscription: project_event_subscription,
206 };
207
208 buffer_diagnostics_editor.update_all_diagnostics(window, cx);
209 buffer_diagnostics_editor
210 }
211
212 fn deploy(
213 workspace: &mut Workspace,
214 _: &DeployCurrentFile,
215 window: &mut Window,
216 cx: &mut Context<Workspace>,
217 ) {
218 // Determine the currently opened path by finding the active editor and
219 // finding the project path for the buffer.
220 // If there's no active editor with a project path, avoiding deploying
221 // the buffer diagnostics view.
222 if let Some(editor) = workspace.active_item_as::<Editor>(cx)
223 && let Some(project_path) = editor.read(cx).active_project_path(cx)
224 {
225 // Check if there's already a `BufferDiagnosticsEditor` tab for this
226 // same path, and if so, focus on that one instead of creating a new
227 // one.
228 let existing_editor = workspace
229 .items_of_type::<BufferDiagnosticsEditor>(cx)
230 .find(|editor| editor.read(cx).project_path == project_path);
231
232 if let Some(editor) = existing_editor {
233 workspace.activate_item(&editor, true, true, window, cx);
234 } else {
235 let include_warnings = match cx.try_global::<IncludeWarnings>() {
236 Some(include_warnings) => include_warnings.0,
237 None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
238 };
239
240 let item = cx.new(|cx| {
241 Self::new(
242 project_path,
243 workspace.project().clone(),
244 editor.read(cx).buffer().read(cx).as_singleton(),
245 include_warnings,
246 window,
247 cx,
248 )
249 });
250
251 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
252 }
253 }
254 }
255
256 pub fn register(
257 workspace: &mut Workspace,
258 _window: Option<&mut Window>,
259 _: &mut Context<Workspace>,
260 ) {
261 workspace.register_action(Self::deploy);
262 }
263
264 fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
265 self.update_all_excerpts(window, cx);
266 }
267
268 fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
269 let project = self.project.read(cx);
270
271 self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
272 }
273
274 /// Enqueue an update to the excerpts and diagnostic blocks being shown in
275 /// the editor.
276 pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
277 // If there's already a task updating the excerpts, early return and let
278 // the other task finish.
279 if self.update_excerpts_task.is_some() {
280 return;
281 }
282
283 let buffer = self.buffer.clone();
284
285 self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
286 cx.background_executor()
287 .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
288 .await;
289
290 if let Some(buffer) = buffer {
291 editor
292 .update_in(cx, |editor, window, cx| {
293 editor.update_excerpts(buffer, window, cx)
294 })?
295 .await?;
296 };
297
298 let _ = editor.update(cx, |editor, cx| {
299 editor.update_excerpts_task = None;
300 cx.notify();
301 });
302
303 Ok(())
304 }));
305 }
306
307 /// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
308 /// buffer.
309 fn update_excerpts(
310 &mut self,
311 buffer: Entity<Buffer>,
312 window: &mut Window,
313 cx: &mut Context<Self>,
314 ) -> Task<Result<()>> {
315 let was_empty = self.multibuffer.read(cx).is_empty();
316 let multibuffer_context = multibuffer_context_lines(cx);
317 let buffer_snapshot = buffer.read(cx).snapshot();
318 let buffer_snapshot_max = buffer_snapshot.max_point();
319 let max_severity = Self::max_diagnostics_severity(self.include_warnings)
320 .into_lsp()
321 .unwrap_or(lsp::DiagnosticSeverity::WARNING);
322
323 cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
324 // Fetch the diagnostics for the whole of the buffer
325 // (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
326 // if the diagnostics changed, if it didn't, early return as there's
327 // nothing to update.
328 let diagnostics = buffer_snapshot
329 .diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
330 .collect::<Vec<_>>();
331
332 let unchanged =
333 buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
334 if buffer_diagnostics_editor
335 .diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
336 {
337 return true;
338 }
339
340 buffer_diagnostics_editor.set_diagnostics(&diagnostics);
341 return false;
342 })?;
343
344 if unchanged {
345 return Ok(());
346 }
347
348 // Mapping between the Group ID and a vector of DiagnosticEntry.
349 let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
350 for entry in diagnostics {
351 grouped
352 .entry(entry.diagnostic.group_id)
353 .or_default()
354 .push(DiagnosticEntryRef {
355 range: entry.range.to_point(&buffer_snapshot),
356 diagnostic: entry.diagnostic,
357 })
358 }
359
360 let mut blocks: Vec<DiagnosticBlock> = Vec::new();
361 for (_, group) in grouped {
362 // If the minimum severity of the group is higher than the
363 // maximum severity, or it doesn't even have severity, skip this
364 // group.
365 if group
366 .iter()
367 .map(|d| d.diagnostic.severity)
368 .min()
369 .is_none_or(|severity| severity > max_severity)
370 {
371 continue;
372 }
373
374 let languages = buffer_diagnostics_editor
375 .read_with(cx, |b, cx| b.project.read(cx).languages().clone())
376 .ok();
377
378 let diagnostic_blocks = cx.update(|_window, cx| {
379 DiagnosticRenderer::diagnostic_blocks_for_group(
380 group,
381 buffer_snapshot.remote_id(),
382 Some(Arc::new(buffer_diagnostics_editor.clone())),
383 languages,
384 cx,
385 )
386 })?;
387
388 // For each of the diagnostic blocks to be displayed in the
389 // editor, figure out its index in the list of blocks.
390 //
391 // The following rules are used to determine the order:
392 // 1. Blocks with a lower start position should come first.
393 // 2. If two blocks have the same start position, the one with
394 // the higher end position should come first.
395 for diagnostic_block in diagnostic_blocks {
396 let index = blocks.partition_point(|probe| {
397 match probe
398 .initial_range
399 .start
400 .cmp(&diagnostic_block.initial_range.start)
401 {
402 Ordering::Less => true,
403 Ordering::Greater => false,
404 Ordering::Equal => {
405 probe.initial_range.end > diagnostic_block.initial_range.end
406 }
407 }
408 });
409
410 blocks.insert(index, diagnostic_block);
411 }
412 }
413
414 // Build the excerpt ranges for this specific buffer's diagnostics,
415 // so those excerpts can later be used to update the excerpts shown
416 // in the editor.
417 // This is done by iterating over the list of diagnostic blocks and
418 // determine what range does the diagnostic block span.
419 let mut excerpt_ranges: Vec<ExcerptRange<_>> = Vec::new();
420
421 for diagnostic_block in blocks.iter() {
422 let excerpt_range = context_range_for_entry(
423 diagnostic_block.initial_range.clone(),
424 multibuffer_context,
425 buffer_snapshot.clone(),
426 &mut cx,
427 )
428 .await;
429 let initial_range = buffer_snapshot
430 .anchor_after(diagnostic_block.initial_range.start)
431 ..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);
432
433 let bin_search = |probe: &ExcerptRange<text::Anchor>| {
434 let context_start = || {
435 probe
436 .context
437 .start
438 .cmp(&excerpt_range.start, &buffer_snapshot)
439 };
440 let context_end =
441 || probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
442 let primary_start = || {
443 probe
444 .primary
445 .start
446 .cmp(&initial_range.start, &buffer_snapshot)
447 };
448 let primary_end =
449 || probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
450 context_start()
451 .then_with(context_end)
452 .then_with(primary_start)
453 .then_with(primary_end)
454 .then(cmp::Ordering::Greater)
455 };
456
457 let index = excerpt_ranges
458 .binary_search_by(bin_search)
459 .unwrap_or_else(|i| i);
460
461 excerpt_ranges.insert(
462 index,
463 ExcerptRange {
464 context: excerpt_range,
465 primary: initial_range,
466 },
467 )
468 }
469
470 // Finally, update the editor's content with the new excerpt ranges
471 // for this editor, as well as the diagnostic blocks.
472 buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
473 // Remove the list of `CustomBlockId` from the editor's display
474 // map, ensuring that if any diagnostics have been solved, the
475 // associated block stops being shown.
476 let block_ids = buffer_diagnostics_editor.blocks.clone();
477
478 buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
479 editor.display_map.update(cx, |display_map, cx| {
480 display_map.remove_blocks(block_ids.into_iter().collect(), cx);
481 })
482 });
483
484 let excerpt_ranges: Vec<_> = excerpt_ranges
485 .into_iter()
486 .map(|range| ExcerptRange {
487 context: range.context.to_point(&buffer_snapshot),
488 primary: range.primary.to_point(&buffer_snapshot),
489 })
490 .collect();
491 buffer_diagnostics_editor
492 .multibuffer
493 .update(cx, |multibuffer, cx| {
494 multibuffer.set_excerpt_ranges_for_path(
495 PathKey::for_buffer(&buffer, cx),
496 buffer.clone(),
497 &buffer_snapshot,
498 excerpt_ranges.clone(),
499 cx,
500 )
501 });
502 let multibuffer_snapshot =
503 buffer_diagnostics_editor.multibuffer.read(cx).snapshot(cx);
504 let anchor_ranges: Vec<Range<editor::Anchor>> = excerpt_ranges
505 .into_iter()
506 .filter_map(|range| {
507 let text_range = buffer_snapshot.anchor_range_inside(range.primary);
508 let start = multibuffer_snapshot.anchor_in_buffer(text_range.start)?;
509 let end = multibuffer_snapshot.anchor_in_buffer(text_range.end)?;
510 Some(start..end)
511 })
512 .collect();
513
514 if was_empty {
515 if let Some(anchor_range) = anchor_ranges.first() {
516 let range_to_select = anchor_range.start..anchor_range.start;
517
518 buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
519 editor.change_selections(Default::default(), window, cx, |selection| {
520 selection.select_anchor_ranges([range_to_select])
521 })
522 });
523
524 // If the `BufferDiagnosticsEditor` is currently
525 // focused, move focus to its editor.
526 if buffer_diagnostics_editor.focus_handle.is_focused(window) {
527 buffer_diagnostics_editor
528 .editor
529 .read(cx)
530 .focus_handle(cx)
531 .focus(window, cx);
532 }
533 }
534 }
535
536 // Cloning the blocks before moving ownership so these can later
537 // be used to set the block contents for testing purposes.
538 #[cfg(test)]
539 let cloned_blocks = blocks.clone();
540
541 // Build new diagnostic blocks to be added to the editor's
542 // display map for the new diagnostics. Update the `blocks`
543 // property before finishing, to ensure the blocks are removed
544 // on the next execution.
545 let editor_blocks = anchor_ranges
546 .into_iter()
547 .zip(blocks)
548 .map(|(anchor, block)| {
549 let editor = buffer_diagnostics_editor.editor.downgrade();
550
551 BlockProperties {
552 placement: BlockPlacement::Near(anchor.start),
553 height: Some(1),
554 style: BlockStyle::Flex,
555 render: Arc::new(move |block_context| {
556 block.render_block(editor.clone(), block_context)
557 }),
558 priority: 1,
559 }
560 });
561
562 let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
563 editor.display_map.update(cx, |display_map, cx| {
564 display_map.insert_blocks(editor_blocks, cx)
565 })
566 });
567
568 // In order to be able to verify which diagnostic blocks are
569 // rendered in the editor, the `set_block_content_for_tests`
570 // function must be used, so that the
571 // `editor::test::editor_content_with_blocks` function can then
572 // be called to fetch these blocks.
573 #[cfg(test)]
574 {
575 for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
576 let markdown = block.markdown.clone();
577 editor::test::set_block_content_for_tests(
578 &buffer_diagnostics_editor.editor,
579 *block_id,
580 cx,
581 move |cx| {
582 markdown::MarkdownElement::rendered_text(
583 markdown.clone(),
584 cx,
585 editor::hover_popover::diagnostics_markdown_style,
586 )
587 },
588 );
589 }
590 }
591
592 buffer_diagnostics_editor.blocks = block_ids;
593 cx.notify()
594 })
595 })
596 }
597
598 fn set_diagnostics(&mut self, diagnostics: &[DiagnosticEntryRef<'_, Anchor>]) {
599 self.diagnostics = diagnostics
600 .iter()
601 .map(DiagnosticEntryRef::to_owned)
602 .collect();
603 }
604
605 fn diagnostics_are_unchanged(
606 &self,
607 diagnostics: &Vec<DiagnosticEntryRef<'_, Anchor>>,
608 snapshot: &BufferSnapshot,
609 ) -> bool {
610 if self.diagnostics.len() != diagnostics.len() {
611 return false;
612 }
613
614 self.diagnostics
615 .iter()
616 .zip(diagnostics.iter())
617 .all(|(existing, new)| {
618 existing.diagnostic.message == new.diagnostic.message
619 && existing.diagnostic.severity == new.diagnostic.severity
620 && existing.diagnostic.is_primary == new.diagnostic.is_primary
621 && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
622 })
623 }
624
625 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
626 // If the `BufferDiagnosticsEditor` is focused and the multibuffer is
627 // not empty, focus on the editor instead, which will allow the user to
628 // start interacting and editing the buffer's contents.
629 if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
630 self.editor.focus_handle(cx).focus(window, cx)
631 }
632 }
633
634 fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
635 if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
636 {
637 self.update_all_excerpts(window, cx);
638 }
639 }
640
641 pub fn toggle_warnings(
642 &mut self,
643 _: &ToggleWarnings,
644 window: &mut Window,
645 cx: &mut Context<Self>,
646 ) {
647 let include_warnings = !self.include_warnings;
648 let max_severity = Self::max_diagnostics_severity(include_warnings);
649
650 self.editor.update(cx, |editor, cx| {
651 editor.set_max_diagnostics_severity(max_severity, cx);
652 });
653
654 self.include_warnings = include_warnings;
655 self.diagnostics.clear();
656 self.update_all_diagnostics(window, cx);
657 }
658
659 fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
660 match include_warnings {
661 true => DiagnosticSeverity::Warning,
662 false => DiagnosticSeverity::Error,
663 }
664 }
665
666 #[cfg(test)]
667 pub fn editor(&self) -> &Entity<Editor> {
668 &self.editor
669 }
670
671 #[cfg(test)]
672 pub fn summary(&self) -> &DiagnosticSummary {
673 &self.summary
674 }
675}
676
677impl Focusable for BufferDiagnosticsEditor {
678 fn focus_handle(&self, _: &App) -> FocusHandle {
679 self.focus_handle.clone()
680 }
681}
682
683impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
684
685impl Item for BufferDiagnosticsEditor {
686 type Event = EditorEvent;
687
688 fn act_as_type<'a>(
689 &'a self,
690 type_id: std::any::TypeId,
691 self_handle: &'a Entity<Self>,
692 _: &'a App,
693 ) -> Option<gpui::AnyEntity> {
694 if type_id == TypeId::of::<Self>() {
695 Some(self_handle.clone().into())
696 } else if type_id == TypeId::of::<Editor>() {
697 Some(self.editor.clone().into())
698 } else {
699 None
700 }
701 }
702
703 fn added_to_workspace(
704 &mut self,
705 workspace: &mut Workspace,
706 window: &mut Window,
707 cx: &mut Context<Self>,
708 ) {
709 self.editor.update(cx, |editor, cx| {
710 editor.added_to_workspace(workspace, window, cx)
711 });
712 }
713
714 fn can_save(&self, _cx: &App) -> bool {
715 true
716 }
717
718 fn can_split(&self) -> bool {
719 true
720 }
721
722 fn clone_on_split(
723 &self,
724 _workspace_id: Option<workspace::WorkspaceId>,
725 window: &mut Window,
726 cx: &mut Context<Self>,
727 ) -> Task<Option<Entity<Self>>>
728 where
729 Self: Sized,
730 {
731 Task::ready(Some(cx.new(|cx| {
732 BufferDiagnosticsEditor::new(
733 self.project_path.clone(),
734 self.project.clone(),
735 self.buffer.clone(),
736 self.include_warnings,
737 window,
738 cx,
739 )
740 })))
741 }
742
743 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
744 self.editor
745 .update(cx, |editor, cx| editor.deactivated(window, cx));
746 }
747
748 fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
749 self.editor.for_each_project_item(cx, f);
750 }
751
752 fn active_project_path(&self, _cx: &App) -> Option<ProjectPath> {
753 Some(self.project_path.clone())
754 }
755
756 fn has_conflict(&self, cx: &App) -> bool {
757 self.multibuffer.read(cx).has_conflict(cx)
758 }
759
760 fn has_deleted_file(&self, cx: &App) -> bool {
761 self.multibuffer.read(cx).has_deleted_file(cx)
762 }
763
764 fn is_dirty(&self, cx: &App) -> bool {
765 self.multibuffer.read(cx).is_dirty(cx)
766 }
767
768 fn capability(&self, cx: &App) -> Capability {
769 self.multibuffer.read(cx).capability()
770 }
771
772 fn navigate(
773 &mut self,
774 data: Arc<dyn Any + Send>,
775 window: &mut Window,
776 cx: &mut Context<Self>,
777 ) -> bool {
778 self.editor
779 .update(cx, |editor, cx| editor.navigate(data, window, cx))
780 }
781
782 fn reload(
783 &mut self,
784 project: Entity<Project>,
785 window: &mut Window,
786 cx: &mut Context<Self>,
787 ) -> Task<Result<()>> {
788 self.editor.reload(project, window, cx)
789 }
790
791 fn save(
792 &mut self,
793 options: workspace::item::SaveOptions,
794 project: Entity<Project>,
795 window: &mut Window,
796 cx: &mut Context<Self>,
797 ) -> Task<Result<()>> {
798 self.editor.save(options, project, window, cx)
799 }
800
801 fn save_as(
802 &mut self,
803 _project: Entity<Project>,
804 _path: ProjectPath,
805 _window: &mut Window,
806 _cx: &mut Context<Self>,
807 ) -> Task<Result<()>> {
808 unreachable!()
809 }
810
811 fn set_nav_history(
812 &mut self,
813 nav_history: ItemNavHistory,
814 _window: &mut Window,
815 cx: &mut Context<Self>,
816 ) {
817 self.editor.update(cx, |editor, _| {
818 editor.set_nav_history(Some(nav_history));
819 })
820 }
821
822 // Builds the content to be displayed in the tab.
823 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
824 let path_style = self.project.read(cx).path_style(cx);
825 let error_count = self.summary.error_count;
826 let warning_count = self.summary.warning_count;
827 let label = Label::new(
828 self.project_path
829 .path
830 .file_name()
831 .map(|s| s.to_string())
832 .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
833 );
834
835 h_flex()
836 .gap_1()
837 .child(label)
838 .when(error_count == 0 && warning_count == 0, |parent| {
839 parent.child(
840 h_flex()
841 .gap_1()
842 .child(Icon::new(IconName::Check).color(Color::Success)),
843 )
844 })
845 .when(error_count > 0, |parent| {
846 parent.child(
847 h_flex()
848 .gap_1()
849 .child(Icon::new(IconName::XCircle).color(Color::Error))
850 .child(Label::new(error_count.to_string()).color(params.text_color())),
851 )
852 })
853 .when(warning_count > 0, |parent| {
854 parent.child(
855 h_flex()
856 .gap_1()
857 .child(Icon::new(IconName::Warning).color(Color::Warning))
858 .child(Label::new(warning_count.to_string()).color(params.text_color())),
859 )
860 })
861 .into_any_element()
862 }
863
864 fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
865 "Buffer Diagnostics".into()
866 }
867
868 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
869 let path_style = self.project.read(cx).path_style(cx);
870 Some(
871 format!(
872 "Buffer Diagnostics - {}",
873 self.project_path.path.display(path_style)
874 )
875 .into(),
876 )
877 }
878
879 fn telemetry_event_text(&self) -> Option<&'static str> {
880 Some("Buffer Diagnostics Opened")
881 }
882
883 fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
884 Editor::to_item_events(event, f)
885 }
886}
887
888impl Render for BufferDiagnosticsEditor {
889 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
890 let path_style = self.project.read(cx).path_style(cx);
891 let filename = self.project_path.path.display(path_style).to_string();
892 let error_count = self.summary.error_count;
893 let warning_count = match self.include_warnings {
894 true => self.summary.warning_count,
895 false => 0,
896 };
897
898 let child = if error_count + warning_count == 0 {
899 let label = match warning_count {
900 0 => "No problems in",
901 _ => "No errors in",
902 };
903
904 v_flex()
905 .key_context("EmptyPane")
906 .size_full()
907 .gap_1()
908 .justify_center()
909 .items_center()
910 .text_center()
911 .bg(cx.theme().colors().editor_background)
912 .child(
913 div()
914 .h_flex()
915 .child(Label::new(label).color(Color::Muted))
916 .child(
917 Button::new("open-file", filename)
918 .style(ButtonStyle::Transparent)
919 .tooltip(Tooltip::text("Open File"))
920 .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
921 if let Some(workspace) = Workspace::for_window(window, cx) {
922 workspace.update(cx, |workspace, cx| {
923 workspace
924 .open_path(
925 buffer_diagnostics.project_path.clone(),
926 None,
927 true,
928 window,
929 cx,
930 )
931 .detach_and_log_err(cx);
932 })
933 }
934 })),
935 ),
936 )
937 .when(self.summary.warning_count > 0, |div| {
938 let label = match self.summary.warning_count {
939 1 => "Show 1 warning".into(),
940 warning_count => format!("Show {} warnings", warning_count),
941 };
942
943 div.child(
944 Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
945 |buffer_diagnostics_editor, _, window, cx| {
946 buffer_diagnostics_editor.toggle_warnings(
947 &Default::default(),
948 window,
949 cx,
950 );
951 cx.notify();
952 },
953 )),
954 )
955 })
956 } else {
957 div().size_full().child(self.editor.clone())
958 };
959
960 div()
961 .key_context("Diagnostics")
962 .track_focus(&self.focus_handle(cx))
963 .size_full()
964 .child(child)
965 }
966}
967
968impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
969 fn include_warnings(&self, cx: &App) -> bool {
970 self.read_with(cx, |buffer_diagnostics_editor, _cx| {
971 buffer_diagnostics_editor.include_warnings
972 })
973 .unwrap_or(false)
974 }
975
976 fn is_updating(&self, cx: &App) -> bool {
977 self.read_with(cx, |buffer_diagnostics_editor, cx| {
978 buffer_diagnostics_editor.update_excerpts_task.is_some()
979 || buffer_diagnostics_editor
980 .project
981 .read(cx)
982 .language_servers_running_disk_based_diagnostics(cx)
983 .next()
984 .is_some()
985 })
986 .unwrap_or(false)
987 }
988
989 fn stop_updating(&self, cx: &mut App) {
990 let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
991 buffer_diagnostics_editor.update_excerpts_task = None;
992 cx.notify();
993 });
994 }
995
996 fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
997 let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
998 buffer_diagnostics_editor.update_all_excerpts(window, cx);
999 });
1000 }
1001
1002 fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
1003 let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
1004 buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
1005 });
1006 }
1007
1008 fn get_diagnostics_for_buffer(
1009 &self,
1010 _buffer_id: text::BufferId,
1011 cx: &App,
1012 ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
1013 self.read_with(cx, |buffer_diagnostics_editor, _cx| {
1014 buffer_diagnostics_editor.diagnostics.clone()
1015 })
1016 .unwrap_or_default()
1017 }
1018}
1019