Skip to main content

fresh/app/
editor_accessors.rs

1//! Plain accessor methods on `Editor`.
2//!
3//! Configuration getters, key-translator/time-source/event-broadcaster
4//! handles, LSP / completion / update query helpers, mode registry
5//! access, status/warning log setup, and the per-frame timer-check
6//! methods (mouse hover / semantic highlight / diagnostic pull /
7//! completion trigger).
8//!
9//! These are mostly small `&self` queries that read a single field;
10//! grouping them together keeps mod.rs focused on the central
11//! orchestration.
12
13use super::*;
14
15impl Editor {
16    /// Get a reference to the async bridge (if available)
17    pub fn async_bridge(&self) -> Option<&AsyncBridge> {
18        self.async_bridge.as_ref()
19    }
20
21    /// Get a reference to the config
22    pub fn config(&self) -> &Config {
23        &self.config
24    }
25
26    /// Get a mutable reference to the config.
27    ///
28    /// Routes through `Arc::make_mut`: if the plugin state snapshot (or any
29    /// other reader) still holds an `Arc` to the current value, this
30    /// CoW-clones so existing readers observe a stable value and the next
31    /// snapshot refresh sees a new pointer. `Arc<T>` has no `DerefMut`, so
32    /// the only way to mutate through `self.config` is via this accessor —
33    /// there is no code path that can silently leave a reader with stale
34    /// data.
35    ///
36    /// Window-side reads (`Window::config()`) read a *separate* Arc clone
37    /// stashed in `WindowResources`. Mutations through `config_mut`
38    /// therefore leave window clones stale until [`Editor::sync_windows_config`]
39    /// runs. Callers that mutate a config field which Window code reads
40    /// (e.g. `editor.line_wrap`, `editor.enable_inlay_hints`,
41    /// `languages`, etc.) must call `sync_windows_config()` afterwards.
42    /// `set_config` does this automatically.
43    pub fn config_mut(&mut self) -> &mut Config {
44        Arc::make_mut(&mut self.config)
45    }
46
47    /// Replace the config wholesale. Used by the "reload config" path and
48    /// by tests that want to swap in a freshly-parsed file. Constructs a
49    /// fresh `Arc`, so any snapshot that still holds the old value sees
50    /// the pointer move and will reserialize on the next refresh.
51    ///
52    /// Also propagates the new `Arc` to every window's
53    /// `resources.config`, so window-scoped reads see the swap.
54    pub fn set_config(&mut self, new_config: Config) {
55        self.config = Arc::new(new_config);
56        self.sync_windows_config();
57    }
58
59    /// Propagate `self.config` to every window's `resources.config` so
60    /// window-side reads (`Window::config()`) see the latest value.
61    /// `Arc::clone` is cheap, so this is a constant-time fanout per
62    /// window. Called from `set_config` and at the end of any
63    /// `config_mut()`-driven mutation that affects window-read fields.
64    pub(crate) fn sync_windows_config(&mut self) {
65        let cfg = self.config.clone();
66        for w in self.windows.values_mut() {
67            w.resources.config = cfg.clone();
68        }
69    }
70
71    /// Replace the cached raw user config. Like `set_config`, constructs
72    /// a fresh `Arc` so the plugin snapshot notices the change.
73    pub(crate) fn set_user_config_raw(&mut self, value: serde_json::Value) {
74        self.user_config_raw = Arc::new(value);
75    }
76
77    /// Mutable access to the active window's merged diagnostics map.
78    /// Routes through `Arc::make_mut`, which CoW-clones while the
79    /// plugin snapshot still holds the old map — readers never
80    /// observe an in-place mutation.
81    pub(crate) fn stored_diagnostics_mut(
82        &mut self,
83    ) -> &mut HashMap<String, Vec<lsp_types::Diagnostic>> {
84        Arc::make_mut(&mut self.active_window_mut().stored_diagnostics)
85    }
86
87    /// Mutable access to the active window's folding-ranges map.
88    /// Same `Arc::make_mut` CoW pattern as `stored_diagnostics_mut`.
89    pub(crate) fn stored_folding_ranges_mut(
90        &mut self,
91    ) -> &mut HashMap<String, Vec<lsp_types::FoldingRange>> {
92        Arc::make_mut(&mut self.active_window_mut().stored_folding_ranges)
93    }
94
95    /// Get a reference to the key translator (for input calibration)
96    pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
97        &self.key_translator
98    }
99
100    /// Get a reference to the time source
101    pub fn time_source(&self) -> &SharedTimeSource {
102        &self.time_source
103    }
104
105    /// Emit a control event
106    pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
107        self.event_broadcaster.emit_named(name, data);
108    }
109
110    /// Send a response to a plugin for an async operation
111    pub(super) fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
112        self.plugin_manager
113            .read()
114            .unwrap()
115            .deliver_response(response);
116    }
117
118    // `take_pending_semantic_token_request` and
119    // `take_pending_semantic_token_range_request` live on `impl Window`
120    // — call them via `self.active_window_mut()`.
121
122    /// Get all keybindings as (key, action) pairs
123    pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
124        self.keybindings.read().unwrap().get_all_bindings()
125    }
126
127    /// Get the formatted keybinding for a specific action (for display in messages)
128    /// Returns None if no keybinding is found for the action
129    pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
130        self.keybindings
131            .read()
132            .unwrap()
133            .find_keybinding_for_action(action_name, self.active_window().key_context.clone())
134    }
135
136    /// Raw-event counterpart: return the `(KeyCode, KeyModifiers)` currently
137    /// bound to `action` in `context`. Intended for callers that need to
138    /// simulate the user pressing the bound key (e2e tests, some hotkey-
139    /// chaining code) without hardcoding a default that a user's rebind
140    /// would invalidate.
141    pub fn keybinding_event_for_action(
142        &self,
143        action: &crate::input::keybindings::Action,
144        context: crate::input::keybindings::KeyContext,
145    ) -> Option<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)> {
146        self.keybindings
147            .read()
148            .unwrap()
149            .get_keybinding_event_for_action(action, context)
150    }
151
152    /// Get mutable access to the mode registry
153    pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
154        &mut self.mode_registry
155    }
156
157    /// Get immutable access to the mode registry
158    pub fn mode_registry(&self) -> &ModeRegistry {
159        &self.mode_registry
160    }
161
162    /// Get the currently active buffer ID.
163    ///
164    /// This is derived from the split manager (single source of truth).
165    /// The editor always has at least one buffer, so this never fails.
166    ///
167    /// When the active split has a buffer-group tab as its active target
168    /// (i.e., `active_group_tab.is_some()`), this returns the buffer of the
169    /// currently-focused inner panel — so that input routing, command palette
170    /// context, buffer mode, and other "what is the user looking at" queries
171    /// resolve to the panel the user is actually interacting with rather than
172    /// the split's background leaf buffer.
173    ///
174    /// The override only takes effect if the inner panel's buffer is still
175    /// live in `self.buffers`; otherwise it falls back to the main split's
176    /// leaf buffer so callers never see a stale/freed buffer id.
177    #[inline]
178    pub fn active_buffer(&self) -> BufferId {
179        let (_, buf) = self.effective_active_pair();
180        buf
181    }
182
183    /// The split id whose `SplitViewState` owns the currently-focused
184    /// cursors/viewport/buffer state. For a regular split this is just
185    /// `split_manager.active_split()`. For a split that has a group tab
186    /// active, this returns the focused inner panel's leaf id (which
187    /// lives in `split_view_states` even though it's not in the main
188    /// split tree).
189    #[inline]
190    pub fn effective_active_split(&self) -> crate::model::event::LeafId {
191        let (split, _) = self.effective_active_pair();
192        split
193    }
194
195    /// Resolve the effective (split, buffer) pair for the currently-focused
196    /// target. This is the single source of truth — both `active_buffer` and
197    /// `effective_active_split` derive from it so they can never disagree.
198    ///
199    /// Returned invariant: `split_view_states[split]` exists, its
200    /// `active_buffer` equals the returned buffer id, `self.buffers`
201    /// contains the returned buffer id, and `split.keyed_states` contains
202    /// an entry for the returned buffer id. Consequently the mutation path
203    /// in `apply_event_to_active_buffer` (which indexes into
204    /// `keyed_states[buffer]`) is always well-defined for the returned pair.
205    ///
206    /// If a buffer-group panel is focused but any of the invariants above
207    /// is not satisfied for the inner leaf (for example because the panel
208    /// buffer was freed without clearing `focused_group_leaf`), the helper
209    /// falls back to the outer split's own leaf. The fallback is also
210    /// validated before being returned.
211    #[inline]
212    fn effective_active_pair(&self) -> (crate::model::event::LeafId, BufferId) {
213        self.active_window().effective_active_pair()
214    }
215
216    /// Get the mode name for the active buffer.
217    ///
218    /// Resolution order:
219    ///   1. The buffer's own virtual-buffer mode, if it has one.
220    ///   2. The mode declared by the buffer-group containing this
221    ///      buffer (e.g. a `git-log` group's keybindings apply to its
222    ///      panels regardless of whether each panel is a virtual
223    ///      buffer or a file-backed one — `openFileStreaming` produces
224    ///      the latter for streaming detail panels).
225    pub fn active_buffer_mode(&self) -> Option<&str> {
226        let buffer_id = self.active_buffer();
227        let win = self.active_window();
228        if let Some(mode) = win
229            .buffer_metadata
230            .get(&buffer_id)
231            .and_then(|meta| meta.virtual_mode())
232        {
233            return Some(mode);
234        }
235        let group_id = win.buffer_to_group.get(&buffer_id).copied()?;
236        win.buffer_groups.get(&group_id).map(|g| g.mode.as_str())
237    }
238
239    /// Check if the active buffer is read-only
240    pub fn is_active_buffer_read_only(&self) -> bool {
241        if let Some(metadata) = self
242            .active_window()
243            .buffer_metadata
244            .get(&self.active_buffer())
245        {
246            if metadata.read_only {
247                return true;
248            }
249            // Also check if the mode is read-only
250            if let Some(mode_name) = metadata.virtual_mode() {
251                return self.mode_registry.is_read_only(mode_name);
252            }
253        }
254        false
255    }
256
257    // `mark_buffer_read_only` lives on `impl Window` — call it via
258    // `self.active_window_mut().mark_buffer_read_only(buffer_id, ro)`.
259
260    /// Get the effective mode for the active buffer.
261    ///
262    /// Buffer-local mode (virtual buffers) takes precedence over the global
263    /// editor mode, so that e.g. a search-replace panel isn't hijacked by
264    /// a markdown-source or vi-mode global mode.
265    pub fn effective_mode(&self) -> Option<&str> {
266        // When a floating widget panel is mounted, its plugin-defined
267        // mode (`editor.setEditorMode(...)`) takes precedence over the
268        // underlying buffer's virtual mode. Without this, opening the
269        // Orchestrator picker from a python3 terminal session would
270        // resolve mode-keybindings against `"terminal"` instead of
271        // `"orchestrator-open"`, so picker-specific shortcuts like
272        // `Alt+N` never reached their handlers.
273        if self.floating_widget_panel.is_some() {
274            if let Some(mode) = self.active_window().editor_mode.as_deref() {
275                return Some(mode);
276            }
277        }
278        self.active_buffer_mode()
279            .or(self.active_window().editor_mode.as_deref())
280    }
281
282    // `has_active_lsp_progress`, `get_lsp_progress`, and
283    // `is_lsp_server_ready` live on `impl Window` — call them via
284    // `self.active_window().has_active_lsp_progress()` etc.
285
286    /// Read-only view of the editor-wide popup stack.
287    ///
288    /// `global_popups` itself is `pub(crate)` so its internals stay
289    /// private to the app module; tests need to inspect its depth /
290    /// contents to verify "no two popups stacked across the buffer-
291    /// local and global stacks" invariants (e.g. issue 1 of the LSP
292    /// indicator-click bugs), so we expose an immutable accessor here.
293    pub fn global_popups(&self) -> &crate::view::popup::PopupManager {
294        &self.global_popups
295    }
296
297    /// The earliest wall-clock deadline at which the main event loop
298    /// needs to wake up and re-render, *purely because of internal
299    /// time-driven UI elements* (animations, the LSP status-bar
300    /// spinner). Returns `None` when no time-driven UI is in flight —
301    /// the loop can sleep until the next user / async event without
302    /// missing a frame.
303    ///
304    /// The `Some` case includes the LSP-progress spinner: its glyph
305    /// is computed from `SystemTime::now() / 100ms`, so the loop has
306    /// to wake at ~100ms cadence to actually advance it. Without
307    /// this signal, the indicator would only tick when an unrelated
308    /// event caused a frame, and the user would see a "frozen"
309    /// spinner whenever the server stopped emitting `$/progress`
310    /// (e.g. died externally — see #1941 issue 3).
311    pub fn next_periodic_redraw_deadline(&self) -> Option<std::time::Instant> {
312        let lsp_progress_deadline = if self.active_window().has_active_lsp_progress() {
313            // 100ms matches the spinner-glyph period in
314            // `lsp_status::compose_lsp_status`.
315            Some(std::time::Instant::now() + std::time::Duration::from_millis(100))
316        } else {
317            None
318        };
319        let anim_deadline = self.active_window().animations.next_deadline();
320        [lsp_progress_deadline, anim_deadline]
321            .into_iter()
322            .flatten()
323            .min()
324    }
325
326    /// Get stored LSP diagnostics (for testing and external access)
327    /// Returns a reference to the diagnostics map keyed by file URI
328    pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
329        &self.active_window().stored_diagnostics
330    }
331
332    /// Check if an update is available
333    pub fn is_update_available(&self) -> bool {
334        self.update_checker
335            .as_ref()
336            .map(|c| c.is_update_available())
337            .unwrap_or(false)
338    }
339
340    /// Get the latest version string if an update is available
341    pub fn latest_version(&self) -> Option<&str> {
342        self.update_checker
343            .as_ref()
344            .and_then(|c| c.latest_version())
345    }
346
347    /// Get the cached release check result (for shutdown notification)
348    pub fn get_update_result(
349        &self,
350    ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
351        self.update_checker
352            .as_ref()
353            .and_then(|c| c.get_cached_result())
354    }
355
356    /// Set a custom update checker (for testing)
357    ///
358    /// This allows injecting a custom PeriodicUpdateChecker that points to a mock server,
359    /// enabling E2E tests for the update notification UI.
360    #[doc(hidden)]
361    pub fn set_update_checker(
362        &mut self,
363        checker: crate::services::release_checker::PeriodicUpdateChecker,
364    ) {
365        self.update_checker = Some(checker);
366    }
367
368    /// Configure LSP server for a specific language
369    pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
370        let __active_id = self.active_window;
371        if let Some(lsp) = self
372            .windows
373            .get_mut(&__active_id)
374            .and_then(|w| w.lsp.as_mut())
375        {
376            lsp.set_language_configs(language, config);
377        }
378    }
379
380    // `running_lsp_servers`, `pending_completion_requests_count`,
381    // `completion_items_count`, `initialized_lsp_server_count`, and
382    // `shutdown_lsp_server` live on `impl Window` — call them via
383    // `self.active_window()` / `self.active_window_mut()`.
384
385    /// Set up warning log monitoring
386    ///
387    /// When warnings/errors are logged, they will be written to the specified path
388    /// and the editor will be notified via the receiver.
389    pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
390        self.warning_log = Some((receiver, path));
391    }
392
393    /// Take the warning-log receiver+path out of this editor.
394    ///
395    /// The receiver is single-consumer and lives for the process's
396    /// lifetime; on a destructive editor restart (e.g. authority swap)
397    /// `main.rs` lifts it from the old editor and re-installs it on the
398    /// new one so warnings keep flowing post-restart instead of vanishing
399    /// with the dropped editor.
400    pub fn take_warning_log(&mut self) -> Option<(std::sync::mpsc::Receiver<()>, PathBuf)> {
401        self.warning_log.take()
402    }
403
404    /// Set the status message log path
405    pub fn set_status_log_path(&mut self, path: PathBuf) {
406        self.status_log_path = Some(path);
407    }
408
409    /// Queue a new authority and restart the editor.
410    ///
411    /// Per the design decision in `docs/internal/AUTHORITY_DESIGN.md`,
412    /// authority transitions piggy-back on the existing
413    /// `change_working_dir` restart path. The caller never sees an
414    /// editor that is half-transitioned: the current `Editor` is
415    /// dropped, `main.rs` rebuilds a fresh one with the queued
416    /// authority, and session restore reopens buffers against the new
417    /// backend. This is slower than an in-place pointer swap but is
418    /// far more robust — every cached `Arc<dyn FileSystem>`, LSP
419    /// handle, terminal PTY, plugin state, and in-flight task is
420    /// dropped cleanly by the existing restart machinery.
421    pub fn install_authority(&mut self, authority: crate::services::authority::Authority) {
422        self.pending_authority = Some(authority);
423        // Re-open the same working directory; `main.rs` picks up the
424        // pending authority from the old editor just before dropping it.
425        self.request_restart(self.working_dir.clone());
426    }
427
428    /// Restore the default local authority. Same destructive-restart
429    /// semantics as `install_authority` — the caller never observes a
430    /// half-transitioned editor.
431    pub fn clear_authority(&mut self) {
432        // Reuse the editor's live trust handle so the restored local authority
433        // is gated by the same workspace-trust state.
434        let trust = std::sync::Arc::clone(&self.authority.workspace_trust);
435        let env = std::sync::Arc::clone(&self.authority.env_provider);
436        self.install_authority(crate::services::authority::Authority::local(trust, env));
437    }
438
439    /// Take the queued authority (if any). Called by `main.rs` on
440    /// restart to move the queued authority into the fresh editor.
441    pub fn take_pending_authority(&mut self) -> Option<crate::services::authority::Authority> {
442        self.pending_authority.take()
443    }
444
445    /// Directly replace the active authority without triggering a
446    /// restart. Intended for the post-construction wiring in `main.rs`
447    /// only, where the editor is still being set up and there is no
448    /// user-visible state to preserve. Do not call this from the event
449    /// loop — use `install_authority` for that.
450    ///
451    /// Also refreshes the plugin state snapshot so hooks that fire after
452    /// this call (notably `plugins_loaded`, fired by `main.rs` right
453    /// after `set_boot_authority`) see the real `authority_label` instead
454    /// of the empty string the temporary `Authority::local()` carried
455    /// during construction.
456    pub fn set_boot_authority(&mut self, authority: crate::services::authority::Authority) {
457        self.authority = authority;
458        // Propagate the new authority to every window's resources so
459        // window-side filesystem/path-translation reads (`Window::authority()`)
460        // see the swap. `Authority` carries internal `Arc`s, so this just
461        // clones cheap handles.
462        let auth = self.authority.clone();
463        for w in self.windows.values_mut() {
464            w.resources.authority = auth.clone();
465        }
466        // Propagate the authority's long-running spawner into the LSP
467        // manager so `force_spawn` can route server processes through
468        // the right backend. The editor rebuilds on every authority
469        // transition (AUTHORITY_DESIGN.md principle 7), so this is the
470        // single wiring point — no need for a hot-swap API. Path
471        // translation rides along for the same reason — LSP URIs need
472        // to be host↔container-translated under the new authority.
473        let __active_id = self.active_window;
474        if let Some(lsp) = self
475            .windows
476            .get_mut(&__active_id)
477            .and_then(|w| w.lsp.as_mut())
478        {
479            lsp.set_long_running_spawner(self.authority.long_running_spawner.clone());
480            lsp.set_path_translation(self.authority.path_translation.clone());
481            lsp.set_workspace_trust(self.authority.workspace_trust.clone());
482        }
483        #[cfg(feature = "plugins")]
484        {
485            self.update_plugin_state_snapshot();
486            // Notify plugins so they can re-register state-gated
487            // commands (e.g. devcontainer `Attach` only when not
488            // attached). Production transitions also trigger a full
489            // editor restart that re-runs plugin init, but firing
490            // here keeps in-process transitions and the test harness
491            // (which simulates the restart inline) consistent.
492            let label = self.authority.display_label.clone();
493            self.plugin_manager.read().unwrap().run_hook(
494                "authority_changed",
495                crate::services::plugins::hooks::HookArgs::AuthorityChanged { label },
496            );
497        }
498    }
499
500    /// Read-only access to the active authority.
501    pub fn authority(&self) -> &crate::services::authority::Authority {
502        &self.authority
503    }
504
505    /// The editor's current working directory.  This is the project
506    /// root; individual buffers may live elsewhere.
507    pub fn working_dir(&self) -> &std::path::Path {
508        &self.working_dir
509    }
510
511    /// The currently active `Session`. Always `WindowId(1)` until
512    /// the multi-session migration step lands; until then this is
513    /// effectively a typed wrapper around `working_dir`. New code
514    /// should prefer this accessor so the eventual migration is a
515    /// no-op for the call site.
516    ///
517    /// Panics if the active session id is not present in the
518    /// `sessions` map. That invariant is upheld by the constructor
519    /// and `setActiveWindow` (when added) — if the panic ever fires
520    /// it indicates a bug in session lifecycle code.
521    pub fn active_window(&self) -> &crate::app::window::Window {
522        self.windows
523            .get(&self.active_window)
524            .expect("active_window id must be a member of sessions")
525    }
526
527    /// The active session's id.
528    pub fn active_session_id(&self) -> fresh_core::WindowId {
529        self.active_window
530    }
531
532    /// Allocate the next globally-unique `BufferId`. Use this in
533    /// `impl Editor` handler bodies that mint new buffer ids. Handlers
534    /// that have already moved to `impl Window` use
535    /// `Window::alloc_buffer_id` (which delegates to the same
536    /// `Arc<BufferIdAllocator>` shared via `WindowResources`).
537    ///
538    /// Keeps `next_buffer_id` in sync with the allocator's high-water
539    /// mark so workspace snapshots that read the `next_buffer_id`
540    /// counter directly continue to see a correct value. The
541    /// allocator's atomic is the source of truth; this counter mirrors
542    /// it for serialization compatibility.
543    pub(crate) fn alloc_buffer_id(&mut self) -> fresh_core::BufferId {
544        let id = self.buffer_id_alloc.next();
545        // Bump the legacy counter past the freshly-issued id so
546        // workspace serialization snapshots see a value at least one
547        // greater than every issued id.
548        if id.0 + 1 > self.next_buffer_id {
549            self.next_buffer_id = id.0 + 1;
550        }
551        id
552    }
553
554    /// Number of sessions currently in the editor. Always 1 until
555    /// the multi-session step lands.
556    pub fn session_count(&self) -> usize {
557        self.windows.len()
558    }
559
560    /// Look up a session by id. Returns `None` if `id` is not in
561    /// the sessions map. Useful for tests; production code that
562    /// needs the active session should use `active_window()`.
563    pub fn session(&self, id: fresh_core::WindowId) -> Option<&crate::app::window::Window> {
564        self.windows.get(&id)
565    }
566
567    /// Active session's utility-dock panel-id → buffer-id map.
568    /// Used by tests to assert that the active window's dock
569    /// occupancy is what was set on it. (Pre-0b this asserted
570    /// "warm-swap restored the stash"; post-0b every window owns
571    /// its own dock, so the assertion is just "this window's
572    /// `panel_ids` map matches expectations.")
573    #[doc(hidden)]
574    pub fn panel_ids_for_test(&self) -> &std::collections::HashMap<String, fresh_core::BufferId> {
575        self.panel_ids()
576    }
577
578    /// Inject a panel_ids entry. Used by tests to populate the
579    /// active session's dock occupancy without going through the
580    /// async plugin command path.
581    #[doc(hidden)]
582    pub fn insert_panel_id_for_test(&mut self, key: String, buffer_id: fresh_core::BufferId) {
583        self.panel_ids_mut().insert(key, buffer_id);
584    }
585
586    /// True iff the active session has an LSP manager attached.
587    /// Used by tests to assert that the active window's `lsp`
588    /// slot is populated. (Pre-0b this exercised the warm-swap
589    /// code; post-0b the LSP manager lives directly on `Window`,
590    /// so the assertion is just "this window's `lsp` is `Some`.")
591    #[doc(hidden)]
592    pub fn has_lsp_for_test(&self) -> bool {
593        self.lsp().is_some()
594    }
595
596    /// Inject an LspManager so tests can prove the swap routes
597    /// it through the session stash without depending on real
598    /// LSP server spawn.
599    #[doc(hidden)]
600    pub fn install_dummy_lsp_for_test(&mut self) {
601        let active = self.active_window;
602        self.active_window_mut().lsp =
603            Some(crate::services::lsp::manager::LspManager::new(active, None));
604    }
605
606    /// Most-recent `path_changed` event the editor received.
607    /// Test-only — used by `watch_path` e2e tests to assert
608    /// kernel events surfaced to the editor.
609    #[doc(hidden)]
610    pub fn last_path_change_for_test(&self) -> Option<&(u64, std::path::PathBuf, &'static str)> {
611        self.last_path_change_for_test.as_ref()
612    }
613
614    /// Most-recent `WatchPathRegistered` plugin response, paired
615    /// with its request_id. Test-only.
616    #[doc(hidden)]
617    pub fn last_watch_response_for_test(&self) -> Option<&(u64, Result<u64, String>)> {
618        self.last_watch_response_for_test.as_ref()
619    }
620
621    /// Inject an mtime entry into the active session's mod-time
622    /// cache. Used by tests to populate `Window.file_mod_times`
623    /// without going through real file I/O. (Pre-0b this was
624    /// reaching the warm-swap stash; post-0b it's a direct
625    /// insert into the active window's cache.)
626    #[doc(hidden)]
627    pub fn insert_mtime_for_test(&mut self, path: std::path::PathBuf, t: std::time::SystemTime) {
628        self.file_mod_times_mut().insert(path, t);
629    }
630
631    /// Whether the active session's mtime cache contains `path`.
632    #[doc(hidden)]
633    pub fn has_mtime_for_test(&self, path: &std::path::Path) -> bool {
634        self.file_mod_times().contains_key(path)
635    }
636
637    /// Mutable access to the active session. Used by lifecycle code
638    /// that re-targets per-session state (renaming, etc.). Same
639    /// panic invariant as `active_window()`.
640    pub fn active_window_mut(&mut self) -> &mut crate::app::window::Window {
641        let id = self.active_window;
642        self.windows
643            .get_mut(&id)
644            .expect("active_window id must be a member of sessions")
645    }
646
647    /// The active window's layout-cache (split-leaf rects, tab rects,
648    /// file-explorer rect, view-line mappings). Mouse hit-testing and
649    /// visual-line motion read from here.
650    pub(crate) fn active_layout(&self) -> &crate::app::types::WindowLayoutCache {
651        &self.active_window().layout_cache
652    }
653
654    /// Mutable handle to the active window's layout cache. Renderer
655    /// writes split / tab / file-explorer hit-test rects here at the
656    /// end of each frame.
657    pub(crate) fn active_layout_mut(&mut self) -> &mut crate::app::types::WindowLayoutCache {
658        &mut self.active_window_mut().layout_cache
659    }
660
661    /// The active window's editor-chrome layout cache (status bar,
662    /// menu, popups, prompt overlay, full-frame cell-theme map).
663    /// Mouse hit-testing reads from here.
664    pub(crate) fn active_chrome(&self) -> &crate::app::types::ChromeLayout {
665        &self.active_window().chrome_layout
666    }
667
668    /// Mutable handle to the active window's chrome-layout cache.
669    /// Renderer writes status-bar / menu / popup / prompt-overlay
670    /// hit-test rects here at the end of each frame.
671    pub(crate) fn active_chrome_mut(&mut self) -> &mut crate::app::types::ChromeLayout {
672        &mut self.active_window_mut().chrome_layout
673    }
674
675    /// Active window's utility-dock panel-id → buffer-id map.
676    /// Each window owns its own dock; switching windows shows a
677    /// different (possibly empty) dock.
678    pub(crate) fn panel_ids(&self) -> &std::collections::HashMap<String, BufferId> {
679        &self.active_window().panel_ids
680    }
681
682    /// Mutable handle to the active window's panel-id map.
683    pub(crate) fn panel_ids_mut(&mut self) -> &mut std::collections::HashMap<String, BufferId> {
684        &mut self.active_window_mut().panel_ids
685    }
686
687    /// Active window's open-file mtime cache. Auto-revert only
688    /// fires for files in the active window — dormant windows
689    /// keep their mtime snapshot until the next dive.
690    pub(crate) fn file_mod_times(
691        &self,
692    ) -> &std::collections::HashMap<std::path::PathBuf, std::time::SystemTime> {
693        &self.active_window().file_mod_times
694    }
695
696    /// Mutable handle to the active window's mtime cache.
697    pub(crate) fn file_mod_times_mut(
698        &mut self,
699    ) -> &mut std::collections::HashMap<std::path::PathBuf, std::time::SystemTime> {
700        &mut self.active_window_mut().file_mod_times
701    }
702
703    /// Active window's file-explorer view (`None` if it's never been
704    /// opened in this window). Each window has its own tree;
705    /// switching windows shows that window's view (or none).
706    pub fn file_explorer(&self) -> Option<&FileTreeView> {
707        self.active_window().file_explorer.as_ref()
708    }
709
710    /// Mutable handle to the active window's file-explorer view.
711    /// Holds `&mut self` for the call's lifetime — for sites that
712    /// also need to read other Editor fields, use direct
713    /// `self.windows.get_mut(&self.active_window).and_then(|w| w.file_explorer.as_mut())`
714    /// instead so the borrow on `self.windows` stays disjoint.
715    pub fn file_explorer_mut(&mut self) -> Option<&mut FileTreeView> {
716        self.active_window_mut().file_explorer.as_mut()
717    }
718
719    /// Active window's buffer storage. Each window owns its
720    /// `EditorState` map outright; closing the window drops them.
721    /// Cross-window iteration goes through `self.windows.values()`
722    /// directly.
723    pub(crate) fn buffers(&self) -> &crate::app::window::WindowBuffers {
724        &self.active_window().buffers
725    }
726
727    /// Mutable handle to the active window's buffer storage.
728    /// Holds `&mut self` for the call's lifetime — at sites that
729    /// need a concurrent mutable borrow on another Window field
730    /// (`splits`, `event_logs`, etc.) take a single
731    /// `let window = self.windows.get_mut(&self.active_window).unwrap()`
732    /// and split-access the disjoint sub-fields directly.
733    pub(crate) fn buffers_mut(&mut self) -> &mut crate::app::window::WindowBuffers {
734        &mut self.active_window_mut().buffers
735    }
736
737    /// Active window's LSP manager (`None` if no LSP has been spawned
738    /// for this window yet). Each window has its own LSP set rooted
739    /// at its project root.
740    pub(crate) fn lsp(&self) -> Option<&crate::services::lsp::manager::LspManager> {
741        self.active_window().lsp.as_ref()
742    }
743
744    /// Mutable handle to the active window's LSP manager. Same
745    /// borrow caveat as `file_explorer_mut()`: at sites that also
746    /// need to read other Editor fields, prefer direct
747    /// `self.windows.get_mut(&self.active_window).and_then(|w| w.lsp.as_mut())`.
748    pub(crate) fn lsp_mut(&mut self) -> Option<&mut crate::services::lsp::manager::LspManager> {
749        self.active_window_mut().lsp.as_mut()
750    }
751
752    /// Active window's split tree. Panics if the window has no
753    /// layout yet — the invariant is "the active window always has
754    /// `splits` populated", upheld by `set_active_window` (which
755    /// seeds the layout on first dive) and by editor init (which
756    /// hands the initial layout to the base window).
757    pub(crate) fn split_manager(&self) -> &crate::view::split::SplitManager {
758        &self
759            .active_window()
760            .buffers
761            .splits()
762            .expect("active window must have a populated split layout")
763            .0
764    }
765
766    /// Mutable handle to the active window's split tree.
767    pub(crate) fn split_manager_mut(&mut self) -> &mut crate::view::split::SplitManager {
768        &mut self
769            .active_window_mut()
770            .buffers
771            .splits_mut()
772            .expect("active window must have a populated split layout")
773            .0
774    }
775
776    /// Active window's per-leaf view state map.
777    #[cfg(test)]
778    pub(crate) fn split_view_states(
779        &self,
780    ) -> &std::collections::HashMap<crate::model::event::LeafId, crate::view::split::SplitViewState>
781    {
782        &self
783            .active_window()
784            .buffers
785            .splits()
786            .expect("active window must have a populated split layout")
787            .1
788    }
789
790    /// Mutable handle to the active window's per-leaf view state map.
791    pub(crate) fn split_view_states_mut(
792        &mut self,
793    ) -> &mut std::collections::HashMap<
794        crate::model::event::LeafId,
795        crate::view::split::SplitViewState,
796    > {
797        &mut self
798            .active_window_mut()
799            .buffers
800            .splits_mut()
801            .expect("active window must have a populated split layout")
802            .1
803    }
804
805    /// Return buffer ids whose on-disk path sits at or under `root`.
806    /// Used by file-explorer operations that need to react when a file
807    /// or directory on disk goes away or moves.
808    pub fn buffer_ids_under_path(&self, root: &std::path::Path) -> Vec<BufferId> {
809        self.windows
810            .get(&self.active_window)
811            .map(|w| &w.buffers)
812            .expect("active window present")
813            .iter()
814            .filter_map(|(id, state)| {
815                let p = state.buffer.file_path()?;
816                if p == root || p.starts_with(root) {
817                    Some(*id)
818                } else {
819                    None
820                }
821            })
822            .collect()
823    }
824
825    /// Get remote connection info if editing remote files
826    ///
827    /// Returns `Some("user@host")` for remote editing, `None` for local.
828    pub fn remote_connection_info(&self) -> Option<&str> {
829        self.authority.filesystem.remote_connection_info()
830    }
831
832    /// Get connection string for display in status bar and file explorer.
833    ///
834    /// Per principle 9, identity lives in the authority. The label set
835    /// by whoever constructed the authority wins; if it is empty (the
836    /// SSH constructor leaves it that way) we fall back to the
837    /// filesystem's `remote_connection_info()`, which knows how to
838    /// annotate disconnected SSH sessions.
839    pub fn connection_display_string(&self) -> Option<String> {
840        if !self.authority.display_label.is_empty() {
841            return Some(self.authority.display_label.clone());
842        }
843        self.remote_connection_info().map(|conn| {
844            if self.authority.filesystem.is_remote_connected() {
845                conn.to_string()
846            } else {
847                format!("{} (Disconnected)", conn)
848            }
849        })
850    }
851
852    /// Get the status log path
853    pub fn get_status_log_path(&self) -> Option<&PathBuf> {
854        self.status_log_path.as_ref()
855    }
856
857    /// Open the status log file (user clicked on status message)
858    pub fn open_status_log(&mut self) {
859        if let Some(path) = self.status_log_path.clone() {
860            // Use open_local_file since log files are always local
861            match self.active_window_mut().open_local_file(&path) {
862                Ok(buffer_id) => {
863                    self.active_window_mut()
864                        .mark_buffer_read_only(buffer_id, true);
865                }
866                Err(e) => {
867                    tracing::error!("Failed to open status log: {}", e);
868                }
869            }
870        } else {
871            self.set_status_message("Status log not available".to_string());
872        }
873    }
874
875    /// Check for and handle any new warnings in the warning log
876    ///
877    /// Updates the general warning domain for the status bar.
878    /// Returns true if new warnings were found.
879    pub fn check_warning_log(&mut self) -> bool {
880        let path = match &self.warning_log {
881            Some((receiver, path)) => {
882                let mut new_warning_count = 0usize;
883                while receiver.try_recv().is_ok() {
884                    new_warning_count += 1;
885                }
886                if new_warning_count == 0 {
887                    return false;
888                }
889                (path.clone(), new_warning_count)
890            }
891            None => return false,
892        };
893        let (path, new_warning_count) = path;
894        self.active_window_mut()
895            .warning_domains
896            .general
897            .add_warnings(new_warning_count);
898        self.active_window_mut()
899            .warning_domains
900            .general
901            .set_log_path(path);
902
903        true
904    }
905
906    /// Get the warning domain registry
907    // Warning-domain accessors live on `impl Window`:
908    //  - `clear_warnings` — call as `self.active_window_mut().clear_warnings()`.
909    //  - Read access via `active_window().warning_domains` directly
910    //    (and its `.general` / `.lsp` sub-registries).
911    // `has_lsp_error`, `get_effective_warning_level`,
912    // `get_general_warning_level`, `get_general_warning_count`,
913    // `get_warning_domains`, `get_warning_log_path`,
914    // `clear_warning_indicator` were thin getters with no remaining
915    // callers and have been removed.
916
917    /// Open the warning log file (user-initiated action). Stays on
918    /// `impl Editor` because it calls editor-orchestration helpers
919    /// (`open_local_file`, `mark_buffer_read_only`).
920    pub fn open_warning_log(&mut self) {
921        if let Some(path) = self
922            .active_window_mut()
923            .warning_domains
924            .general
925            .log_path
926            .clone()
927        {
928            // Use open_local_file since log files are always local
929            match self.active_window_mut().open_local_file(&path) {
930                Ok(buffer_id) => {
931                    self.active_window_mut()
932                        .mark_buffer_read_only(buffer_id, true);
933                }
934                Err(e) => {
935                    tracing::error!("Failed to open warning log: {}", e);
936                }
937            }
938        }
939    }
940
941    // `update_lsp_warning_domain` lives on `impl Window` — call it via
942    // `self.active_window_mut().update_lsp_warning_domain()`.
943
944    /// Check if mouse hover timer has expired and trigger LSP hover request
945    ///
946    /// This implements debounced hover - we wait for the configured delay before
947    /// sending the request to avoid spamming the LSP server on every mouse move.
948    /// Returns true if a hover request was triggered.
949    pub fn check_mouse_hover_timer(&mut self) -> bool {
950        // Check if mouse hover is enabled
951        if !self.config.editor.mouse_hover_enabled {
952            return false;
953        }
954
955        let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
956
957        // Get hover state without borrowing self
958        let hover_info = match self.active_window_mut().mouse_state.lsp_hover_state {
959            Some((byte_pos, start_time, screen_x, screen_y)) => {
960                if self.active_window_mut().mouse_state.lsp_hover_request_sent {
961                    return false; // Already sent request for this position
962                }
963                if start_time.elapsed() < hover_delay {
964                    return false; // Timer hasn't expired yet
965                }
966                Some((byte_pos, screen_x, screen_y))
967            }
968            None => return false,
969        };
970
971        let Some((byte_pos, screen_x, screen_y)) = hover_info else {
972            return false;
973        };
974
975        // Store mouse position for popup positioning
976        self.active_window_mut()
977            .hover
978            .set_screen_position((screen_x, screen_y));
979
980        // Request hover at the byte position — only mark as sent if dispatched
981        match self.request_hover_at_position(byte_pos) {
982            Ok(true) => {
983                self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
984                true
985            }
986            Ok(false) => false, // no server ready, timer will retry
987            Err(e) => {
988                tracing::debug!("Failed to request hover: {}", e);
989                false
990            }
991        }
992    }
993
994    // `check_semantic_highlight_timer` lives on `impl Window` — call it
995    // via `self.active_window().check_semantic_highlight_timer()`.
996
997    // `check_diagnostic_pull_timer` lives on `impl Window` — call it via
998    // `self.active_window_mut().check_diagnostic_pull_timer()`. Pulls
999    // run against the active window's LSP manager and its per-window
1000    // `scheduled_diagnostic_pull` debounce slot.
1001
1002    /// Check if completion trigger timer has expired and trigger completion if so
1003    ///
1004    /// This implements debounced completion - we wait for quick_suggestions_delay_ms
1005    /// before sending the completion request to avoid spamming the LSP server.
1006    /// Returns true if a completion request was triggered.
1007    pub fn check_completion_trigger_timer(&mut self) -> bool {
1008        // Check if we have a scheduled completion trigger
1009        let Some(trigger_time) = self.active_window_mut().scheduled_completion_trigger else {
1010            return false;
1011        };
1012
1013        // Check if the timer has expired
1014        if Instant::now() < trigger_time {
1015            return false;
1016        }
1017
1018        // Clear the scheduled trigger
1019        self.active_window_mut().scheduled_completion_trigger = None;
1020
1021        // Don't trigger if a popup is already visible
1022        if self.active_state().popups.is_visible() {
1023            return false;
1024        }
1025
1026        // Trigger the completion request
1027        self.request_completion();
1028
1029        true
1030    }
1031}