Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:32:05.592Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

diagnostics.rs

891 lines · 31.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.