Skip to main content

fresh/app/
editor_accessors.rs

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