Skip to repository content891 lines · 31.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:32:55.967Z 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
diagnostics.rs
1use super::*;
2
3pub trait DiagnosticRenderer {
4 fn render_group(
5 &self,
6 diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
7 buffer_id: BufferId,
8 snapshot: EditorSnapshot,
9 editor: WeakEntity<Editor>,
10 language_registry: Option<Arc<LanguageRegistry>>,
11 cx: &mut App,
12 ) -> Vec<BlockProperties<Anchor>>;
13
14 fn render_hover(
15 &self,
16 diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
17 range: Range<Point>,
18 buffer_id: BufferId,
19 language_registry: Option<Arc<LanguageRegistry>>,
20 cx: &mut App,
21 ) -> Option<Entity<markdown::Markdown>>;
22
23 fn open_link(
24 &self,
25 editor: &mut Editor,
26 link: SharedString,
27 window: &mut Window,
28 cx: &mut Context<Editor>,
29 );
30}
31
32pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) {
33 cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer)));
34}
35
36pub(super) struct GlobalDiagnosticRenderer(Arc<dyn DiagnosticRenderer>);
37
38impl GlobalDiagnosticRenderer {
39 pub(super) fn global(cx: &App) -> Option<Arc<dyn DiagnosticRenderer>> {
40 cx.try_global::<Self>().map(|g| g.0.clone())
41 }
42}
43
44impl gpui::Global for GlobalDiagnosticRenderer {}
45
46#[derive(Debug, Clone)]
47pub(super) struct InlineDiagnostic {
48 pub(super) message: SharedString,
49 pub(super) group_id: usize,
50 pub(super) is_primary: bool,
51 pub(super) start: Point,
52 pub(super) severity: lsp::DiagnosticSeverity,
53}
54
55#[derive(Debug, PartialEq, Eq)]
56pub(super) struct ActiveDiagnosticGroup {
57 active_range: Range<Anchor>,
58 active_message: String,
59 group_id: usize,
60 blocks: HashSet<CustomBlockId>,
61}
62
63#[derive(Debug, PartialEq, Eq)]
64pub(super) enum ActiveDiagnostic {
65 None,
66 All,
67 Group(ActiveDiagnosticGroup),
68}
69
70impl Editor {
71 pub fn go_to_diagnostic(
72 &mut self,
73 action: &GoToDiagnostic,
74 window: &mut Window,
75 cx: &mut Context<Self>,
76 ) {
77 if !self.diagnostics_enabled() {
78 return;
79 }
80
81 self.go_to_diagnostic_at_cursor(Direction::Next, action.severity, window, cx);
82 }
83
84 pub fn go_to_prev_diagnostic(
85 &mut self,
86 action: &GoToPreviousDiagnostic,
87 window: &mut Window,
88 cx: &mut Context<Self>,
89 ) {
90 if !self.diagnostics_enabled() {
91 return;
92 }
93
94 self.go_to_diagnostic_at_cursor(Direction::Prev, action.severity, window, cx);
95 }
96
97 fn diagnostics_before_cursor<'a>(
98 buffer: &'a MultiBufferSnapshot,
99 cursor: MultiBufferOffset,
100 severity: GoToDiagnosticSeverityFilter,
101 ) -> impl Iterator<Item = DiagnosticEntryRef<'a, MultiBufferOffset>> {
102 buffer
103 .diagnostics_in_range(MultiBufferOffset(0)..cursor)
104 .filter(move |entry| entry.range.start <= cursor)
105 .filter(move |entry| severity.matches(entry.diagnostic.severity))
106 .filter(|entry| entry.range.start != entry.range.end)
107 .filter(|entry| !entry.diagnostic.is_unnecessary)
108 }
109
110 fn diagnostics_after_cursor<'a>(
111 buffer: &'a MultiBufferSnapshot,
112 cursor: MultiBufferOffset,
113 severity: GoToDiagnosticSeverityFilter,
114 ) -> impl Iterator<Item = DiagnosticEntryRef<'a, MultiBufferOffset>> {
115 buffer
116 .diagnostics_in_range(cursor..buffer.len())
117 .filter(move |entry| entry.range.start >= cursor)
118 .filter(move |entry| severity.matches(entry.diagnostic.severity))
119 .filter(|entry| entry.range.start != entry.range.end)
120 .filter(|entry| !entry.diagnostic.is_unnecessary)
121 }
122
123 /// Attempts to expand the diagnostic at the current cursor position,
124 /// updating the cursor position to the diagnostic's start point.
125 ///
126 /// In case there's no diagnostic at the current cursor position, this will
127 /// fallback to finding the next or previous diagnostic instead, depending
128 /// on the provided `direction`.
129 pub fn go_to_diagnostic_at_cursor(
130 &mut self,
131 direction: Direction,
132 severity: GoToDiagnosticSeverityFilter,
133 window: &mut Window,
134 cx: &mut Context<Self>,
135 ) {
136 let buffer = self.buffer.read(cx).snapshot(cx);
137 let selection = self
138 .selections
139 .newest::<MultiBufferOffset>(&self.display_snapshot(cx));
140
141 let before = Self::diagnostics_before_cursor(&buffer, selection.start, severity);
142 let after = Self::diagnostics_after_cursor(&buffer, selection.start, severity);
143 let active_group_id = match &self.active_diagnostics {
144 ActiveDiagnostic::Group(group) => Some(group.group_id),
145 _ => None,
146 };
147
148 let mut cursor_on_active = false;
149 let mut target = None;
150
151 for diagnostic in after.chain(before) {
152 let contains_cursor = diagnostic.range.contains(&selection.start)
153 || diagnostic.range.end == selection.head();
154
155 if !contains_cursor {
156 continue;
157 }
158
159 if active_group_id == Some(diagnostic.diagnostic.group_id) {
160 cursor_on_active = true;
161 } else if target.is_none() {
162 target = Some(diagnostic);
163 }
164 }
165
166 match (target, cursor_on_active) {
167 (Some(diagnostic), false) => self.activate_diagnostic(&buffer, diagnostic, window, cx),
168 _ => self.go_to_diagnostic_in_direction(
169 &buffer, &selection, direction, severity, window, cx,
170 ),
171 }
172 }
173
174 fn activate_diagnostic(
175 &mut self,
176 buffer: &MultiBufferSnapshot,
177 diagnostic: DiagnosticEntryRef<MultiBufferOffset>,
178 window: &mut Window,
179 cx: &mut Context<Self>,
180 ) {
181 let diagnostic_start = buffer.anchor_after(diagnostic.range.start);
182 let Some((buffer_anchor, _)) = buffer.anchor_to_buffer_anchor(diagnostic_start) else {
183 return;
184 };
185 let buffer_id = buffer_anchor.buffer_id;
186 let snapshot = self.snapshot(window, cx);
187 if snapshot.intersects_fold(diagnostic.range.start) {
188 self.unfold_ranges(std::slice::from_ref(&diagnostic.range), true, false, cx);
189 }
190 self.change_selections(Default::default(), window, cx, |s| {
191 s.select_ranges(vec![diagnostic.range.start..diagnostic.range.start])
192 });
193 self.activate_diagnostics(buffer_id, diagnostic, window, cx);
194 self.refresh_edit_prediction(
195 true,
196 false,
197 EditPredictionRequestTrigger::DiagnosticNavigation,
198 window,
199 cx,
200 );
201 }
202
203 pub fn go_to_diagnostic_in_direction(
204 &mut self,
205 buffer: &MultiBufferSnapshot,
206 selection: &Selection<MultiBufferOffset>,
207 direction: Direction,
208 severity: GoToDiagnosticSeverityFilter,
209 window: &mut Window,
210 cx: &mut Context<Self>,
211 ) {
212 let mut active_group_id = None;
213 if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics
214 && active_group.active_range.start.to_offset(&buffer) == selection.start
215 {
216 active_group_id = Some(active_group.group_id);
217 }
218
219 let before = Self::diagnostics_before_cursor(&buffer, selection.start, severity);
220 let after = Self::diagnostics_after_cursor(&buffer, selection.start, severity);
221
222 let mut found: Option<DiagnosticEntryRef<MultiBufferOffset>> = None;
223 if direction == Direction::Prev {
224 'outer: for prev_diagnostics in [before.collect::<Vec<_>>(), after.collect::<Vec<_>>()]
225 {
226 for diagnostic in prev_diagnostics.into_iter().rev() {
227 if diagnostic.range.start != selection.start
228 || active_group_id
229 .is_some_and(|active| diagnostic.diagnostic.group_id < active)
230 {
231 found = Some(diagnostic);
232 break 'outer;
233 }
234 }
235 }
236 } else {
237 for diagnostic in after.chain(before) {
238 if diagnostic.range.start != selection.start
239 || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active)
240 {
241 found = Some(diagnostic);
242 break;
243 }
244 }
245 }
246
247 let Some(next_diagnostic) = found else {
248 return;
249 };
250
251 self.activate_diagnostic(&buffer, next_diagnostic, window, cx);
252 }
253
254 #[cfg(any(test, feature = "test-support"))]
255 pub fn active_diagnostic_message(&self) -> Option<&str> {
256 match &self.active_diagnostics {
257 ActiveDiagnostic::Group(group) => Some(group.active_message.as_str()),
258 _ => None,
259 }
260 }
261
262 pub fn set_all_diagnostics_active(&mut self, cx: &mut Context<Self>) {
263 if !self.diagnostics_enabled() {
264 return;
265 }
266 self.dismiss_diagnostics(cx);
267 self.active_diagnostics = ActiveDiagnostic::All;
268 }
269
270 /// Disable inline diagnostics rendering for this editor.
271 pub fn disable_inline_diagnostics(&mut self) {
272 self.inline_diagnostics_enabled = false;
273 self.inline_diagnostics_update = Task::ready(());
274 self.inline_diagnostics.clear();
275 }
276
277 pub fn disable_diagnostics(&mut self, cx: &mut Context<Self>) {
278 self.diagnostics_enabled = false;
279 self.dismiss_diagnostics(cx);
280 self.inline_diagnostics_update = Task::ready(());
281 self.inline_diagnostics.clear();
282 }
283
284 pub fn diagnostics_enabled(&self) -> bool {
285 self.diagnostics_enabled && self.lsp_data_enabled()
286 }
287
288 pub fn inline_diagnostics_enabled(&self) -> bool {
289 self.inline_diagnostics_enabled && self.diagnostics_enabled()
290 }
291
292 pub fn show_inline_diagnostics(&self) -> bool {
293 self.show_inline_diagnostics
294 }
295
296 pub fn toggle_inline_diagnostics(
297 &mut self,
298 _: &ToggleInlineDiagnostics,
299 window: &mut Window,
300 cx: &mut Context<Editor>,
301 ) {
302 self.show_inline_diagnostics = !self.show_inline_diagnostics;
303 self.refresh_inline_diagnostics(false, window, cx);
304 }
305
306 pub fn set_max_diagnostics_severity(&mut self, severity: DiagnosticSeverity, cx: &mut App) {
307 self.diagnostics_max_severity = severity;
308 self.display_map.update(cx, |display_map, _| {
309 display_map.diagnostics_max_severity = self.diagnostics_max_severity;
310 });
311 }
312
313 pub fn toggle_diagnostics(
314 &mut self,
315 _: &ToggleDiagnostics,
316 window: &mut Window,
317 cx: &mut Context<Editor>,
318 ) {
319 let diagnostics_enabled =
320 self.diagnostics_enabled() && self.diagnostics_max_severity != DiagnosticSeverity::Off;
321 self.diagnostics_enabled = !diagnostics_enabled;
322
323 let new_severity = if self.diagnostics_enabled {
324 EditorSettings::get_global(cx)
325 .diagnostics_max_severity
326 .filter(|severity| severity != &DiagnosticSeverity::Off)
327 .unwrap_or(DiagnosticSeverity::Hint)
328 } else {
329 DiagnosticSeverity::Off
330 };
331 self.set_max_diagnostics_severity(new_severity, cx);
332 if self.diagnostics_enabled {
333 self.active_diagnostics = ActiveDiagnostic::None;
334 self.refresh_inline_diagnostics(false, window, cx);
335 } else {
336 self.inline_diagnostics_update = Task::ready(());
337 self.inline_diagnostics.clear();
338 cx.notify();
339 }
340 }
341
342 pub(super) fn all_diagnostics_active(&self) -> bool {
343 self.active_diagnostics == ActiveDiagnostic::All
344 }
345
346 pub(super) fn active_diagnostic_group_id(&self) -> Option<usize> {
347 match &self.active_diagnostics {
348 ActiveDiagnostic::Group(group) => Some(group.group_id),
349 _ => None,
350 }
351 }
352
353 pub(super) fn has_active_diagnostic_group(&self) -> bool {
354 matches!(self.active_diagnostics, ActiveDiagnostic::Group(_))
355 }
356
357 pub(super) fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
358 if !self.diagnostics_enabled() {
359 return;
360 }
361
362 if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics {
363 let buffer = self.buffer.read(cx).snapshot(cx);
364 let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer);
365 let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer);
366 let is_valid = buffer
367 .diagnostics_in_range::<MultiBufferOffset>(primary_range_start..primary_range_end)
368 .any(|entry| {
369 entry.diagnostic.is_primary
370 && !entry.range.is_empty()
371 && entry.range.start == primary_range_start
372 && entry.diagnostic.message == active_diagnostics.active_message
373 });
374
375 if !is_valid {
376 self.dismiss_diagnostics(cx);
377 }
378 }
379 }
380
381 pub(super) fn activate_diagnostics(
382 &mut self,
383 buffer_id: BufferId,
384 diagnostic: DiagnosticEntryRef<'_, MultiBufferOffset>,
385 window: &mut Window,
386 cx: &mut Context<Self>,
387 ) {
388 if !self.diagnostics_enabled() || matches!(self.active_diagnostics, ActiveDiagnostic::All) {
389 return;
390 }
391 self.dismiss_diagnostics(cx);
392 let buffer = self.buffer.read(cx).snapshot(cx);
393
394 let blocks = if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
395 let snapshot = self.snapshot(window, cx);
396 let diagnostic_group = buffer
397 .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
398 .collect::<Vec<_>>();
399
400 let language_registry = self
401 .project()
402 .map(|project| project.read(cx).languages().clone());
403
404 let blocks = renderer.render_group(
405 diagnostic_group,
406 buffer_id,
407 snapshot,
408 cx.weak_entity(),
409 language_registry,
410 cx,
411 );
412
413 self.display_map.update(cx, |display_map, cx| {
414 display_map.insert_blocks(blocks, cx).into_iter().collect()
415 })
416 } else {
417 // Ensure that, even if there's no global renderer set, we still use
418 // an empty set of blocks, such that we can record the active group
419 // below instead of bailing out.
420 HashSet::default()
421 };
422
423 self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup {
424 active_range: buffer.anchor_before(diagnostic.range.start)
425 ..buffer.anchor_after(diagnostic.range.end),
426 active_message: diagnostic.diagnostic.message.clone(),
427 group_id: diagnostic.diagnostic.group_id,
428 blocks,
429 });
430 cx.notify();
431 }
432
433 pub(super) fn dismiss_diagnostics(&mut self, cx: &mut Context<Self>) {
434 if matches!(self.active_diagnostics, ActiveDiagnostic::All) {
435 return;
436 };
437
438 let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None);
439 if let ActiveDiagnostic::Group(group) = prev {
440 self.display_map.update(cx, |display_map, cx| {
441 display_map.remove_blocks(group.blocks, cx);
442 });
443 cx.notify();
444 }
445 }
446
447 pub(super) fn refresh_inline_diagnostics(
448 &mut self,
449 debounce: bool,
450 window: &mut Window,
451 cx: &mut Context<Self>,
452 ) {
453 let max_severity = ProjectSettings::get_global(cx)
454 .diagnostics
455 .inline
456 .max_severity
457 .unwrap_or(self.diagnostics_max_severity);
458
459 if !self.inline_diagnostics_enabled()
460 || !self.diagnostics_enabled()
461 || !self.show_inline_diagnostics
462 || max_severity == DiagnosticSeverity::Off
463 {
464 self.inline_diagnostics_update = Task::ready(());
465 self.inline_diagnostics.clear();
466 cx.notify();
467 return;
468 }
469
470 let debounce_ms = ProjectSettings::get_global(cx)
471 .diagnostics
472 .inline
473 .update_debounce_ms;
474 let debounce = if debounce && debounce_ms > 0 {
475 Some(Duration::from_millis(debounce_ms))
476 } else {
477 None
478 };
479 self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| {
480 if let Some(debounce) = debounce {
481 cx.background_executor().timer(debounce).await;
482 }
483 let Some(snapshot) = editor.upgrade().map(|editor| {
484 editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
485 }) else {
486 return;
487 };
488
489 let new_inline_diagnostics = cx
490 .background_spawn(async move {
491 let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new();
492 for diagnostic_entry in
493 snapshot.diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
494 {
495 let message = diagnostic_entry
496 .diagnostic
497 .message
498 .split_once('\n')
499 .map(|(line, _)| line)
500 .map(SharedString::new)
501 .unwrap_or_else(|| {
502 SharedString::new(&*diagnostic_entry.diagnostic.message)
503 });
504 let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start);
505 let (Ok(i) | Err(i)) = inline_diagnostics
506 .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot));
507 inline_diagnostics.insert(
508 i,
509 (
510 start_anchor,
511 InlineDiagnostic {
512 message,
513 group_id: diagnostic_entry.diagnostic.group_id,
514 start: diagnostic_entry.range.start.to_point(&snapshot),
515 is_primary: diagnostic_entry.diagnostic.is_primary,
516 severity: diagnostic_entry.diagnostic.severity,
517 },
518 ),
519 );
520 }
521 inline_diagnostics
522 })
523 .await;
524
525 editor
526 .update(cx, |editor, cx| {
527 editor.inline_diagnostics = new_inline_diagnostics;
528 cx.notify();
529 })
530 .ok();
531 });
532 }
533
534 pub(super) fn pull_diagnostics(
535 &mut self,
536 buffer_id: BufferId,
537 _window: &Window,
538 cx: &mut Context<Self>,
539 ) -> Option<()> {
540 // `ActiveDiagnostic::All` is a special mode where editor's diagnostics are managed by the external view,
541 // skip any LSP updates for it.
542
543 if self.active_diagnostics == ActiveDiagnostic::All || !self.diagnostics_enabled() {
544 return None;
545 }
546 let pull_diagnostics_settings = ProjectSettings::get_global(cx)
547 .diagnostics
548 .lsp_pull_diagnostics;
549 if !pull_diagnostics_settings.enabled {
550 return None;
551 }
552 let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
553 let project = self.project()?.downgrade();
554 let buffer = self.buffer().read(cx).buffer(buffer_id)?;
555
556 self.pull_diagnostics_task = cx.spawn(async move |_, cx| {
557 cx.background_executor().timer(debounce).await;
558 if let Ok(task) = project.update(cx, |project, cx| {
559 project.lsp_store().update(cx, |lsp_store, cx| {
560 lsp_store.pull_diagnostics_for_buffer(buffer, cx)
561 })
562 }) {
563 task.await.log_err();
564 }
565 project
566 .update(cx, |project, cx| {
567 project.lsp_store().update(cx, |lsp_store, cx| {
568 lsp_store.pull_document_diagnostics_for_buffer_edit(buffer_id, cx);
569 })
570 })
571 .log_err();
572 });
573
574 Some(())
575 }
576
577 pub(super) fn update_diagnostics_state(
578 &mut self,
579 window: &mut Window,
580 cx: &mut Context<'_, Editor>,
581 ) {
582 if !self.diagnostics_enabled() {
583 return;
584 }
585 self.refresh_active_diagnostics(cx);
586 self.refresh_inline_diagnostics(true, window, cx);
587 self.scrollbar_marker_state.dirty = true;
588 cx.notify();
589 }
590
591 pub fn any_active_diagnostics(&self) -> bool {
592 match &self.active_diagnostics {
593 ActiveDiagnostic::None => false,
594 ActiveDiagnostic::All => true,
595 ActiveDiagnostic::Group(_) => true,
596 }
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use crate::{
603 actions::{
604 ToggleCodeLens, ToggleDiagnostics, ToggleInlayHints, ToggleInlineDiagnostics,
605 ToggleSemanticHighlights,
606 },
607 editor_tests::init_test,
608 test::editor_test_context::EditorTestContext,
609 };
610 use gpui::{Action, TestAppContext, UpdateGlobal};
611 use indoc::indoc;
612 use language::DiagnosticSourceKind;
613 use lsp::LanguageServerId;
614 use settings::{DelayMs, SettingsStore};
615 use std::sync::{
616 Arc,
617 atomic::{self, AtomicUsize},
618 };
619 use util::path;
620
621 fn setup_inline_diagnostics(cx: &mut EditorTestContext) {
622 cx.update(|_, cx| {
623 SettingsStore::update_global(cx, |store, cx| {
624 store.update_user_settings(cx, |settings| {
625 let inline = settings
626 .diagnostics
627 .get_or_insert_default()
628 .inline
629 .get_or_insert_default();
630 inline.enabled = Some(true);
631 inline.update_debounce_ms = Some(DelayMs(0));
632 });
633 });
634 });
635
636 cx.set_state(indoc! {"
637 fn func(abc dˇef: i32) -> u32 {
638 }
639 "});
640
641 let lsp_store =
642 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
643 cx.update(|_, cx| {
644 lsp_store.update(cx, |lsp_store, cx| {
645 lsp_store
646 .update_diagnostics(
647 LanguageServerId(0),
648 lsp::PublishDiagnosticsParams {
649 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
650 version: None,
651 diagnostics: vec![lsp::Diagnostic {
652 range: lsp::Range::new(
653 lsp::Position::new(0, 12),
654 lsp::Position::new(0, 15),
655 ),
656 severity: Some(lsp::DiagnosticSeverity::ERROR),
657 message: "cannot find value `def`".to_string(),
658 ..Default::default()
659 }],
660 },
661 None,
662 DiagnosticSourceKind::Pushed,
663 &[],
664 cx,
665 )
666 .unwrap()
667 });
668 });
669 cx.run_until_parked();
670
671 cx.update_editor(|editor, _, _| {
672 assert_eq!(
673 editor.inline_diagnostics.len(),
674 1,
675 "inline diagnostics should appear after the language server publishes them"
676 );
677 });
678 }
679
680 #[gpui::test]
681 async fn test_toggle_diagnostics_refreshes_inline_diagnostics(cx: &mut TestAppContext) {
682 init_test(cx, |_| {});
683 let mut cx = EditorTestContext::new(cx).await;
684 setup_inline_diagnostics(&mut cx);
685
686 cx.update_editor(|editor, window, cx| {
687 editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
688 });
689 cx.run_until_parked();
690 cx.update_editor(|editor, _, _| {
691 assert_eq!(
692 editor.inline_diagnostics.len(),
693 0,
694 "inline diagnostics should be cleared after disabling diagnostics"
695 );
696 });
697
698 cx.update_editor(|editor, window, cx| {
699 editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
700 });
701 cx.run_until_parked();
702 cx.update_editor(|editor, _, _| {
703 assert_eq!(
704 editor.inline_diagnostics.len(),
705 1,
706 "inline diagnostics should reappear after re-enabling diagnostics, without further editor events"
707 );
708 });
709 }
710
711 #[gpui::test]
712 async fn test_toggle_inline_diagnostics_notifies_on_hide(cx: &mut TestAppContext) {
713 init_test(cx, |_| {});
714 let mut cx = EditorTestContext::new(cx).await;
715 setup_inline_diagnostics(&mut cx);
716
717 let notify_count = Arc::new(AtomicUsize::new(0));
718 let editor = cx.editor.clone();
719 let _subscription = cx.update({
720 let notify_count = notify_count.clone();
721 move |_, cx| {
722 cx.observe(&editor, move |_, _| {
723 notify_count.fetch_add(1, atomic::Ordering::SeqCst);
724 })
725 }
726 });
727
728 cx.update_editor(|editor, window, cx| {
729 editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
730 });
731 cx.update_editor(|editor, _, _| {
732 assert_eq!(
733 editor.inline_diagnostics.len(),
734 0,
735 "inline diagnostics should be cleared after toggling them off"
736 );
737 });
738 assert_eq!(
739 notify_count.load(atomic::Ordering::SeqCst),
740 1,
741 "toggling inline diagnostics off should notify to repaint the editor"
742 );
743
744 cx.update_editor(|editor, window, cx| {
745 editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
746 });
747 cx.run_until_parked();
748 cx.update_editor(|editor, _, _| {
749 assert_eq!(
750 editor.inline_diagnostics.len(),
751 1,
752 "inline diagnostics should reappear after toggling them back on"
753 );
754 });
755 }
756
757 #[gpui::test]
758 async fn test_actions_gated_by_lsp_data(cx: &mut TestAppContext) {
759 init_test(cx, |_| {});
760 let mut cx = EditorTestContext::new(cx).await;
761 let lsp_data_actions: [&dyn Action; 4] = [
762 &ToggleDiagnostics,
763 &ToggleInlayHints,
764 &ToggleCodeLens,
765 &ToggleSemanticHighlights,
766 ];
767
768 cx.update_editor(|editor, _, cx| {
769 editor.enable_lsp_data = false;
770 cx.notify();
771 });
772 cx.update(|window, cx| {
773 for action in lsp_data_actions {
774 assert!(
775 !window.is_action_available(action, cx),
776 "{} should not be available when LSP data is disabled",
777 action.name()
778 );
779 }
780 });
781
782 cx.update_editor(|editor, _, cx| {
783 editor.enable_lsp_data = true;
784 cx.notify();
785 });
786 cx.update(|window, cx| {
787 for action in lsp_data_actions {
788 assert!(
789 window.is_action_available(action, cx),
790 "{} should be available again after re-enabling LSP data",
791 action.name()
792 );
793 }
794 });
795 }
796
797 #[gpui::test]
798 async fn test_toggle_diagnostics_remains_available_after_disabling_diagnostics(
799 cx: &mut TestAppContext,
800 ) {
801 init_test(cx, |_| {});
802 let mut cx = EditorTestContext::new(cx).await;
803
804 cx.dispatch_action(ToggleDiagnostics);
805 cx.update_editor(|editor, window, cx| {
806 assert!(
807 !editor.diagnostics_enabled(),
808 "diagnostics should be disabled after dispatching ToggleDiagnostics"
809 );
810 assert!(
811 window.is_action_available(&ToggleDiagnostics, cx),
812 "ToggleDiagnostics should still be available after disabling diagnostics, \
813 so the user can re-enable it"
814 );
815 });
816
817 cx.dispatch_action(ToggleDiagnostics);
818 cx.update_editor(|editor, window, cx| {
819 assert!(
820 editor.diagnostics_enabled(),
821 "diagnostics should be re-enabled after a second dispatch of ToggleDiagnostics"
822 );
823 assert!(
824 window.is_action_available(&ToggleDiagnostics, cx),
825 "ToggleDiagnostics should be available again after re-enabling diagnostics"
826 );
827 });
828 }
829
830 #[gpui::test]
831 async fn test_toggle_inline_diagnostics_availability(cx: &mut TestAppContext) {
832 init_test(cx, |_| {});
833 let mut cx = EditorTestContext::new(cx).await;
834
835 cx.dispatch_action(ToggleDiagnostics);
836 cx.update(|window, cx| {
837 assert!(
838 !window.is_action_available(&ToggleInlineDiagnostics, cx),
839 "ToggleInlineDiagnostics should not be available when diagnostics are disabled"
840 );
841 });
842
843 cx.dispatch_action(ToggleDiagnostics);
844 cx.update(|window, cx| {
845 assert!(
846 window.is_action_available(&ToggleInlineDiagnostics, cx),
847 "ToggleInlineDiagnostics should be available again after re-enabling diagnostics"
848 );
849 });
850
851 let initial_show = cx.update_editor(|editor, _, _| editor.show_inline_diagnostics());
852 cx.dispatch_action(ToggleInlineDiagnostics);
853 cx.update_editor(|editor, window, cx| {
854 assert_eq!(
855 editor.show_inline_diagnostics(),
856 !initial_show,
857 "inline diagnostics visibility should flip after dispatching ToggleInlineDiagnostics"
858 );
859 assert!(
860 window.is_action_available(&ToggleInlineDiagnostics, cx),
861 "ToggleInlineDiagnostics should still be available after toggling it, \
862 so the user can toggle it back"
863 );
864 });
865
866 cx.dispatch_action(ToggleInlineDiagnostics);
867 cx.update_editor(|editor, window, cx| {
868 assert_eq!(
869 editor.show_inline_diagnostics(),
870 initial_show,
871 "inline diagnostics visibility should flip back after a second dispatch of ToggleInlineDiagnostics"
872 );
873 assert!(
874 window.is_action_available(&ToggleInlineDiagnostics, cx),
875 "ToggleInlineDiagnostics should remain available after toggling it back"
876 );
877 });
878
879 cx.update_editor(|editor, _, cx| {
880 editor.disable_inline_diagnostics();
881 cx.notify();
882 });
883 cx.update(|window, cx| {
884 assert!(
885 !window.is_action_available(&ToggleInlineDiagnostics, cx),
886 "ToggleInlineDiagnostics should not be available in editors with inline diagnostics permanently disabled"
887 );
888 });
889 }
890}
891