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 pub fn config_mut(&mut self) -> &mut Config {
36 Arc::make_mut(&mut self.config)
37 }
38
39 /// Replace the config wholesale. Used by the "reload config" path and
40 /// by tests that want to swap in a freshly-parsed file. Constructs a
41 /// fresh `Arc`, so any snapshot that still holds the old value sees
42 /// the pointer move and will reserialize on the next refresh.
43 pub fn set_config(&mut self, new_config: Config) {
44 self.config = Arc::new(new_config);
45 }
46
47 /// Replace the cached raw user config. Like `set_config`, constructs
48 /// a fresh `Arc` so the plugin snapshot notices the change.
49 pub(crate) fn set_user_config_raw(&mut self, value: serde_json::Value) {
50 self.user_config_raw = Arc::new(value);
51 }
52
53 /// Mutable access to the merged diagnostics map. Routes through
54 /// `Arc::make_mut`, which CoW-clones while the plugin snapshot still
55 /// holds the old map — readers never observe an in-place mutation.
56 pub(crate) fn stored_diagnostics_mut(
57 &mut self,
58 ) -> &mut HashMap<String, Vec<lsp_types::Diagnostic>> {
59 Arc::make_mut(&mut self.stored_diagnostics)
60 }
61
62 /// Mutable access to the folding-ranges map. CoW-clones through
63 /// `Arc::make_mut` for the same reason as `stored_diagnostics_mut`.
64 pub(crate) fn stored_folding_ranges_mut(
65 &mut self,
66 ) -> &mut HashMap<String, Vec<lsp_types::FoldingRange>> {
67 Arc::make_mut(&mut self.stored_folding_ranges)
68 }
69
70 /// Get a reference to the key translator (for input calibration)
71 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
72 &self.key_translator
73 }
74
75 /// Get a reference to the time source
76 pub fn time_source(&self) -> &SharedTimeSource {
77 &self.time_source
78 }
79
80 /// Emit a control event
81 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
82 self.event_broadcaster.emit_named(name, data);
83 }
84
85 /// Send a response to a plugin for an async operation
86 pub(super) fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
87 self.plugin_manager.deliver_response(response);
88 }
89
90 /// Remove a pending semantic token request from tracking maps.
91 pub(super) fn take_pending_semantic_token_request(
92 &mut self,
93 request_id: u64,
94 ) -> Option<SemanticTokenFullRequest> {
95 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
96 self.semantic_tokens_in_flight.remove(&request.buffer_id);
97 Some(request)
98 } else {
99 None
100 }
101 }
102
103 /// Remove a pending semantic token range request from tracking maps.
104 pub(super) fn take_pending_semantic_token_range_request(
105 &mut self,
106 request_id: u64,
107 ) -> Option<SemanticTokenRangeRequest> {
108 if let Some(request) = self
109 .pending_semantic_token_range_requests
110 .remove(&request_id)
111 {
112 self.semantic_tokens_range_in_flight
113 .remove(&request.buffer_id);
114 Some(request)
115 } else {
116 None
117 }
118 }
119
120 /// Get all keybindings as (key, action) pairs
121 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
122 self.keybindings.read().unwrap().get_all_bindings()
123 }
124
125 /// Get the formatted keybinding for a specific action (for display in messages)
126 /// Returns None if no keybinding is found for the action
127 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
128 self.keybindings
129 .read()
130 .unwrap()
131 .find_keybinding_for_action(action_name, self.key_context.clone())
132 }
133
134 /// Get mutable access to the mode registry
135 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
136 &mut self.mode_registry
137 }
138
139 /// Get immutable access to the mode registry
140 pub fn mode_registry(&self) -> &ModeRegistry {
141 &self.mode_registry
142 }
143
144 /// Get the currently active buffer ID.
145 ///
146 /// This is derived from the split manager (single source of truth).
147 /// The editor always has at least one buffer, so this never fails.
148 ///
149 /// When the active split has a buffer-group tab as its active target
150 /// (i.e., `active_group_tab.is_some()`), this returns the buffer of the
151 /// currently-focused inner panel — so that input routing, command palette
152 /// context, buffer mode, and other "what is the user looking at" queries
153 /// resolve to the panel the user is actually interacting with rather than
154 /// the split's background leaf buffer.
155 ///
156 /// The override only takes effect if the inner panel's buffer is still
157 /// live in `self.buffers`; otherwise it falls back to the main split's
158 /// leaf buffer so callers never see a stale/freed buffer id.
159 #[inline]
160 pub fn active_buffer(&self) -> BufferId {
161 let (_, buf) = self.effective_active_pair();
162 buf
163 }
164
165 /// The split id whose `SplitViewState` owns the currently-focused
166 /// cursors/viewport/buffer state. For a regular split this is just
167 /// `split_manager.active_split()`. For a split that has a group tab
168 /// active, this returns the focused inner panel's leaf id (which
169 /// lives in `split_view_states` even though it's not in the main
170 /// split tree).
171 #[inline]
172 pub fn effective_active_split(&self) -> crate::model::event::LeafId {
173 let (split, _) = self.effective_active_pair();
174 split
175 }
176
177 /// Resolve the effective (split, buffer) pair for the currently-focused
178 /// target. This is the single source of truth — both `active_buffer` and
179 /// `effective_active_split` derive from it so they can never disagree.
180 ///
181 /// Returned invariant: `split_view_states[split]` exists, its
182 /// `active_buffer` equals the returned buffer id, `self.buffers`
183 /// contains the returned buffer id, and `split.keyed_states` contains
184 /// an entry for the returned buffer id. Consequently the mutation path
185 /// in `apply_event_to_active_buffer` (which indexes into
186 /// `keyed_states[buffer]`) is always well-defined for the returned pair.
187 ///
188 /// If a buffer-group panel is focused but any of the invariants above
189 /// is not satisfied for the inner leaf (for example because the panel
190 /// buffer was freed without clearing `focused_group_leaf`), the helper
191 /// falls back to the outer split's own leaf. The fallback is also
192 /// validated before being returned.
193 #[inline]
194 fn effective_active_pair(&self) -> (crate::model::event::LeafId, BufferId) {
195 let active_split = self.split_manager.active_split();
196 if let Some(vs) = self.split_view_states.get(&active_split) {
197 if vs.active_group_tab.is_some() {
198 if let Some(inner_leaf) = vs.focused_group_leaf {
199 if let Some(inner_vs) = self.split_view_states.get(&inner_leaf) {
200 let inner_buf = inner_vs.active_buffer;
201 if self.buffers.contains_key(&inner_buf)
202 && inner_vs.keyed_states.contains_key(&inner_buf)
203 {
204 return (inner_leaf, inner_buf);
205 }
206 }
207 }
208 }
209 }
210 let outer_buf = self
211 .split_manager
212 .active_buffer_id()
213 .expect("Editor always has at least one buffer");
214 (active_split, outer_buf)
215 }
216
217 /// Get the mode name for the active buffer (if it's a virtual buffer)
218 pub fn active_buffer_mode(&self) -> Option<&str> {
219 self.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.buffer_metadata.get(&self.active_buffer()) {
227 if metadata.read_only {
228 return true;
229 }
230 // Also check if the mode is read-only
231 if let Some(mode_name) = metadata.virtual_mode() {
232 return self.mode_registry.is_read_only(mode_name);
233 }
234 }
235 false
236 }
237
238 /// Check if editing should be disabled for the active buffer
239 /// This returns true when editing_disabled is true (e.g., for read-only virtual buffers)
240 pub fn is_editing_disabled(&self) -> bool {
241 self.active_state().editing_disabled
242 }
243
244 /// Mark a buffer as read-only, setting both metadata and editor state consistently.
245 /// This is the single entry point for making a buffer read-only.
246 pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
247 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
248 metadata.read_only = read_only;
249 }
250 if let Some(state) = self.buffers.get_mut(&buffer_id) {
251 state.editing_disabled = read_only;
252 }
253 }
254
255 /// Get the effective mode for the active buffer.
256 ///
257 /// Buffer-local mode (virtual buffers) takes precedence over the global
258 /// editor mode, so that e.g. a search-replace panel isn't hijacked by
259 /// a markdown-source or vi-mode global mode.
260 pub fn effective_mode(&self) -> Option<&str> {
261 self.active_buffer_mode().or(self.editor_mode.as_deref())
262 }
263
264 /// Check if LSP has any active progress tasks (e.g., indexing)
265 pub fn has_active_lsp_progress(&self) -> bool {
266 !self.lsp_progress.is_empty()
267 }
268
269 /// Get the current LSP progress info (if any)
270 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
271 self.lsp_progress
272 .iter()
273 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
274 .collect()
275 }
276
277 /// Check if any LSP server for a given language is running (ready)
278 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
279 use crate::services::async_bridge::LspServerStatus;
280 self.lsp_server_statuses
281 .iter()
282 .any(|((lang, server_name), status)| {
283 if !matches!(status, LspServerStatus::Running) {
284 return false;
285 }
286 if lang == language {
287 return true;
288 }
289 // Check if this server's scope accepts the queried language
290 self.lsp
291 .as_ref()
292 .and_then(|lsp| lsp.server_scope(server_name))
293 .map(|scope| scope.accepts(language))
294 .unwrap_or(false)
295 })
296 }
297
298 /// Get stored LSP diagnostics (for testing and external access)
299 /// Returns a reference to the diagnostics map keyed by file URI
300 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
301 &self.stored_diagnostics
302 }
303
304 /// Check if an update is available
305 pub fn is_update_available(&self) -> bool {
306 self.update_checker
307 .as_ref()
308 .map(|c| c.is_update_available())
309 .unwrap_or(false)
310 }
311
312 /// Get the latest version string if an update is available
313 pub fn latest_version(&self) -> Option<&str> {
314 self.update_checker
315 .as_ref()
316 .and_then(|c| c.latest_version())
317 }
318
319 /// Get the cached release check result (for shutdown notification)
320 pub fn get_update_result(
321 &self,
322 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
323 self.update_checker
324 .as_ref()
325 .and_then(|c| c.get_cached_result())
326 }
327
328 /// Set a custom update checker (for testing)
329 ///
330 /// This allows injecting a custom PeriodicUpdateChecker that points to a mock server,
331 /// enabling E2E tests for the update notification UI.
332 #[doc(hidden)]
333 pub fn set_update_checker(
334 &mut self,
335 checker: crate::services::release_checker::PeriodicUpdateChecker,
336 ) {
337 self.update_checker = Some(checker);
338 }
339
340 /// Configure LSP server for a specific language
341 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
342 if let Some(ref mut lsp) = self.lsp {
343 lsp.set_language_configs(language, config);
344 }
345 }
346
347 /// Get a list of currently running LSP server languages
348 pub fn running_lsp_servers(&self) -> Vec<String> {
349 self.lsp
350 .as_ref()
351 .map(|lsp| lsp.running_servers())
352 .unwrap_or_default()
353 }
354
355 /// Return the number of pending completion requests.
356 pub fn pending_completion_requests_count(&self) -> usize {
357 self.pending_completion_requests.len()
358 }
359
360 /// Return the number of stored completion items.
361 pub fn completion_items_count(&self) -> usize {
362 self.completion_items.as_ref().map_or(0, |v| v.len())
363 }
364
365 /// Return the number of initialized LSP servers for a given language.
366 pub fn initialized_lsp_server_count(&self, language: &str) -> usize {
367 self.lsp
368 .as_ref()
369 .map(|lsp| {
370 lsp.get_handles(language)
371 .iter()
372 .filter(|sh| sh.capabilities.initialized)
373 .count()
374 })
375 .unwrap_or(0)
376 }
377
378 /// Shutdown an LSP server by language (marks it as disabled until manual restart)
379 ///
380 /// Returns true if the server was found and shutdown, false otherwise
381 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
382 if let Some(ref mut lsp) = self.lsp {
383 lsp.shutdown_server(language)
384 } else {
385 false
386 }
387 }
388
389 /// Enable event log streaming to a file
390 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
391 // Enable streaming for all existing event logs
392 for event_log in self.event_logs.values_mut() {
393 event_log.enable_streaming(&path)?;
394 }
395 Ok(())
396 }
397
398 /// Log keystroke for debugging
399 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
400 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
401 event_log.log_keystroke(key_code, modifiers);
402 }
403 }
404
405 /// Set up warning log monitoring
406 ///
407 /// When warnings/errors are logged, they will be written to the specified path
408 /// and the editor will be notified via the receiver.
409 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
410 self.warning_log = Some((receiver, path));
411 }
412
413 /// Set the status message log path
414 pub fn set_status_log_path(&mut self, path: PathBuf) {
415 self.status_log_path = Some(path);
416 }
417
418 /// Set the process spawner for plugin command execution
419 /// Use RemoteProcessSpawner for remote editing, LocalProcessSpawner for local
420 pub fn set_process_spawner(
421 &mut self,
422 spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
423 ) {
424 self.process_spawner = spawner;
425 }
426
427 /// Get remote connection info if editing remote files
428 ///
429 /// Returns `Some("user@host")` for remote editing, `None` for local.
430 pub fn remote_connection_info(&self) -> Option<&str> {
431 self.filesystem.remote_connection_info()
432 }
433
434 /// Get the status log path
435 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
436 self.status_log_path.as_ref()
437 }
438
439 /// Open the status log file (user clicked on status message)
440 pub fn open_status_log(&mut self) {
441 if let Some(path) = self.status_log_path.clone() {
442 // Use open_local_file since log files are always local
443 match self.open_local_file(&path) {
444 Ok(buffer_id) => {
445 self.mark_buffer_read_only(buffer_id, true);
446 }
447 Err(e) => {
448 tracing::error!("Failed to open status log: {}", e);
449 }
450 }
451 } else {
452 self.set_status_message("Status log not available".to_string());
453 }
454 }
455
456 /// Check for and handle any new warnings in the warning log
457 ///
458 /// Updates the general warning domain for the status bar.
459 /// Returns true if new warnings were found.
460 pub fn check_warning_log(&mut self) -> bool {
461 let Some((receiver, path)) = &self.warning_log else {
462 return false;
463 };
464
465 // Non-blocking check for any warnings
466 let mut new_warning_count = 0usize;
467 while receiver.try_recv().is_ok() {
468 new_warning_count += 1;
469 }
470
471 if new_warning_count > 0 {
472 // Update general warning domain (don't auto-open file)
473 self.warning_domains.general.add_warnings(new_warning_count);
474 self.warning_domains.general.set_log_path(path.clone());
475 }
476
477 new_warning_count > 0
478 }
479
480 /// Get the warning domain registry
481 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
482 &self.warning_domains
483 }
484
485 /// Get the warning log path (for opening when user clicks indicator)
486 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
487 self.warning_domains.general.log_path.as_ref()
488 }
489
490 /// Open the warning log file (user-initiated action)
491 pub fn open_warning_log(&mut self) {
492 if let Some(path) = self.warning_domains.general.log_path.clone() {
493 // Use open_local_file since log files are always local
494 match self.open_local_file(&path) {
495 Ok(buffer_id) => {
496 self.mark_buffer_read_only(buffer_id, true);
497 }
498 Err(e) => {
499 tracing::error!("Failed to open warning log: {}", e);
500 }
501 }
502 }
503 }
504
505 /// Clear the general warning indicator (user dismissed)
506 pub fn clear_warning_indicator(&mut self) {
507 self.warning_domains.general.clear();
508 }
509
510 /// Clear all warning indicators (user dismissed via command)
511 pub fn clear_warnings(&mut self) {
512 self.warning_domains.general.clear();
513 self.warning_domains.lsp.clear();
514 self.status_message = Some("Warnings cleared".to_string());
515 }
516
517 /// Check if any LSP server is in error state
518 pub fn has_lsp_error(&self) -> bool {
519 self.warning_domains.lsp.level() == WarningLevel::Error
520 }
521
522 /// Get the effective warning level for the status bar (LSP indicator)
523 /// Returns Error if LSP has errors, Warning if there are warnings, None otherwise
524 pub fn get_effective_warning_level(&self) -> WarningLevel {
525 self.warning_domains.lsp.level()
526 }
527
528 /// Get the general warning level (for the general warning badge)
529 pub fn get_general_warning_level(&self) -> WarningLevel {
530 self.warning_domains.general.level()
531 }
532
533 /// Get the general warning count
534 pub fn get_general_warning_count(&self) -> usize {
535 self.warning_domains.general.count
536 }
537
538 /// Update LSP warning domain from server statuses
539 pub fn update_lsp_warning_domain(&mut self) {
540 self.warning_domains
541 .lsp
542 .update_from_statuses(&self.lsp_server_statuses);
543 }
544
545 /// Check if mouse hover timer has expired and trigger LSP hover request
546 ///
547 /// This implements debounced hover - we wait for the configured delay before
548 /// sending the request to avoid spamming the LSP server on every mouse move.
549 /// Returns true if a hover request was triggered.
550 pub fn check_mouse_hover_timer(&mut self) -> bool {
551 // Check if mouse hover is enabled
552 if !self.config.editor.mouse_hover_enabled {
553 return false;
554 }
555
556 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
557
558 // Get hover state without borrowing self
559 let hover_info = match self.mouse_state.lsp_hover_state {
560 Some((byte_pos, start_time, screen_x, screen_y)) => {
561 if self.mouse_state.lsp_hover_request_sent {
562 return false; // Already sent request for this position
563 }
564 if start_time.elapsed() < hover_delay {
565 return false; // Timer hasn't expired yet
566 }
567 Some((byte_pos, screen_x, screen_y))
568 }
569 None => return false,
570 };
571
572 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
573 return false;
574 };
575
576 // Store mouse position for popup positioning
577 self.hover.set_screen_position((screen_x, screen_y));
578
579 // Request hover at the byte position — only mark as sent if dispatched
580 match self.request_hover_at_position(byte_pos) {
581 Ok(true) => {
582 self.mouse_state.lsp_hover_request_sent = true;
583 true
584 }
585 Ok(false) => false, // no server ready, timer will retry
586 Err(e) => {
587 tracing::debug!("Failed to request hover: {}", e);
588 false
589 }
590 }
591 }
592
593 /// Check if semantic highlight debounce timer has expired
594 ///
595 /// Returns true if a redraw is needed because the debounce period has elapsed
596 /// and semantic highlights need to be recomputed.
597 pub fn check_semantic_highlight_timer(&self) -> bool {
598 // Check all buffers for pending semantic highlight redraws
599 for state in self.buffers.values() {
600 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
601 if remaining.is_zero() {
602 return true;
603 }
604 }
605 }
606 false
607 }
608
609 /// Check if diagnostic pull timer has expired and trigger re-pull if so.
610 ///
611 /// Debounced diagnostic re-pull after document changes — waits 500ms after
612 /// the last edit before requesting fresh diagnostics from the LSP server.
613 pub fn check_diagnostic_pull_timer(&mut self) -> bool {
614 let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
615 return false;
616 };
617
618 if Instant::now() < trigger_time {
619 return false;
620 }
621
622 self.scheduled_diagnostic_pull = None;
623
624 // Get URI and language for this buffer
625 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
626 return false;
627 };
628 let Some(uri) = metadata.file_uri().cloned() else {
629 return false;
630 };
631 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
632 return false;
633 };
634
635 let Some(lsp) = self.lsp.as_mut() else {
636 return false;
637 };
638 let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
639 else {
640 return false;
641 };
642 let client = &mut sh.handle;
643
644 let request_id = self.next_lsp_request_id;
645 self.next_lsp_request_id += 1;
646 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
647 if let Err(e) = client.document_diagnostic(request_id, uri.clone(), previous_result_id) {
648 tracing::debug!(
649 "Failed to pull diagnostics after edit for {}: {}",
650 uri.as_str(),
651 e
652 );
653 } else {
654 tracing::debug!(
655 "Pulling diagnostics after edit for {} (request_id={})",
656 uri.as_str(),
657 request_id
658 );
659 }
660
661 false // no immediate redraw needed; diagnostics arrive asynchronously
662 }
663
664 /// Check if completion trigger timer has expired and trigger completion if so
665 ///
666 /// This implements debounced completion - we wait for quick_suggestions_delay_ms
667 /// before sending the completion request to avoid spamming the LSP server.
668 /// Returns true if a completion request was triggered.
669 pub fn check_completion_trigger_timer(&mut self) -> bool {
670 // Check if we have a scheduled completion trigger
671 let Some(trigger_time) = self.scheduled_completion_trigger else {
672 return false;
673 };
674
675 // Check if the timer has expired
676 if Instant::now() < trigger_time {
677 return false;
678 }
679
680 // Clear the scheduled trigger
681 self.scheduled_completion_trigger = None;
682
683 // Don't trigger if a popup is already visible
684 if self.active_state().popups.is_visible() {
685 return false;
686 }
687
688 // Trigger the completion request
689 self.request_completion();
690
691 true
692 }
693}