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 // Paste-pending deadline: the editor tick falls back to the
321 // internal clipboard if the async arboard read doesn't return
322 // by this point. Including it here makes the main loop wake
323 // exactly when the timeout needs to fire, so a hung clipboard
324 // owner can't block the UI past `PASTE_ASYNC_DEADLINE`.
325 let paste_deadline = self.next_paste_deadline();
326 // Note: the terminal-title poll deadline is intentionally NOT folded
327 // in here. This deadline path caps the loop's wait to one frame
328 // (~16ms) for smooth animation, which would turn the ~1s title poll
329 // into a 60Hz busy loop. The loop's existing 50ms idle poll is fine
330 // granularity to notice `terminal_titles_need_poll` going true.
331 [lsp_progress_deadline, anim_deadline, paste_deadline]
332 .into_iter()
333 .flatten()
334 .min()
335 }
336
337 /// Earliest time a terminal tab needs its foreground-process title
338 /// re-polled, across all windows. `None` when no window has an
339 /// auto-named (non-explicit) terminal. Drives the event loop's periodic
340 /// wakeups so a tab reflects a command that starts or exits while the
341 /// UI is otherwise idle (the render that follows runs
342 /// `Window::sync_terminal_titles`).
343 pub fn terminal_title_poll_deadline(&self) -> Option<std::time::Instant> {
344 if !self.config.editor.terminal_auto_title {
345 return None;
346 }
347 let mut earliest: Option<std::time::Instant> = None;
348 for window in self.windows.values() {
349 let has_auto = window
350 .terminal_buffers
351 .keys()
352 .any(|b| !window.terminal_explicit_titles.contains(b));
353 if !has_auto {
354 continue;
355 }
356 let deadline = match window.terminal_fg_poll_at {
357 Some(last) => last + crate::app::terminal::FG_POLL_INTERVAL,
358 None => std::time::Instant::now(),
359 };
360 earliest = Some(earliest.map_or(deadline, |e| e.min(deadline)));
361 }
362 earliest
363 }
364
365 /// Whether a terminal tab is due for a foreground-process title poll
366 /// (its deadline has passed). The event loop ORs this into its
367 /// `needs_render` decision so the periodic wakeup actually paints a
368 /// frame, matching how animations and the LSP spinner are handled.
369 pub fn terminal_titles_need_poll(&self) -> bool {
370 self.terminal_title_poll_deadline()
371 .is_some_and(|d| d <= std::time::Instant::now())
372 }
373
374 /// Get stored LSP diagnostics (for testing and external access)
375 /// Returns a reference to the diagnostics map keyed by file URI
376 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
377 &self.active_window().stored_diagnostics
378 }
379
380 /// Check if an update is available
381 pub fn is_update_available(&self) -> bool {
382 self.update_checker
383 .as_ref()
384 .map(|c| c.is_update_available())
385 .unwrap_or(false)
386 }
387
388 /// Get the latest version string if an update is available
389 pub fn latest_version(&self) -> Option<&str> {
390 self.update_checker
391 .as_ref()
392 .and_then(|c| c.latest_version())
393 }
394
395 /// Get the cached release check result (for shutdown notification)
396 pub fn get_update_result(
397 &self,
398 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
399 self.update_checker
400 .as_ref()
401 .and_then(|c| c.get_cached_result())
402 }
403
404 /// Set a custom update checker (for testing)
405 ///
406 /// This allows injecting a custom PeriodicUpdateChecker that points to a mock server,
407 /// enabling E2E tests for the update notification UI.
408 #[doc(hidden)]
409 pub fn set_update_checker(
410 &mut self,
411 checker: crate::services::release_checker::PeriodicUpdateChecker,
412 ) {
413 self.update_checker = Some(checker);
414 }
415
416 /// Configure LSP server for a specific language
417 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
418 let __active_id = self.active_window;
419 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
420 lsp.set_language_configs(language, config);
421 }
422 }
423
424 // `running_lsp_servers`, `pending_completion_requests_count`,
425 // `completion_items_count`, `initialized_lsp_server_count`, and
426 // `shutdown_lsp_server` live on `impl Window` — call them via
427 // `self.active_window()` / `self.active_window_mut()`.
428
429 /// Set up warning log monitoring
430 ///
431 /// When warnings/errors are logged, they will be written to the specified path
432 /// and the editor will be notified via the receiver.
433 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
434 self.warning_log = Some((receiver, path));
435 }
436
437 /// Take the warning-log receiver+path out of this editor.
438 ///
439 /// The receiver is single-consumer and lives for the process's
440 /// lifetime; on a destructive editor restart (e.g. authority swap)
441 /// `main.rs` lifts it from the old editor and re-installs it on the
442 /// new one so warnings keep flowing post-restart instead of vanishing
443 /// with the dropped editor.
444 pub fn take_warning_log(&mut self) -> Option<(std::sync::mpsc::Receiver<()>, PathBuf)> {
445 self.warning_log.take()
446 }
447
448 /// Set the status message log path
449 pub fn set_status_log_path(&mut self, path: PathBuf) {
450 self.status_log_path = Some(path);
451 }
452
453 /// Queue a new authority and restart the editor.
454 ///
455 /// Per the design decision in `docs/internal/AUTHORITY_DESIGN.md`,
456 /// authority transitions piggy-back on the existing
457 /// `change_working_dir` restart path. The caller never sees an
458 /// editor that is half-transitioned: the current `Editor` is
459 /// dropped, `main.rs` rebuilds a fresh one with the queued
460 /// authority, and session restore reopens buffers against the new
461 /// backend. This is slower than an in-place pointer swap but is
462 /// far more robust — every cached `Arc<dyn FileSystem>`, LSP
463 /// handle, terminal PTY, plugin state, and in-flight task is
464 /// dropped cleanly by the existing restart machinery.
465 pub fn install_authority(&mut self, authority: crate::services::authority::Authority) {
466 self.pending_authority = Some(authority);
467 // Re-open the same working directory; `main.rs` picks up the
468 // pending authority from the old editor just before dropping it.
469 self.request_restart(self.working_dir().to_path_buf());
470 }
471
472 /// Install a new authority that owns a live connection, parking its
473 /// keepalive bundle so the connection survives the restart.
474 ///
475 /// Remote-agent backends (SSH-style, K8s) hold carrier processes,
476 /// reconnect/heartbeat tasks, and a Tokio handle that must outlive
477 /// the `Editor` rebuild — exactly the role of the daemon's
478 /// `session_keepalive` slot. The restart loop pairs
479 /// `take_pending_authority` with `take_pending_keepalive` and moves
480 /// the bundle into the process-/server-level keepalive, dropping the
481 /// previous one (tearing down the prior connection). Opaque
482 /// `Box<dyn Any + Send>` so core/main need not name the backend.
483 pub fn install_authority_with_keepalive(
484 &mut self,
485 authority: crate::services::authority::Authority,
486 keepalive: Box<dyn std::any::Any + Send>,
487 working_dir: std::path::PathBuf,
488 ) {
489 // Unlike `install_authority` (which re-opens the *current* working
490 // dir), a remote-agent attach must re-root the editor at the pod-side
491 // workspace — otherwise the explorer, quick-open, and open-file all
492 // operate on the local host path, which doesn't exist in the pod.
493 self.pending_keepalive = Some(keepalive);
494 self.pending_authority = Some(authority);
495 self.request_restart(working_dir);
496 }
497
498 /// Restore the default local authority. Same destructive-restart
499 /// semantics as `install_authority` — the caller never observes a
500 /// half-transitioned editor.
501 pub fn clear_authority(&mut self) {
502 // Reuse the editor's live trust handle so the restored local authority
503 // is gated by the same workspace-trust state.
504 let trust = std::sync::Arc::clone(&self.authority().workspace_trust);
505 let env = std::sync::Arc::clone(&self.authority().env_provider);
506 // Detaching returns this session to a plain local backend; clear its
507 // persisted spec so a later restore doesn't try to reconnect a
508 // backend the user explicitly left.
509 self.active_window_mut().authority_spec =
510 crate::services::authority::SessionAuthoritySpec::Local;
511 self.install_authority(crate::services::authority::Authority::local(trust, env));
512 }
513
514 /// Take the queued authority (if any). Called by `main.rs` on
515 /// restart to move the queued authority into the fresh editor.
516 pub fn take_pending_authority(&mut self) -> Option<crate::services::authority::Authority> {
517 self.pending_authority.take()
518 }
519
520 /// Take the keepalive bundle queued alongside a pending authority by
521 /// [`Self::install_authority_with_keepalive`]. Called by the restart
522 /// loop right beside `take_pending_authority` so the new connection's
523 /// carrier/tasks are parked before the old `Editor` is dropped.
524 pub fn take_pending_keepalive(&mut self) -> Option<Box<dyn std::any::Any + Send>> {
525 self.pending_keepalive.take()
526 }
527
528 /// Directly replace the active authority without triggering a
529 /// restart. Intended for the post-construction wiring in `main.rs`
530 /// only, where the editor is still being set up and there is no
531 /// user-visible state to preserve. Do not call this from the event
532 /// loop — use `install_authority` for that.
533 ///
534 /// Also refreshes the plugin state snapshot so hooks that fire after
535 /// this call (notably `plugins_loaded`, fired by `main.rs` right
536 /// after `set_boot_authority`) see the real `authority_label` instead
537 /// of the empty string the temporary `Authority::local()` carried
538 /// during construction.
539 pub fn set_boot_authority(&mut self, authority: crate::services::authority::Authority) {
540 // The installed authority belongs to the *active/owning* session only
541 // — the backend for the working-dir project the attach (or
542 // `fresh user@host` launch) re-rooted at. Background windows are
543 // distinct projects and must NOT inherit it; each gets its **own**
544 // fresh local authority (its own trust + env scoped to its root), so
545 // trusting/activating in the active project never leaks into them. The
546 // active window owns `authority`; per-window LSP backends are
547 // re-pointed from whatever authority that window will own.
548 let active_id = self.active_window;
549 let bg_roots: Vec<(fresh_core::WindowId, std::path::PathBuf)> = self
550 .windows
551 .iter()
552 .filter(|(id, _)| **id != active_id)
553 .map(|(id, w)| (*id, w.root.clone()))
554 .collect();
555 // (root → fresh local authority) for every background window.
556 let mut installs: Vec<(fresh_core::WindowId, crate::services::authority::Authority)> =
557 bg_roots
558 .into_iter()
559 .map(|(id, root)| {
560 (
561 id,
562 crate::services::authority::Authority::local_scoped(
563 self.session_scope_for(&root),
564 ),
565 )
566 })
567 .collect();
568 installs.push((active_id, authority));
569 // Re-point each window's LSP backend, then **move** the authority into
570 // the window (single owner — never cloned).
571 for (id, a) in installs {
572 if let Some(w) = self.windows.get_mut(&id) {
573 w.lsp
574 .set_long_running_spawner(a.long_running_spawner.clone());
575 w.lsp.set_path_translation(a.path_translation.clone());
576 w.lsp.set_workspace_trust(a.workspace_trust.clone());
577 w.authority = a;
578 }
579 }
580 // Re-point quick-open's file provider at the now-active backend (the
581 // provider captured the previous authority's filesystem + spawner).
582 let (fs, sp) = {
583 let a = &self.active_window().authority;
584 (a.filesystem.clone(), a.process_spawner.clone())
585 };
586 self.quick_open_registry.set_file_backends(fs, sp);
587 #[cfg(feature = "plugins")]
588 {
589 self.update_plugin_state_snapshot();
590 // Notify plugins so they can re-register state-gated commands
591 // (e.g. devcontainer `Attach` only when not attached).
592 let label = self.active_window().authority.display_label.clone();
593 self.plugin_manager.read().unwrap().run_hook(
594 "authority_changed",
595 crate::services::plugins::hooks::HookArgs::AuthorityChanged { label },
596 );
597 }
598 }
599
600 /// The active window's id. The active session under the (in-progress)
601 /// per-session model; today pinned to the single window.
602 pub fn active_window_id(&self) -> fresh_core::WindowId {
603 self.active_window
604 }
605
606 /// Swap a *single* session's (window's) authority without a restart —
607 /// the per-session counterpart to [`Self::set_boot_authority`], which
608 /// fans one authority across every window at boot.
609 ///
610 /// Updates that window's `resources.authority` and re-points its LSP
611 /// backend (long-running spawner, path translation, trust); when the
612 /// window is the active one, mirrors into the editor-wide `authority`
613 /// cache the rest of the editor reads and fires the `authority_changed`
614 /// hook. This is the activation primitive a per-session attach (the
615 /// planned `attachRemoteAgent` op, and the Orchestrator session-swap)
616 /// builds on, and the seam that lets distinct windows hold distinct
617 /// authorities concurrently (`AUTHORITY_DESIGN.md` §"Evolution:
618 /// per-session authority").
619 ///
620 /// Caveat — why production attach still goes through the destructive
621 /// `install_authority` restart: like `set_boot_authority`, this does
622 /// not invalidate per-buffer captured filesystem handles or terminals
623 /// opened under the previous authority. Hot-swapping those safely is
624 /// the remaining per-window cache-invalidation work gated on the live
625 /// multi-session migration; until it lands, this method is the
626 /// infrastructure seam, exercised by tests and the activation path,
627 /// not yet the user-facing attach.
628 /// Set a session's **backend spec** — the persisted descriptor of how to
629 /// rebuild/reconnect its backend ([`SessionAuthoritySpec`]). Independent of
630 /// the live [`Authority`]: a session is *dormant* when its spec is remote
631 /// but its live authority is the local placeholder (no keepalive), which is
632 /// what `reconnect_dormant_session_if_needed` acts on. Set by the install
633 /// points (`setAuthority`, born-attached attach) and on restore.
634 pub fn set_session_authority_spec(
635 &mut self,
636 window_id: fresh_core::WindowId,
637 spec: crate::services::authority::SessionAuthoritySpec,
638 ) {
639 if let Some(w) = self.windows.get_mut(&window_id) {
640 w.authority_spec = spec;
641 }
642 }
643
644 pub fn set_session_authority(
645 &mut self,
646 window_id: fresh_core::WindowId,
647 authority: crate::services::authority::Authority,
648 ) {
649 let is_active = self.active_window == window_id;
650 if let Some(w) = self.windows.get_mut(&window_id) {
651 // Re-point this window's LSP backend, then **move** the authority
652 // into the window it owns (single owner — never cloned).
653 let lsp = &mut w.lsp;
654 lsp.set_long_running_spawner(authority.long_running_spawner.clone());
655 lsp.set_path_translation(authority.path_translation.clone());
656 lsp.set_workspace_trust(authority.workspace_trust.clone());
657 w.authority = authority;
658 }
659 if is_active {
660 // The active backend *is* this window's authority now — re-point
661 // quick-open's file provider at it (same stale-capture fix).
662 let (fs, sp) = {
663 let a = &self.active_window().authority;
664 (a.filesystem.clone(), a.process_spawner.clone())
665 };
666 self.quick_open_registry.set_file_backends(fs, sp);
667 #[cfg(feature = "plugins")]
668 {
669 self.update_plugin_state_snapshot();
670 let label = self.active_window().authority.display_label.clone();
671 self.plugin_manager.read().unwrap().run_hook(
672 "authority_changed",
673 crate::services::plugins::hooks::HookArgs::AuthorityChanged { label },
674 );
675 }
676 }
677 }
678
679 /// Adopt the now-active window's authority into the editor-wide caches,
680 /// called by [`Self::set_active_window`] right after the active pointer
681 /// moves. The per-window `resources.authority` is already correct (each
682 /// window owns its own, re-pointed at creation / on
683 /// `set_session_authority`); this propagates it to the single editor-wide
684 /// `self.authority` the rest of the editor reads, and re-points quick-open
685 /// at the new backend's filesystem/spawner.
686 ///
687 /// `previous_label` is the active backend's label *before* the switch:
688 /// when it is unchanged (the overwhelmingly common local→local case, and
689 /// any switch between same-backend windows) we skip the
690 /// `authority_changed` hook + snapshot churn so window switching stays
691 /// cheap and the status bar doesn't flicker.
692 pub(crate) fn adopt_active_window_authority(&mut self, previous_label: &str) {
693 // No editor-wide copy to update — the active backend *is*
694 // `active_window().authority`. Re-point quick-open at it and fire the
695 // hook when the label actually changed.
696 let (label_changed, fs, sp) = {
697 let a = &self.active_window().authority;
698 (
699 a.display_label != previous_label,
700 a.filesystem.clone(),
701 a.process_spawner.clone(),
702 )
703 };
704 self.quick_open_registry.set_file_backends(fs, sp);
705 if label_changed {
706 #[cfg(feature = "plugins")]
707 {
708 self.update_plugin_state_snapshot();
709 let label = self.active_window().authority.display_label.clone();
710 self.plugin_manager.read().unwrap().run_hook(
711 "authority_changed",
712 crate::services::plugins::hooks::HookArgs::AuthorityChanged { label },
713 );
714 }
715 }
716 }
717
718 /// Read-only access to the active authority.
719 pub fn authority(&self) -> &crate::services::authority::Authority {
720 // The editor's active backend *is* the active window's authority —
721 // there is no separate editor-wide copy. Each window owns its
722 // authority outright (no `Clone`), so a session's backend/trust/env
723 // can never be shared into another window (issue #2280).
724 &self.active_window().authority
725 }
726
727 /// Move the active window's `Authority` out, leaving a local placeholder.
728 /// Used by the restart loops to carry the active session's backend into
729 /// the rebuilt editor across a *non-transition* restart — `Authority` is
730 /// non-`Clone`, so it must be moved. The editor is being torn down
731 /// immediately after, so the placeholder left behind is never observed.
732 pub fn take_active_authority(&mut self) -> crate::services::authority::Authority {
733 let placeholder = crate::services::authority::Authority::local(
734 std::sync::Arc::new(crate::services::workspace_trust::WorkspaceTrust::permissive()),
735 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
736 );
737 std::mem::replace(&mut self.active_window_mut().authority, placeholder)
738 }
739
740 /// The editor's current working directory — the active window's
741 /// project root. Derived, not stored: there is no separate
742 /// `working_dir` field that could drift out of sync with the active
743 /// window (issue #2056). Individual buffers may live elsewhere.
744 pub fn working_dir(&self) -> &std::path::Path {
745 &self.active_window().root
746 }
747
748 /// Marker files/dirs that triggered the workspace-trust prompt (for the
749 /// semantic trust-dialog projection). See view/scene.rs.
750 pub(crate) fn workspace_trust_markers(&self) -> &[String] {
751 &self.workspace_trust_markers
752 }
753
754 /// Whether the workspace-trust prompt is cancellable (palette-invoked) vs a
755 /// mandatory startup gate.
756 pub(crate) fn workspace_trust_cancellable(&self) -> bool {
757 self.workspace_trust_prompt_cancellable
758 }
759
760 /// The directory context this editor was constructed against (config_dir,
761 /// data_dir, …). Exposed so e2e tests can wire trust stores and other
762 /// per-project state to the same locations the editor reads from.
763 pub fn dir_context(&self) -> &crate::config_io::DirectoryContext {
764 &self.dir_context
765 }
766
767 /// The currently active `Session`. Always `WindowId(1)` until
768 /// the multi-session migration step lands; until then this is
769 /// effectively a typed wrapper around `working_dir`. New code
770 /// should prefer this accessor so the eventual migration is a
771 /// no-op for the call site.
772 ///
773 /// Panics if the active session id is not present in the
774 /// `sessions` map. That invariant is upheld by the constructor
775 /// and `setActiveWindow` (when added) — if the panic ever fires
776 /// it indicates a bug in session lifecycle code.
777 pub fn active_window(&self) -> &crate::app::window::Window {
778 self.windows
779 .get(&self.active_window)
780 .expect("active_window id must be a member of sessions")
781 }
782
783 pub(crate) fn file_explorer_slot_providers(
784 &self,
785 ) -> crate::view::file_tree::ExplorerSlotProviders {
786 crate::view::file_tree::default_slot_providers()
787 }
788
789 pub(crate) fn file_explorer_slot_resolver(
790 &self,
791 ) -> crate::view::file_tree::ExplorerSlotResolver<'static> {
792 self.file_explorer_slot_providers().resolver()
793 }
794
795 /// The active session's id.
796 pub fn active_session_id(&self) -> fresh_core::WindowId {
797 self.active_window
798 }
799
800 /// True iff the editor-global dock is open AND currently holds
801 /// keyboard focus. Test helpers use this to wait for `Toggle Dock`'s
802 /// async focus-grab to settle before dispatching subsequent keys;
803 /// without that readiness check, keys can race into the editor
804 /// during the gap and the test silently waits for a dock response
805 /// that never comes.
806 pub fn is_dock_focused(&self) -> bool {
807 self.dock.as_ref().is_some_and(|d| d.focused)
808 }
809
810 /// Allocate the next globally-unique `BufferId`. Use this in
811 /// `impl Editor` handler bodies that mint new buffer ids. Handlers
812 /// that have already moved to `impl Window` use
813 /// `Window::alloc_buffer_id` (which delegates to the same
814 /// `Arc<BufferIdAllocator>` shared via `WindowResources`).
815 ///
816 /// Keeps `next_buffer_id` in sync with the allocator's high-water
817 /// mark so workspace snapshots that read the `next_buffer_id`
818 /// counter directly continue to see a correct value. The
819 /// allocator's atomic is the source of truth; this counter mirrors
820 /// it for serialization compatibility.
821 pub(crate) fn alloc_buffer_id(&mut self) -> fresh_core::BufferId {
822 let id = self.buffer_id_alloc.next();
823 // Bump the legacy counter past the freshly-issued id so
824 // workspace serialization snapshots see a value at least one
825 // greater than every issued id.
826 if id.0 + 1 > self.next_buffer_id {
827 self.next_buffer_id = id.0 + 1;
828 }
829 id
830 }
831
832 /// Number of sessions currently in the editor. Always 1 until
833 /// the multi-session step lands.
834 pub fn session_count(&self) -> usize {
835 self.windows.len()
836 }
837
838 /// Look up a session by id. Returns `None` if `id` is not in
839 /// the sessions map. Useful for tests; production code that
840 /// needs the active session should use `active_window()`.
841 pub fn session(&self, id: fresh_core::WindowId) -> Option<&crate::app::window::Window> {
842 self.windows.get(&id)
843 }
844
845 /// `(id, label)` for every remote session discovered at boot but not yet
846 /// connected — the authority-less `dormant_remote` descriptors that have no
847 /// `Window` (so they don't show up in [`Self::session`]). Diagnostic / test
848 /// accessor: a dive (`SetActiveWindow`) connects one and promotes it to a
849 /// real window. See `bring_dormant_remote_online`.
850 #[doc(hidden)]
851 pub fn dormant_remote_sessions_for_test(&self) -> Vec<(fresh_core::WindowId, String)> {
852 self.dormant_remote
853 .iter()
854 .map(|(id, d)| (*id, d.label.clone()))
855 .collect()
856 }
857
858 /// Active session's utility-dock panel-id → buffer-id map.
859 /// Used by tests to assert that the active window's dock
860 /// occupancy is what was set on it. (Pre-0b this asserted
861 /// "warm-swap restored the stash"; post-0b every window owns
862 /// its own dock, so the assertion is just "this window's
863 /// `panel_ids` map matches expectations.")
864 #[doc(hidden)]
865 pub fn panel_ids_for_test(&self) -> &std::collections::HashMap<String, fresh_core::BufferId> {
866 self.panel_ids()
867 }
868
869 /// Inject a panel_ids entry. Used by tests to populate the
870 /// active session's dock occupancy without going through the
871 /// async plugin command path.
872 #[doc(hidden)]
873 pub fn insert_panel_id_for_test(&mut self, key: String, buffer_id: fresh_core::BufferId) {
874 self.panel_ids_mut().insert(key, buffer_id);
875 }
876
877 /// True iff the active session has an LSP manager attached. Every
878 /// window now owns one by construction (`Window::new`), so this is
879 /// always true; the helper is retained as a regression guard so a
880 /// future change that reintroduces a manager-less window state is
881 /// caught by the orchestrator-window tests.
882 #[doc(hidden)]
883 pub fn has_lsp_for_test(&self) -> bool {
884 self.lsp().is_some()
885 }
886
887 /// Most-recent `path_changed` event the editor received.
888 /// Test-only — used by `watch_path` e2e tests to assert
889 /// kernel events surfaced to the editor.
890 #[doc(hidden)]
891 pub fn last_path_change_for_test(&self) -> Option<&(u64, std::path::PathBuf, &'static str)> {
892 self.last_path_change_for_test.as_ref()
893 }
894
895 /// Most-recent `WatchPathRegistered` plugin response, paired
896 /// with its request_id. Test-only.
897 #[doc(hidden)]
898 pub fn last_watch_response_for_test(&self) -> Option<&(u64, Result<u64, String>)> {
899 self.last_watch_response_for_test.as_ref()
900 }
901
902 /// Inject an mtime entry into the active session's mod-time
903 /// cache. Used by tests to populate `Window.file_mod_times`
904 /// without going through real file I/O. (Pre-0b this was
905 /// reaching the warm-swap stash; post-0b it's a direct
906 /// insert into the active window's cache.)
907 #[doc(hidden)]
908 pub fn insert_mtime_for_test(&mut self, path: std::path::PathBuf, t: std::time::SystemTime) {
909 self.file_mod_times_mut().insert(path, t);
910 }
911
912 /// Whether the active session's mtime cache contains `path`.
913 #[doc(hidden)]
914 pub fn has_mtime_for_test(&self, path: &std::path::Path) -> bool {
915 self.file_mod_times().contains_key(path)
916 }
917
918 /// Mutable access to the active session. Used by lifecycle code
919 /// that re-targets per-session state (renaming, etc.). Same
920 /// panic invariant as `active_window()`.
921 pub fn active_window_mut(&mut self) -> &mut crate::app::window::Window {
922 let id = self.active_window;
923 self.windows
924 .get_mut(&id)
925 .expect("active_window id must be a member of sessions")
926 }
927
928 /// Borrow one of the two coexisting widget-panel slots (centered
929 /// modal vs. left dock). See `PanelSlot`.
930 pub(crate) fn panel(
931 &self,
932 slot: crate::app::PanelSlot,
933 ) -> Option<&crate::app::FloatingWidgetState> {
934 match slot {
935 crate::app::PanelSlot::Floating => self.floating_widget_panel.as_ref(),
936 crate::app::PanelSlot::Dock => self.dock.as_ref(),
937 }
938 }
939
940 /// Mutable handle to one of the two widget-panel slots.
941 pub(crate) fn panel_mut(
942 &mut self,
943 slot: crate::app::PanelSlot,
944 ) -> Option<&mut crate::app::FloatingWidgetState> {
945 match slot {
946 crate::app::PanelSlot::Floating => self.floating_widget_panel.as_mut(),
947 crate::app::PanelSlot::Dock => self.dock.as_mut(),
948 }
949 }
950
951 /// Mutable handle to the slot *option* itself (for take/assign).
952 pub(crate) fn panel_opt_mut(
953 &mut self,
954 slot: crate::app::PanelSlot,
955 ) -> &mut Option<crate::app::FloatingWidgetState> {
956 match slot {
957 crate::app::PanelSlot::Floating => &mut self.floating_widget_panel,
958 crate::app::PanelSlot::Dock => &mut self.dock,
959 }
960 }
961
962 /// Which slot currently holds the panel with this identity, if any.
963 #[cfg(feature = "plugins")]
964 pub(crate) fn slot_of_panel(
965 &self,
966 panel_key: &crate::widgets::PanelKey,
967 ) -> Option<crate::app::PanelSlot> {
968 if self
969 .floating_widget_panel
970 .as_ref()
971 .is_some_and(|f| &f.panel_key == panel_key)
972 {
973 Some(crate::app::PanelSlot::Floating)
974 } else if self
975 .dock
976 .as_ref()
977 .is_some_and(|f| &f.panel_key == panel_key)
978 {
979 Some(crate::app::PanelSlot::Dock)
980 } else {
981 None
982 }
983 }
984
985 /// Map a panel sentinel buffer-id back to its slot.
986 pub(crate) fn slot_for_panel_buffer(buffer_id: BufferId) -> Option<crate::app::PanelSlot> {
987 if buffer_id == crate::app::FLOATING_PANEL_BUFFER_ID {
988 Some(crate::app::PanelSlot::Floating)
989 } else if buffer_id == crate::app::DOCK_PANEL_BUFFER_ID {
990 Some(crate::app::PanelSlot::Dock)
991 } else {
992 None
993 }
994 }
995
996 /// The active window's layout-cache (split-leaf rects, tab rects,
997 /// file-explorer rect, view-line mappings). Mouse hit-testing and
998 /// visual-line motion read from here.
999 pub(crate) fn active_layout(&self) -> &crate::app::types::WindowLayoutCache {
1000 &self.active_window().layout_cache
1001 }
1002
1003 /// Mutable handle to the active window's layout cache. Renderer
1004 /// writes split / tab / file-explorer hit-test rects here at the
1005 /// end of each frame.
1006 pub(crate) fn active_layout_mut(&mut self) -> &mut crate::app::types::WindowLayoutCache {
1007 &mut self.active_window_mut().layout_cache
1008 }
1009
1010 /// The active window's editor-chrome layout cache (status bar,
1011 /// menu, popups, prompt overlay, full-frame cell-theme map).
1012 /// Mouse hit-testing reads from here.
1013 pub(crate) fn active_chrome(&self) -> &crate::app::types::ChromeLayout {
1014 &self.active_window().chrome_layout
1015 }
1016
1017 /// Mutable handle to the active window's chrome-layout cache.
1018 /// Renderer writes status-bar / menu / popup / prompt-overlay
1019 /// hit-test rects here at the end of each frame.
1020 pub(crate) fn active_chrome_mut(&mut self) -> &mut crate::app::types::ChromeLayout {
1021 &mut self.active_window_mut().chrome_layout
1022 }
1023
1024 // --- semantic accessors for the web/GUI chrome (read-only projections) ---
1025
1026 /// The menu-bar state (which menu is open, highlighted item, condition
1027 /// context for `when`/checkbox evaluation).
1028 pub(crate) fn menu_state(&self) -> &crate::view::ui::MenuState {
1029 &self.menu_state
1030 }
1031
1032 /// The keybinding accelerator for an action (e.g. "Ctrl+S"), if any.
1033 pub(crate) fn accelerator_for(&self, action: &str) -> Option<String> {
1034 self.keybindings.read().ok().and_then(|kb| {
1035 kb.find_keybinding_for_action(action, crate::input::keybindings::KeyContext::Normal)
1036 })
1037 }
1038
1039 /// Display name for a buffer (tab label), if known.
1040 pub(crate) fn buffer_display_name(&self, id: fresh_core::BufferId) -> Option<String> {
1041 self.active_window()
1042 .buffer_metadata
1043 .get(&id)
1044 .map(|m| m.display_name.clone())
1045 }
1046
1047 /// Whether a buffer has unsaved changes (for the tab's modified dot).
1048 pub(crate) fn buffer_is_modified(&self, id: fresh_core::BufferId) -> bool {
1049 self.buffers()
1050 .get(&id)
1051 .map(|s| s.buffer.is_modified())
1052 .unwrap_or(false)
1053 }
1054
1055 /// Active window's utility-dock panel-id → buffer-id map.
1056 /// Each window owns its own dock; switching windows shows a
1057 /// different (possibly empty) dock.
1058 pub(crate) fn panel_ids(&self) -> &std::collections::HashMap<String, BufferId> {
1059 &self.active_window().panel_ids
1060 }
1061
1062 /// Mutable handle to the active window's panel-id map.
1063 pub(crate) fn panel_ids_mut(&mut self) -> &mut std::collections::HashMap<String, BufferId> {
1064 &mut self.active_window_mut().panel_ids
1065 }
1066
1067 /// Active window's open-file mtime cache. Auto-revert only
1068 /// fires for files in the active window — dormant windows
1069 /// keep their mtime snapshot until the next dive.
1070 pub(crate) fn file_mod_times(
1071 &self,
1072 ) -> &std::collections::HashMap<std::path::PathBuf, std::time::SystemTime> {
1073 &self.active_window().file_mod_times
1074 }
1075
1076 /// Mutable handle to the active window's mtime cache.
1077 pub(crate) fn file_mod_times_mut(
1078 &mut self,
1079 ) -> &mut std::collections::HashMap<std::path::PathBuf, std::time::SystemTime> {
1080 &mut self.active_window_mut().file_mod_times
1081 }
1082
1083 /// Active window's file-explorer view (`None` if it's never been
1084 /// opened in this window). Each window has its own tree;
1085 /// switching windows shows that window's view (or none).
1086 pub fn file_explorer(&self) -> Option<&FileTreeView> {
1087 self.active_window().file_explorer.as_ref()
1088 }
1089
1090 /// Mutable handle to the active window's file-explorer view.
1091 /// Holds `&mut self` for the call's lifetime — for sites that
1092 /// also need to read other Editor fields, use direct
1093 /// `self.windows.get_mut(&self.active_window).and_then(|w| w.file_explorer.as_mut())`
1094 /// instead so the borrow on `self.windows` stays disjoint.
1095 pub fn file_explorer_mut(&mut self) -> Option<&mut FileTreeView> {
1096 self.active_window_mut().file_explorer.as_mut()
1097 }
1098
1099 /// Active window's buffer storage. Each window owns its
1100 /// `EditorState` map outright; closing the window drops them.
1101 /// Cross-window iteration goes through `self.windows.values()`
1102 /// directly.
1103 pub(crate) fn buffers(&self) -> &crate::app::window::WindowBuffers {
1104 &self.active_window().buffers
1105 }
1106
1107 /// Mutable handle to the active window's buffer storage.
1108 /// Holds `&mut self` for the call's lifetime — at sites that
1109 /// need a concurrent mutable borrow on another Window field
1110 /// (`splits`, `event_logs`, etc.) take a single
1111 /// `let window = self.windows.get_mut(&self.active_window).unwrap()`
1112 /// and split-access the disjoint sub-fields directly.
1113 pub(crate) fn buffers_mut(&mut self) -> &mut crate::app::window::WindowBuffers {
1114 &mut self.active_window_mut().buffers
1115 }
1116
1117 /// Active window's LSP manager. Each window owns one rooted at its
1118 /// project root (built in `Window::new`), so this is always
1119 /// present; the `Option` is retained for call-site ergonomics and
1120 /// because the active-window lookup is itself fallible in spirit.
1121 pub(crate) fn lsp(&self) -> Option<&crate::services::lsp::manager::LspManager> {
1122 Some(&self.active_window().lsp)
1123 }
1124
1125 /// Mutable handle to the active window's LSP manager. Same
1126 /// borrow caveat as `file_explorer_mut()`: at sites that also
1127 /// need to read other Editor fields, prefer direct
1128 /// `self.windows.get_mut(&self.active_window).map(|w| &mut w.lsp)`.
1129 pub(crate) fn lsp_mut(&mut self) -> Option<&mut crate::services::lsp::manager::LspManager> {
1130 Some(&mut self.active_window_mut().lsp)
1131 }
1132
1133 /// Active window's split tree. Panics if the window has no
1134 /// layout yet — the invariant is "the active window always has
1135 /// `splits` populated", upheld by `set_active_window` (which
1136 /// seeds the layout on first dive) and by editor init (which
1137 /// hands the initial layout to the base window).
1138 pub(crate) fn split_manager(&self) -> &crate::view::split::SplitManager {
1139 &self
1140 .active_window()
1141 .buffers
1142 .splits()
1143 .expect("active window must have a populated split layout")
1144 .0
1145 }
1146
1147 /// Mutable handle to the active window's split tree.
1148 pub(crate) fn split_manager_mut(&mut self) -> &mut crate::view::split::SplitManager {
1149 &mut self
1150 .active_window_mut()
1151 .buffers
1152 .splits_mut()
1153 .expect("active window must have a populated split layout")
1154 .0
1155 }
1156
1157 /// Active window's per-leaf view state map.
1158 #[cfg(test)]
1159 pub(crate) fn split_view_states(
1160 &self,
1161 ) -> &std::collections::HashMap<crate::model::event::LeafId, crate::view::split::SplitViewState>
1162 {
1163 &self
1164 .active_window()
1165 .buffers
1166 .splits()
1167 .expect("active window must have a populated split layout")
1168 .1
1169 }
1170
1171 /// Mutable handle to the active window's per-leaf view state map.
1172 pub(crate) fn split_view_states_mut(
1173 &mut self,
1174 ) -> &mut std::collections::HashMap<
1175 crate::model::event::LeafId,
1176 crate::view::split::SplitViewState,
1177 > {
1178 &mut self
1179 .active_window_mut()
1180 .buffers
1181 .splits_mut()
1182 .expect("active window must have a populated split layout")
1183 .1
1184 }
1185
1186 /// Return buffer ids whose on-disk path sits at or under `root`.
1187 /// Used by file-explorer operations that need to react when a file
1188 /// or directory on disk goes away or moves.
1189 pub fn buffer_ids_under_path(&self, root: &std::path::Path) -> Vec<BufferId> {
1190 self.windows
1191 .get(&self.active_window)
1192 .map(|w| &w.buffers)
1193 .expect("active window present")
1194 .iter()
1195 .filter_map(|(id, state)| {
1196 let p = state.buffer.file_path()?;
1197 if p == root || p.starts_with(root) {
1198 Some(*id)
1199 } else {
1200 None
1201 }
1202 })
1203 .collect()
1204 }
1205
1206 /// Get remote connection info if editing remote files
1207 ///
1208 /// Returns `Some("user@host")` for remote editing, `None` for local.
1209 pub fn remote_connection_info(&self) -> Option<&str> {
1210 self.authority().filesystem.remote_connection_info()
1211 }
1212
1213 /// Get connection string for display in status bar and file explorer.
1214 ///
1215 /// Per principle 9, identity lives in the authority. The label set
1216 /// by whoever constructed the authority wins; if it is empty (the
1217 /// SSH constructor leaves it that way) we fall back to the
1218 /// filesystem's `remote_connection_info()`, which knows how to
1219 /// annotate disconnected SSH sessions.
1220 pub fn connection_display_string(&self) -> Option<String> {
1221 if !self.authority().display_label.is_empty() {
1222 return Some(self.authority().display_label.clone());
1223 }
1224 self.remote_connection_info().map(|conn| {
1225 if self.authority().filesystem.is_remote_connected() {
1226 conn.to_string()
1227 } else {
1228 format!("{} (Disconnected)", conn)
1229 }
1230 })
1231 }
1232
1233 /// Get the status log path
1234 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
1235 self.status_log_path.as_ref()
1236 }
1237
1238 /// Open the status log file (user clicked on status message)
1239 pub fn open_status_log(&mut self) {
1240 if let Some(path) = self.status_log_path.clone() {
1241 // Use open_local_file since log files are always local
1242 match self.active_window_mut().open_local_file(&path) {
1243 Ok(buffer_id) => {
1244 self.active_window_mut()
1245 .mark_buffer_read_only(buffer_id, true);
1246 }
1247 Err(e) => {
1248 tracing::error!("Failed to open status log: {}", e);
1249 }
1250 }
1251 } else {
1252 self.set_status_message("Status log not available".to_string());
1253 }
1254 }
1255
1256 /// Check for and handle any new warnings in the warning log
1257 ///
1258 /// Updates the general warning domain for the status bar.
1259 /// Returns true if new warnings were found.
1260 pub fn check_warning_log(&mut self) -> bool {
1261 let path = match &self.warning_log {
1262 Some((receiver, path)) => {
1263 let mut new_warning_count = 0usize;
1264 while receiver.try_recv().is_ok() {
1265 new_warning_count += 1;
1266 }
1267 if new_warning_count == 0 {
1268 return false;
1269 }
1270 (path.clone(), new_warning_count)
1271 }
1272 None => return false,
1273 };
1274 let (path, new_warning_count) = path;
1275 self.active_window_mut()
1276 .warning_domains
1277 .general
1278 .add_warnings(new_warning_count);
1279 self.active_window_mut()
1280 .warning_domains
1281 .general
1282 .set_log_path(path);
1283
1284 true
1285 }
1286
1287 /// Get the warning domain registry
1288 // Warning-domain accessors live on `impl Window`:
1289 // - `clear_warnings` — call as `self.active_window_mut().clear_warnings()`.
1290 // - Read access via `active_window().warning_domains` directly
1291 // (and its `.general` / `.lsp` sub-registries).
1292 // `has_lsp_error`, `get_effective_warning_level`,
1293 // `get_general_warning_level`, `get_general_warning_count`,
1294 // `get_warning_domains`, `get_warning_log_path`,
1295 // `clear_warning_indicator` were thin getters with no remaining
1296 // callers and have been removed.
1297
1298 /// Open the warning log file (user-initiated action). Stays on
1299 /// `impl Editor` because it calls editor-orchestration helpers
1300 /// (`open_local_file`, `mark_buffer_read_only`).
1301 pub fn open_warning_log(&mut self) {
1302 if let Some(path) = self
1303 .active_window_mut()
1304 .warning_domains
1305 .general
1306 .log_path
1307 .clone()
1308 {
1309 // Use open_local_file since log files are always local
1310 match self.active_window_mut().open_local_file(&path) {
1311 Ok(buffer_id) => {
1312 self.active_window_mut()
1313 .mark_buffer_read_only(buffer_id, true);
1314 }
1315 Err(e) => {
1316 tracing::error!("Failed to open warning log: {}", e);
1317 }
1318 }
1319 }
1320 }
1321
1322 // `update_lsp_warning_domain` lives on `impl Window` — call it via
1323 // `self.active_window_mut().update_lsp_warning_domain()`.
1324
1325 /// Check if mouse hover timer has expired and trigger LSP hover request
1326 ///
1327 /// This implements debounced hover - we wait for the configured delay before
1328 /// sending the request to avoid spamming the LSP server on every mouse move.
1329 /// Returns true if a hover request was triggered.
1330 /// True when the LSP status popup (the one opened by clicking the "LSP"
1331 /// indicator in the status bar) is the top popup.
1332 ///
1333 /// Hover popups share the active state's popup stack with it, but the LSP
1334 /// status popup is non-transient, so the hover dismiss-transients pass
1335 /// leaves it in place and a hover would stack on top of it. Callers use
1336 /// this to suppress hover while it is open.
1337 pub(crate) fn is_lsp_status_popup_open(&self) -> bool {
1338 self.active_state()
1339 .popups
1340 .top()
1341 .is_some_and(|p| matches!(p.resolver, crate::view::popup::PopupResolver::LspStatus))
1342 }
1343
1344 pub fn check_mouse_hover_timer(&mut self) -> bool {
1345 // Check if mouse hover is enabled
1346 if !self.config.editor.mouse_hover_enabled {
1347 return false;
1348 }
1349
1350 // Suppress hover while the LSP status popup is open so the hover card
1351 // doesn't stack on top of it.
1352 if self.is_lsp_status_popup_open() {
1353 return false;
1354 }
1355
1356 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
1357
1358 // Get hover state without borrowing self
1359 let hover_info = match self.active_window_mut().mouse_state.lsp_hover_state {
1360 Some((byte_pos, start_time, screen_x, screen_y)) => {
1361 if self.active_window_mut().mouse_state.lsp_hover_request_sent {
1362 return false; // Already sent request for this position
1363 }
1364 if start_time.elapsed() < hover_delay {
1365 return false; // Timer hasn't expired yet
1366 }
1367 Some((byte_pos, screen_x, screen_y))
1368 }
1369 None => return false,
1370 };
1371
1372 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
1373 return false;
1374 };
1375
1376 // Store mouse position for popup positioning
1377 self.active_window_mut()
1378 .hover
1379 .set_screen_position((screen_x, screen_y));
1380
1381 // Request hover at the byte position — only mark as sent if dispatched
1382 match self.request_hover_at_position(byte_pos) {
1383 Ok(true) => {
1384 self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
1385 true
1386 }
1387 Ok(false) => false, // no server ready, timer will retry
1388 Err(e) => {
1389 tracing::debug!("Failed to request hover: {}", e);
1390 false
1391 }
1392 }
1393 }
1394
1395 // `check_semantic_highlight_timer` lives on `impl Window` — call it
1396 // via `self.active_window().check_semantic_highlight_timer()`.
1397
1398 // `check_diagnostic_pull_timer` lives on `impl Window` — call it via
1399 // `self.active_window_mut().check_diagnostic_pull_timer()`. Pulls
1400 // run against the active window's LSP manager and its per-window
1401 // `scheduled_diagnostic_pull` debounce slot.
1402
1403 /// Check if completion trigger timer has expired and trigger completion if so
1404 ///
1405 /// This implements debounced completion - we wait for quick_suggestions_delay_ms
1406 /// before sending the completion request to avoid spamming the LSP server.
1407 /// Returns true if a completion request was triggered.
1408 pub fn check_completion_trigger_timer(&mut self) -> bool {
1409 // Check if we have a scheduled completion trigger
1410 let Some(trigger_time) = self.active_window_mut().scheduled_completion_trigger else {
1411 return false;
1412 };
1413
1414 // Check if the timer has expired
1415 if Instant::now() < trigger_time {
1416 return false;
1417 }
1418
1419 // Clear the scheduled trigger
1420 self.active_window_mut().scheduled_completion_trigger = None;
1421
1422 // Don't trigger if a popup is already visible
1423 if self.active_state().popups.is_visible() {
1424 return false;
1425 }
1426
1427 // Trigger the completion request
1428 self.request_completion();
1429
1430 true
1431 }
1432}