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 (if it's a virtual buffer)
217 pub fn active_buffer_mode(&self) -> Option<&str> {
218 self.active_window()
219 .buffer_metadata
220 .get(&self.active_buffer())
221 .and_then(|meta| meta.virtual_mode())
222 }
223
224 /// Check if the active buffer is read-only
225 pub fn is_active_buffer_read_only(&self) -> bool {
226 if let Some(metadata) = self
227 .active_window()
228 .buffer_metadata
229 .get(&self.active_buffer())
230 {
231 if metadata.read_only {
232 return true;
233 }
234 // Also check if the mode is read-only
235 if let Some(mode_name) = metadata.virtual_mode() {
236 return self.mode_registry.is_read_only(mode_name);
237 }
238 }
239 false
240 }
241
242 // `mark_buffer_read_only` lives on `impl Window` — call it via
243 // `self.active_window_mut().mark_buffer_read_only(buffer_id, ro)`.
244
245 /// Get the effective mode for the active buffer.
246 ///
247 /// Buffer-local mode (virtual buffers) takes precedence over the global
248 /// editor mode, so that e.g. a search-replace panel isn't hijacked by
249 /// a markdown-source or vi-mode global mode.
250 pub fn effective_mode(&self) -> Option<&str> {
251 self.active_buffer_mode()
252 .or(self.active_window().editor_mode.as_deref())
253 }
254
255 // `has_active_lsp_progress`, `get_lsp_progress`, and
256 // `is_lsp_server_ready` live on `impl Window` — call them via
257 // `self.active_window().has_active_lsp_progress()` etc.
258
259 /// Read-only view of the editor-wide popup stack.
260 ///
261 /// `global_popups` itself is `pub(crate)` so its internals stay
262 /// private to the app module; tests need to inspect its depth /
263 /// contents to verify "no two popups stacked across the buffer-
264 /// local and global stacks" invariants (e.g. issue 1 of the LSP
265 /// indicator-click bugs), so we expose an immutable accessor here.
266 pub fn global_popups(&self) -> &crate::view::popup::PopupManager {
267 &self.global_popups
268 }
269
270 /// The earliest wall-clock deadline at which the main event loop
271 /// needs to wake up and re-render, *purely because of internal
272 /// time-driven UI elements* (animations, the LSP status-bar
273 /// spinner). Returns `None` when no time-driven UI is in flight —
274 /// the loop can sleep until the next user / async event without
275 /// missing a frame.
276 ///
277 /// The `Some` case includes the LSP-progress spinner: its glyph
278 /// is computed from `SystemTime::now() / 100ms`, so the loop has
279 /// to wake at ~100ms cadence to actually advance it. Without
280 /// this signal, the indicator would only tick when an unrelated
281 /// event caused a frame, and the user would see a "frozen"
282 /// spinner whenever the server stopped emitting `$/progress`
283 /// (e.g. died externally — see #1941 issue 3).
284 pub fn next_periodic_redraw_deadline(&self) -> Option<std::time::Instant> {
285 let lsp_progress_deadline = if self.active_window().has_active_lsp_progress() {
286 // 100ms matches the spinner-glyph period in
287 // `lsp_status::compose_lsp_status`.
288 Some(std::time::Instant::now() + std::time::Duration::from_millis(100))
289 } else {
290 None
291 };
292 let anim_deadline = self.active_window().animations.next_deadline();
293 [lsp_progress_deadline, anim_deadline]
294 .into_iter()
295 .flatten()
296 .min()
297 }
298
299 /// Get stored LSP diagnostics (for testing and external access)
300 /// Returns a reference to the diagnostics map keyed by file URI
301 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
302 &self.active_window().stored_diagnostics
303 }
304
305 /// Check if an update is available
306 pub fn is_update_available(&self) -> bool {
307 self.update_checker
308 .as_ref()
309 .map(|c| c.is_update_available())
310 .unwrap_or(false)
311 }
312
313 /// Get the latest version string if an update is available
314 pub fn latest_version(&self) -> Option<&str> {
315 self.update_checker
316 .as_ref()
317 .and_then(|c| c.latest_version())
318 }
319
320 /// Get the cached release check result (for shutdown notification)
321 pub fn get_update_result(
322 &self,
323 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
324 self.update_checker
325 .as_ref()
326 .and_then(|c| c.get_cached_result())
327 }
328
329 /// Set a custom update checker (for testing)
330 ///
331 /// This allows injecting a custom PeriodicUpdateChecker that points to a mock server,
332 /// enabling E2E tests for the update notification UI.
333 #[doc(hidden)]
334 pub fn set_update_checker(
335 &mut self,
336 checker: crate::services::release_checker::PeriodicUpdateChecker,
337 ) {
338 self.update_checker = Some(checker);
339 }
340
341 /// Configure LSP server for a specific language
342 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
343 let __active_id = self.active_window;
344 if let Some(lsp) = self
345 .windows
346 .get_mut(&__active_id)
347 .and_then(|w| w.lsp.as_mut())
348 {
349 lsp.set_language_configs(language, config);
350 }
351 }
352
353 // `running_lsp_servers`, `pending_completion_requests_count`,
354 // `completion_items_count`, `initialized_lsp_server_count`, and
355 // `shutdown_lsp_server` live on `impl Window` — call them via
356 // `self.active_window()` / `self.active_window_mut()`.
357
358 /// Set up warning log monitoring
359 ///
360 /// When warnings/errors are logged, they will be written to the specified path
361 /// and the editor will be notified via the receiver.
362 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
363 self.warning_log = Some((receiver, path));
364 }
365
366 /// Take the warning-log receiver+path out of this editor.
367 ///
368 /// The receiver is single-consumer and lives for the process's
369 /// lifetime; on a destructive editor restart (e.g. authority swap)
370 /// `main.rs` lifts it from the old editor and re-installs it on the
371 /// new one so warnings keep flowing post-restart instead of vanishing
372 /// with the dropped editor.
373 pub fn take_warning_log(&mut self) -> Option<(std::sync::mpsc::Receiver<()>, PathBuf)> {
374 self.warning_log.take()
375 }
376
377 /// Set the status message log path
378 pub fn set_status_log_path(&mut self, path: PathBuf) {
379 self.status_log_path = Some(path);
380 }
381
382 /// Queue a new authority and restart the editor.
383 ///
384 /// Per the design decision in `docs/internal/AUTHORITY_DESIGN.md`,
385 /// authority transitions piggy-back on the existing
386 /// `change_working_dir` restart path. The caller never sees an
387 /// editor that is half-transitioned: the current `Editor` is
388 /// dropped, `main.rs` rebuilds a fresh one with the queued
389 /// authority, and session restore reopens buffers against the new
390 /// backend. This is slower than an in-place pointer swap but is
391 /// far more robust — every cached `Arc<dyn FileSystem>`, LSP
392 /// handle, terminal PTY, plugin state, and in-flight task is
393 /// dropped cleanly by the existing restart machinery.
394 pub fn install_authority(&mut self, authority: crate::services::authority::Authority) {
395 self.pending_authority = Some(authority);
396 // Re-open the same working directory; `main.rs` picks up the
397 // pending authority from the old editor just before dropping it.
398 self.request_restart(self.working_dir.clone());
399 }
400
401 /// Restore the default local authority. Same destructive-restart
402 /// semantics as `install_authority` — the caller never observes a
403 /// half-transitioned editor.
404 pub fn clear_authority(&mut self) {
405 self.install_authority(crate::services::authority::Authority::local());
406 }
407
408 /// Take the queued authority (if any). Called by `main.rs` on
409 /// restart to move the queued authority into the fresh editor.
410 pub fn take_pending_authority(&mut self) -> Option<crate::services::authority::Authority> {
411 self.pending_authority.take()
412 }
413
414 /// Directly replace the active authority without triggering a
415 /// restart. Intended for the post-construction wiring in `main.rs`
416 /// only, where the editor is still being set up and there is no
417 /// user-visible state to preserve. Do not call this from the event
418 /// loop — use `install_authority` for that.
419 ///
420 /// Also refreshes the plugin state snapshot so hooks that fire after
421 /// this call (notably `plugins_loaded`, fired by `main.rs` right
422 /// after `set_boot_authority`) see the real `authority_label` instead
423 /// of the empty string the temporary `Authority::local()` carried
424 /// during construction.
425 pub fn set_boot_authority(&mut self, authority: crate::services::authority::Authority) {
426 self.authority = authority;
427 // Propagate the new authority to every window's resources so
428 // window-side filesystem/path-translation reads (`Window::authority()`)
429 // see the swap. `Authority` carries internal `Arc`s, so this just
430 // clones cheap handles.
431 let auth = self.authority.clone();
432 for w in self.windows.values_mut() {
433 w.resources.authority = auth.clone();
434 }
435 // Propagate the authority's long-running spawner into the LSP
436 // manager so `force_spawn` can route server processes through
437 // the right backend. The editor rebuilds on every authority
438 // transition (AUTHORITY_DESIGN.md principle 7), so this is the
439 // single wiring point — no need for a hot-swap API. Path
440 // translation rides along for the same reason — LSP URIs need
441 // to be host↔container-translated under the new authority.
442 let __active_id = self.active_window;
443 if let Some(lsp) = self
444 .windows
445 .get_mut(&__active_id)
446 .and_then(|w| w.lsp.as_mut())
447 {
448 lsp.set_long_running_spawner(self.authority.long_running_spawner.clone());
449 lsp.set_path_translation(self.authority.path_translation.clone());
450 }
451 #[cfg(feature = "plugins")]
452 {
453 self.update_plugin_state_snapshot();
454 // Notify plugins so they can re-register state-gated
455 // commands (e.g. devcontainer `Attach` only when not
456 // attached). Production transitions also trigger a full
457 // editor restart that re-runs plugin init, but firing
458 // here keeps in-process transitions and the test harness
459 // (which simulates the restart inline) consistent.
460 let label = self.authority.display_label.clone();
461 self.plugin_manager.read().unwrap().run_hook(
462 "authority_changed",
463 crate::services::plugins::hooks::HookArgs::AuthorityChanged { label },
464 );
465 }
466 }
467
468 /// Read-only access to the active authority.
469 pub fn authority(&self) -> &crate::services::authority::Authority {
470 &self.authority
471 }
472
473 /// The editor's current working directory. This is the project
474 /// root; individual buffers may live elsewhere.
475 pub fn working_dir(&self) -> &std::path::Path {
476 &self.working_dir
477 }
478
479 /// The currently active `Session`. Always `WindowId(1)` until
480 /// the multi-session migration step lands; until then this is
481 /// effectively a typed wrapper around `working_dir`. New code
482 /// should prefer this accessor so the eventual migration is a
483 /// no-op for the call site.
484 ///
485 /// Panics if the active session id is not present in the
486 /// `sessions` map. That invariant is upheld by the constructor
487 /// and `setActiveWindow` (when added) — if the panic ever fires
488 /// it indicates a bug in session lifecycle code.
489 pub fn active_window(&self) -> &crate::app::window::Window {
490 self.windows
491 .get(&self.active_window)
492 .expect("active_window id must be a member of sessions")
493 }
494
495 /// The active session's id.
496 pub fn active_session_id(&self) -> fresh_core::WindowId {
497 self.active_window
498 }
499
500 /// Allocate the next globally-unique `BufferId`. Use this in
501 /// `impl Editor` handler bodies that mint new buffer ids. Handlers
502 /// that have already moved to `impl Window` use
503 /// `Window::alloc_buffer_id` (which delegates to the same
504 /// `Arc<BufferIdAllocator>` shared via `WindowResources`).
505 ///
506 /// Keeps `next_buffer_id` in sync with the allocator's high-water
507 /// mark so workspace snapshots that read the `next_buffer_id`
508 /// counter directly continue to see a correct value. The
509 /// allocator's atomic is the source of truth; this counter mirrors
510 /// it for serialization compatibility.
511 pub(crate) fn alloc_buffer_id(&mut self) -> fresh_core::BufferId {
512 let id = self.buffer_id_alloc.next();
513 // Bump the legacy counter past the freshly-issued id so
514 // workspace serialization snapshots see a value at least one
515 // greater than every issued id.
516 if id.0 + 1 > self.next_buffer_id {
517 self.next_buffer_id = id.0 + 1;
518 }
519 id
520 }
521
522 /// Number of sessions currently in the editor. Always 1 until
523 /// the multi-session step lands.
524 pub fn session_count(&self) -> usize {
525 self.windows.len()
526 }
527
528 /// Look up a session by id. Returns `None` if `id` is not in
529 /// the sessions map. Useful for tests; production code that
530 /// needs the active session should use `active_window()`.
531 pub fn session(&self, id: fresh_core::WindowId) -> Option<&crate::app::window::Window> {
532 self.windows.get(&id)
533 }
534
535 /// Active session's utility-dock panel-id → buffer-id map.
536 /// Used by tests to assert that the active window's dock
537 /// occupancy is what was set on it. (Pre-0b this asserted
538 /// "warm-swap restored the stash"; post-0b every window owns
539 /// its own dock, so the assertion is just "this window's
540 /// `panel_ids` map matches expectations.")
541 #[doc(hidden)]
542 pub fn panel_ids_for_test(&self) -> &std::collections::HashMap<String, fresh_core::BufferId> {
543 self.panel_ids()
544 }
545
546 /// Inject a panel_ids entry. Used by tests to populate the
547 /// active session's dock occupancy without going through the
548 /// async plugin command path.
549 #[doc(hidden)]
550 pub fn insert_panel_id_for_test(&mut self, key: String, buffer_id: fresh_core::BufferId) {
551 self.panel_ids_mut().insert(key, buffer_id);
552 }
553
554 /// True iff the active session has an LSP manager attached.
555 /// Used by tests to assert that the active window's `lsp`
556 /// slot is populated. (Pre-0b this exercised the warm-swap
557 /// code; post-0b the LSP manager lives directly on `Window`,
558 /// so the assertion is just "this window's `lsp` is `Some`.")
559 #[doc(hidden)]
560 pub fn has_lsp_for_test(&self) -> bool {
561 self.lsp().is_some()
562 }
563
564 /// Inject an LspManager so tests can prove the swap routes
565 /// it through the session stash without depending on real
566 /// LSP server spawn.
567 #[doc(hidden)]
568 pub fn install_dummy_lsp_for_test(&mut self) {
569 let active = self.active_window;
570 self.active_window_mut().lsp =
571 Some(crate::services::lsp::manager::LspManager::new(active, None));
572 }
573
574 /// Most-recent `path_changed` event the editor received.
575 /// Test-only — used by `watch_path` e2e tests to assert
576 /// kernel events surfaced to the editor.
577 #[doc(hidden)]
578 pub fn last_path_change_for_test(&self) -> Option<&(u64, std::path::PathBuf, &'static str)> {
579 self.last_path_change_for_test.as_ref()
580 }
581
582 /// Most-recent `WatchPathRegistered` plugin response, paired
583 /// with its request_id. Test-only.
584 #[doc(hidden)]
585 pub fn last_watch_response_for_test(&self) -> Option<&(u64, Result<u64, String>)> {
586 self.last_watch_response_for_test.as_ref()
587 }
588
589 /// Inject an mtime entry into the active session's mod-time
590 /// cache. Used by tests to populate `Window.file_mod_times`
591 /// without going through real file I/O. (Pre-0b this was
592 /// reaching the warm-swap stash; post-0b it's a direct
593 /// insert into the active window's cache.)
594 #[doc(hidden)]
595 pub fn insert_mtime_for_test(&mut self, path: std::path::PathBuf, t: std::time::SystemTime) {
596 self.file_mod_times_mut().insert(path, t);
597 }
598
599 /// Whether the active session's mtime cache contains `path`.
600 #[doc(hidden)]
601 pub fn has_mtime_for_test(&self, path: &std::path::Path) -> bool {
602 self.file_mod_times().contains_key(path)
603 }
604
605 /// Mutable access to the active session. Used by lifecycle code
606 /// that re-targets per-session state (renaming, etc.). Same
607 /// panic invariant as `active_window()`.
608 pub fn active_window_mut(&mut self) -> &mut crate::app::window::Window {
609 let id = self.active_window;
610 self.windows
611 .get_mut(&id)
612 .expect("active_window id must be a member of sessions")
613 }
614
615 /// The active window's layout-cache (split-leaf rects, tab rects,
616 /// file-explorer rect, view-line mappings). Mouse hit-testing and
617 /// visual-line motion read from here.
618 pub(crate) fn active_layout(&self) -> &crate::app::types::WindowLayoutCache {
619 &self.active_window().layout_cache
620 }
621
622 /// Mutable handle to the active window's layout cache. Renderer
623 /// writes split / tab / file-explorer hit-test rects here at the
624 /// end of each frame.
625 pub(crate) fn active_layout_mut(&mut self) -> &mut crate::app::types::WindowLayoutCache {
626 &mut self.active_window_mut().layout_cache
627 }
628
629 /// The active window's editor-chrome layout cache (status bar,
630 /// menu, popups, prompt overlay, full-frame cell-theme map).
631 /// Mouse hit-testing reads from here.
632 pub(crate) fn active_chrome(&self) -> &crate::app::types::ChromeLayout {
633 &self.active_window().chrome_layout
634 }
635
636 /// Mutable handle to the active window's chrome-layout cache.
637 /// Renderer writes status-bar / menu / popup / prompt-overlay
638 /// hit-test rects here at the end of each frame.
639 pub(crate) fn active_chrome_mut(&mut self) -> &mut crate::app::types::ChromeLayout {
640 &mut self.active_window_mut().chrome_layout
641 }
642
643 /// Active window's utility-dock panel-id → buffer-id map.
644 /// Each window owns its own dock; switching windows shows a
645 /// different (possibly empty) dock.
646 pub(crate) fn panel_ids(&self) -> &std::collections::HashMap<String, BufferId> {
647 &self.active_window().panel_ids
648 }
649
650 /// Mutable handle to the active window's panel-id map.
651 pub(crate) fn panel_ids_mut(&mut self) -> &mut std::collections::HashMap<String, BufferId> {
652 &mut self.active_window_mut().panel_ids
653 }
654
655 /// Active window's open-file mtime cache. Auto-revert only
656 /// fires for files in the active window — dormant windows
657 /// keep their mtime snapshot until the next dive.
658 pub(crate) fn file_mod_times(
659 &self,
660 ) -> &std::collections::HashMap<std::path::PathBuf, std::time::SystemTime> {
661 &self.active_window().file_mod_times
662 }
663
664 /// Mutable handle to the active window's mtime cache.
665 pub(crate) fn file_mod_times_mut(
666 &mut self,
667 ) -> &mut std::collections::HashMap<std::path::PathBuf, std::time::SystemTime> {
668 &mut self.active_window_mut().file_mod_times
669 }
670
671 /// Active window's file-explorer view (`None` if it's never been
672 /// opened in this window). Each window has its own tree;
673 /// switching windows shows that window's view (or none).
674 pub fn file_explorer(&self) -> Option<&FileTreeView> {
675 self.active_window().file_explorer.as_ref()
676 }
677
678 /// Mutable handle to the active window's file-explorer view.
679 /// Holds `&mut self` for the call's lifetime — for sites that
680 /// also need to read other Editor fields, use direct
681 /// `self.windows.get_mut(&self.active_window).and_then(|w| w.file_explorer.as_mut())`
682 /// instead so the borrow on `self.windows` stays disjoint.
683 pub fn file_explorer_mut(&mut self) -> Option<&mut FileTreeView> {
684 self.active_window_mut().file_explorer.as_mut()
685 }
686
687 /// Active window's buffer storage. Each window owns its
688 /// `EditorState` map outright; closing the window drops them.
689 /// Cross-window iteration goes through `self.windows.values()`
690 /// directly.
691 pub(crate) fn buffers(&self) -> &crate::app::window::WindowBuffers {
692 &self.active_window().buffers
693 }
694
695 /// Mutable handle to the active window's buffer storage.
696 /// Holds `&mut self` for the call's lifetime — at sites that
697 /// need a concurrent mutable borrow on another Window field
698 /// (`splits`, `event_logs`, etc.) take a single
699 /// `let window = self.windows.get_mut(&self.active_window).unwrap()`
700 /// and split-access the disjoint sub-fields directly.
701 pub(crate) fn buffers_mut(&mut self) -> &mut crate::app::window::WindowBuffers {
702 &mut self.active_window_mut().buffers
703 }
704
705 /// Active window's LSP manager (`None` if no LSP has been spawned
706 /// for this window yet). Each window has its own LSP set rooted
707 /// at its project root.
708 pub(crate) fn lsp(&self) -> Option<&crate::services::lsp::manager::LspManager> {
709 self.active_window().lsp.as_ref()
710 }
711
712 /// Mutable handle to the active window's LSP manager. Same
713 /// borrow caveat as `file_explorer_mut()`: at sites that also
714 /// need to read other Editor fields, prefer direct
715 /// `self.windows.get_mut(&self.active_window).and_then(|w| w.lsp.as_mut())`.
716 pub(crate) fn lsp_mut(&mut self) -> Option<&mut crate::services::lsp::manager::LspManager> {
717 self.active_window_mut().lsp.as_mut()
718 }
719
720 /// Active window's split tree. Panics if the window has no
721 /// layout yet — the invariant is "the active window always has
722 /// `splits` populated", upheld by `set_active_window` (which
723 /// seeds the layout on first dive) and by editor init (which
724 /// hands the initial layout to the base window).
725 pub(crate) fn split_manager(&self) -> &crate::view::split::SplitManager {
726 &self
727 .active_window()
728 .buffers
729 .splits()
730 .expect("active window must have a populated split layout")
731 .0
732 }
733
734 /// Mutable handle to the active window's split tree.
735 pub(crate) fn split_manager_mut(&mut self) -> &mut crate::view::split::SplitManager {
736 &mut self
737 .active_window_mut()
738 .buffers
739 .splits_mut()
740 .expect("active window must have a populated split layout")
741 .0
742 }
743
744 /// Active window's per-leaf view state map.
745 #[cfg(test)]
746 pub(crate) fn split_view_states(
747 &self,
748 ) -> &std::collections::HashMap<crate::model::event::LeafId, crate::view::split::SplitViewState>
749 {
750 &self
751 .active_window()
752 .buffers
753 .splits()
754 .expect("active window must have a populated split layout")
755 .1
756 }
757
758 /// Mutable handle to the active window's per-leaf view state map.
759 pub(crate) fn split_view_states_mut(
760 &mut self,
761 ) -> &mut std::collections::HashMap<
762 crate::model::event::LeafId,
763 crate::view::split::SplitViewState,
764 > {
765 &mut self
766 .active_window_mut()
767 .buffers
768 .splits_mut()
769 .expect("active window must have a populated split layout")
770 .1
771 }
772
773 /// Return buffer ids whose on-disk path sits at or under `root`.
774 /// Used by file-explorer operations that need to react when a file
775 /// or directory on disk goes away or moves.
776 pub fn buffer_ids_under_path(&self, root: &std::path::Path) -> Vec<BufferId> {
777 self.windows
778 .get(&self.active_window)
779 .map(|w| &w.buffers)
780 .expect("active window present")
781 .iter()
782 .filter_map(|(id, state)| {
783 let p = state.buffer.file_path()?;
784 if p == root || p.starts_with(root) {
785 Some(*id)
786 } else {
787 None
788 }
789 })
790 .collect()
791 }
792
793 /// Get remote connection info if editing remote files
794 ///
795 /// Returns `Some("user@host")` for remote editing, `None` for local.
796 pub fn remote_connection_info(&self) -> Option<&str> {
797 self.authority.filesystem.remote_connection_info()
798 }
799
800 /// Get connection string for display in status bar and file explorer.
801 ///
802 /// Per principle 9, identity lives in the authority. The label set
803 /// by whoever constructed the authority wins; if it is empty (the
804 /// SSH constructor leaves it that way) we fall back to the
805 /// filesystem's `remote_connection_info()`, which knows how to
806 /// annotate disconnected SSH sessions.
807 pub fn connection_display_string(&self) -> Option<String> {
808 if !self.authority.display_label.is_empty() {
809 return Some(self.authority.display_label.clone());
810 }
811 self.remote_connection_info().map(|conn| {
812 if self.authority.filesystem.is_remote_connected() {
813 conn.to_string()
814 } else {
815 format!("{} (Disconnected)", conn)
816 }
817 })
818 }
819
820 /// Get the status log path
821 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
822 self.status_log_path.as_ref()
823 }
824
825 /// Open the status log file (user clicked on status message)
826 pub fn open_status_log(&mut self) {
827 if let Some(path) = self.status_log_path.clone() {
828 // Use open_local_file since log files are always local
829 match self.active_window_mut().open_local_file(&path) {
830 Ok(buffer_id) => {
831 self.active_window_mut()
832 .mark_buffer_read_only(buffer_id, true);
833 }
834 Err(e) => {
835 tracing::error!("Failed to open status log: {}", e);
836 }
837 }
838 } else {
839 self.set_status_message("Status log not available".to_string());
840 }
841 }
842
843 /// Check for and handle any new warnings in the warning log
844 ///
845 /// Updates the general warning domain for the status bar.
846 /// Returns true if new warnings were found.
847 pub fn check_warning_log(&mut self) -> bool {
848 let path = match &self.warning_log {
849 Some((receiver, path)) => {
850 let mut new_warning_count = 0usize;
851 while receiver.try_recv().is_ok() {
852 new_warning_count += 1;
853 }
854 if new_warning_count == 0 {
855 return false;
856 }
857 (path.clone(), new_warning_count)
858 }
859 None => return false,
860 };
861 let (path, new_warning_count) = path;
862 self.active_window_mut()
863 .warning_domains
864 .general
865 .add_warnings(new_warning_count);
866 self.active_window_mut()
867 .warning_domains
868 .general
869 .set_log_path(path);
870
871 true
872 }
873
874 /// Get the warning domain registry
875 // Warning-domain accessors live on `impl Window`:
876 // - `clear_warnings` — call as `self.active_window_mut().clear_warnings()`.
877 // - Read access via `active_window().warning_domains` directly
878 // (and its `.general` / `.lsp` sub-registries).
879 // `has_lsp_error`, `get_effective_warning_level`,
880 // `get_general_warning_level`, `get_general_warning_count`,
881 // `get_warning_domains`, `get_warning_log_path`,
882 // `clear_warning_indicator` were thin getters with no remaining
883 // callers and have been removed.
884
885 /// Open the warning log file (user-initiated action). Stays on
886 /// `impl Editor` because it calls editor-orchestration helpers
887 /// (`open_local_file`, `mark_buffer_read_only`).
888 pub fn open_warning_log(&mut self) {
889 if let Some(path) = self
890 .active_window_mut()
891 .warning_domains
892 .general
893 .log_path
894 .clone()
895 {
896 // Use open_local_file since log files are always local
897 match self.active_window_mut().open_local_file(&path) {
898 Ok(buffer_id) => {
899 self.active_window_mut()
900 .mark_buffer_read_only(buffer_id, true);
901 }
902 Err(e) => {
903 tracing::error!("Failed to open warning log: {}", e);
904 }
905 }
906 }
907 }
908
909 // `update_lsp_warning_domain` lives on `impl Window` — call it via
910 // `self.active_window_mut().update_lsp_warning_domain()`.
911
912 /// Check if mouse hover timer has expired and trigger LSP hover request
913 ///
914 /// This implements debounced hover - we wait for the configured delay before
915 /// sending the request to avoid spamming the LSP server on every mouse move.
916 /// Returns true if a hover request was triggered.
917 pub fn check_mouse_hover_timer(&mut self) -> bool {
918 // Check if mouse hover is enabled
919 if !self.config.editor.mouse_hover_enabled {
920 return false;
921 }
922
923 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
924
925 // Get hover state without borrowing self
926 let hover_info = match self.active_window_mut().mouse_state.lsp_hover_state {
927 Some((byte_pos, start_time, screen_x, screen_y)) => {
928 if self.active_window_mut().mouse_state.lsp_hover_request_sent {
929 return false; // Already sent request for this position
930 }
931 if start_time.elapsed() < hover_delay {
932 return false; // Timer hasn't expired yet
933 }
934 Some((byte_pos, screen_x, screen_y))
935 }
936 None => return false,
937 };
938
939 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
940 return false;
941 };
942
943 // Store mouse position for popup positioning
944 self.active_window_mut()
945 .hover
946 .set_screen_position((screen_x, screen_y));
947
948 // Request hover at the byte position — only mark as sent if dispatched
949 match self.request_hover_at_position(byte_pos) {
950 Ok(true) => {
951 self.active_window_mut().mouse_state.lsp_hover_request_sent = true;
952 true
953 }
954 Ok(false) => false, // no server ready, timer will retry
955 Err(e) => {
956 tracing::debug!("Failed to request hover: {}", e);
957 false
958 }
959 }
960 }
961
962 // `check_semantic_highlight_timer` lives on `impl Window` — call it
963 // via `self.active_window().check_semantic_highlight_timer()`.
964
965 // `check_diagnostic_pull_timer` lives on `impl Window` — call it via
966 // `self.active_window_mut().check_diagnostic_pull_timer()`. Pulls
967 // run against the active window's LSP manager and its per-window
968 // `scheduled_diagnostic_pull` debounce slot.
969
970 /// Check if completion trigger timer has expired and trigger completion if so
971 ///
972 /// This implements debounced completion - we wait for quick_suggestions_delay_ms
973 /// before sending the completion request to avoid spamming the LSP server.
974 /// Returns true if a completion request was triggered.
975 pub fn check_completion_trigger_timer(&mut self) -> bool {
976 // Check if we have a scheduled completion trigger
977 let Some(trigger_time) = self.active_window_mut().scheduled_completion_trigger else {
978 return false;
979 };
980
981 // Check if the timer has expired
982 if Instant::now() < trigger_time {
983 return false;
984 }
985
986 // Clear the scheduled trigger
987 self.active_window_mut().scheduled_completion_trigger = None;
988
989 // Don't trigger if a popup is already visible
990 if self.active_state().popups.is_visible() {
991 return false;
992 }
993
994 // Trigger the completion request
995 self.request_completion();
996
997 true
998 }
999}