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 /// Raw-event counterpart: return the `(KeyCode, KeyModifiers)` currently
135 /// bound to `action` in `context`. Intended for callers that need to
136 /// simulate the user pressing the bound key (e2e tests, some hotkey-
137 /// chaining code) without hardcoding a default that a user's rebind
138 /// would invalidate.
139 pub fn keybinding_event_for_action(
140 &self,
141 action: &crate::input::keybindings::Action,
142 context: crate::input::keybindings::KeyContext,
143 ) -> Option<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)> {
144 self.keybindings
145 .read()
146 .unwrap()
147 .get_keybinding_event_for_action(action, context)
148 }
149
150 /// Get mutable access to the mode registry
151 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
152 &mut self.mode_registry
153 }
154
155 /// Get immutable access to the mode registry
156 pub fn mode_registry(&self) -> &ModeRegistry {
157 &self.mode_registry
158 }
159
160 /// Get the currently active buffer ID.
161 ///
162 /// This is derived from the split manager (single source of truth).
163 /// The editor always has at least one buffer, so this never fails.
164 ///
165 /// When the active split has a buffer-group tab as its active target
166 /// (i.e., `active_group_tab.is_some()`), this returns the buffer of the
167 /// currently-focused inner panel — so that input routing, command palette
168 /// context, buffer mode, and other "what is the user looking at" queries
169 /// resolve to the panel the user is actually interacting with rather than
170 /// the split's background leaf buffer.
171 ///
172 /// The override only takes effect if the inner panel's buffer is still
173 /// live in `self.buffers`; otherwise it falls back to the main split's
174 /// leaf buffer so callers never see a stale/freed buffer id.
175 #[inline]
176 pub fn active_buffer(&self) -> BufferId {
177 let (_, buf) = self.effective_active_pair();
178 buf
179 }
180
181 /// The split id whose `SplitViewState` owns the currently-focused
182 /// cursors/viewport/buffer state. For a regular split this is just
183 /// `split_manager.active_split()`. For a split that has a group tab
184 /// active, this returns the focused inner panel's leaf id (which
185 /// lives in `split_view_states` even though it's not in the main
186 /// split tree).
187 #[inline]
188 pub fn effective_active_split(&self) -> crate::model::event::LeafId {
189 let (split, _) = self.effective_active_pair();
190 split
191 }
192
193 /// Resolve the effective (split, buffer) pair for the currently-focused
194 /// target. This is the single source of truth — both `active_buffer` and
195 /// `effective_active_split` derive from it so they can never disagree.
196 ///
197 /// Returned invariant: `split_view_states[split]` exists, its
198 /// `active_buffer` equals the returned buffer id, `self.buffers`
199 /// contains the returned buffer id, and `split.keyed_states` contains
200 /// an entry for the returned buffer id. Consequently the mutation path
201 /// in `apply_event_to_active_buffer` (which indexes into
202 /// `keyed_states[buffer]`) is always well-defined for the returned pair.
203 ///
204 /// If a buffer-group panel is focused but any of the invariants above
205 /// is not satisfied for the inner leaf (for example because the panel
206 /// buffer was freed without clearing `focused_group_leaf`), the helper
207 /// falls back to the outer split's own leaf. The fallback is also
208 /// validated before being returned.
209 #[inline]
210 fn effective_active_pair(&self) -> (crate::model::event::LeafId, BufferId) {
211 let active_split = self.split_manager.active_split();
212 if let Some(vs) = self.split_view_states.get(&active_split) {
213 if vs.active_group_tab.is_some() {
214 if let Some(inner_leaf) = vs.focused_group_leaf {
215 if let Some(inner_vs) = self.split_view_states.get(&inner_leaf) {
216 let inner_buf = inner_vs.active_buffer;
217 if self.buffers.contains_key(&inner_buf)
218 && inner_vs.keyed_states.contains_key(&inner_buf)
219 {
220 return (inner_leaf, inner_buf);
221 }
222 }
223 }
224 }
225 }
226 let outer_buf = self
227 .split_manager
228 .active_buffer_id()
229 .expect("Editor always has at least one buffer");
230 (active_split, outer_buf)
231 }
232
233 /// Get the mode name for the active buffer (if it's a virtual buffer)
234 pub fn active_buffer_mode(&self) -> Option<&str> {
235 self.buffer_metadata
236 .get(&self.active_buffer())
237 .and_then(|meta| meta.virtual_mode())
238 }
239
240 /// Check if the active buffer is read-only
241 pub fn is_active_buffer_read_only(&self) -> bool {
242 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
243 if metadata.read_only {
244 return true;
245 }
246 // Also check if the mode is read-only
247 if let Some(mode_name) = metadata.virtual_mode() {
248 return self.mode_registry.is_read_only(mode_name);
249 }
250 }
251 false
252 }
253
254 /// Check if editing should be disabled for the active buffer
255 /// This returns true when editing_disabled is true (e.g., for read-only virtual buffers)
256 pub fn is_editing_disabled(&self) -> bool {
257 self.active_state().editing_disabled
258 }
259
260 /// Mark a buffer as read-only, setting both metadata and editor state consistently.
261 /// This is the single entry point for making a buffer read-only.
262 pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
263 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
264 metadata.read_only = read_only;
265 }
266 if let Some(state) = self.buffers.get_mut(&buffer_id) {
267 state.editing_disabled = read_only;
268 }
269 }
270
271 /// Get the effective mode for the active buffer.
272 ///
273 /// Buffer-local mode (virtual buffers) takes precedence over the global
274 /// editor mode, so that e.g. a search-replace panel isn't hijacked by
275 /// a markdown-source or vi-mode global mode.
276 pub fn effective_mode(&self) -> Option<&str> {
277 self.active_buffer_mode().or(self.editor_mode.as_deref())
278 }
279
280 /// Check if LSP has any active progress tasks (e.g., indexing)
281 pub fn has_active_lsp_progress(&self) -> bool {
282 !self.lsp_progress.is_empty()
283 }
284
285 /// Toggle the LSP auto-prompt popup on this editor instance.
286 ///
287 /// See `app::lsp_auto_prompt` for the full rationale. In short:
288 /// tests default this to `false` to stop the popup from
289 /// swallowing keystrokes in scenarios that don't exercise LSP;
290 /// tests that DO exercise it re-enable on the specific harness
291 /// they care about.
292 pub fn set_lsp_auto_prompt_enabled(&mut self, enabled: bool) {
293 self.lsp_auto_prompt_enabled = enabled;
294 }
295
296 /// Get the current LSP progress info (if any)
297 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
298 self.lsp_progress
299 .iter()
300 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
301 .collect()
302 }
303
304 /// Check if any LSP server for a given language is running (ready)
305 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
306 use crate::services::async_bridge::LspServerStatus;
307 self.lsp_server_statuses
308 .iter()
309 .any(|((lang, server_name), status)| {
310 if !matches!(status, LspServerStatus::Running) {
311 return false;
312 }
313 if lang == language {
314 return true;
315 }
316 // Check if this server's scope accepts the queried language
317 self.lsp
318 .as_ref()
319 .and_then(|lsp| lsp.server_scope(server_name))
320 .map(|scope| scope.accepts(language))
321 .unwrap_or(false)
322 })
323 }
324
325 /// Get stored LSP diagnostics (for testing and external access)
326 /// Returns a reference to the diagnostics map keyed by file URI
327 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
328 &self.stored_diagnostics
329 }
330
331 /// Check if an update is available
332 pub fn is_update_available(&self) -> bool {
333 self.update_checker
334 .as_ref()
335 .map(|c| c.is_update_available())
336 .unwrap_or(false)
337 }
338
339 /// Get the latest version string if an update is available
340 pub fn latest_version(&self) -> Option<&str> {
341 self.update_checker
342 .as_ref()
343 .and_then(|c| c.latest_version())
344 }
345
346 /// Get the cached release check result (for shutdown notification)
347 pub fn get_update_result(
348 &self,
349 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
350 self.update_checker
351 .as_ref()
352 .and_then(|c| c.get_cached_result())
353 }
354
355 /// Set a custom update checker (for testing)
356 ///
357 /// This allows injecting a custom PeriodicUpdateChecker that points to a mock server,
358 /// enabling E2E tests for the update notification UI.
359 #[doc(hidden)]
360 pub fn set_update_checker(
361 &mut self,
362 checker: crate::services::release_checker::PeriodicUpdateChecker,
363 ) {
364 self.update_checker = Some(checker);
365 }
366
367 /// Configure LSP server for a specific language
368 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
369 if let Some(ref mut lsp) = self.lsp {
370 lsp.set_language_configs(language, config);
371 }
372 }
373
374 /// Get a list of currently running LSP server languages
375 pub fn running_lsp_servers(&self) -> Vec<String> {
376 self.lsp
377 .as_ref()
378 .map(|lsp| lsp.running_servers())
379 .unwrap_or_default()
380 }
381
382 /// Return the number of pending completion requests.
383 pub fn pending_completion_requests_count(&self) -> usize {
384 self.pending_completion_requests.len()
385 }
386
387 /// Return the number of stored completion items.
388 pub fn completion_items_count(&self) -> usize {
389 self.completion_items.as_ref().map_or(0, |v| v.len())
390 }
391
392 /// Return the number of initialized LSP servers for a given language.
393 pub fn initialized_lsp_server_count(&self, language: &str) -> usize {
394 self.lsp
395 .as_ref()
396 .map(|lsp| {
397 lsp.get_handles(language)
398 .iter()
399 .filter(|sh| sh.capabilities.initialized)
400 .count()
401 })
402 .unwrap_or(0)
403 }
404
405 /// Shutdown an LSP server by language (marks it as disabled until manual restart)
406 ///
407 /// Returns true if the server was found and shutdown, false otherwise
408 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
409 if let Some(ref mut lsp) = self.lsp {
410 lsp.shutdown_server(language)
411 } else {
412 false
413 }
414 }
415
416 /// Enable event log streaming to a file
417 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
418 // Enable streaming for all existing event logs
419 for event_log in self.event_logs.values_mut() {
420 event_log.enable_streaming(&path)?;
421 }
422 Ok(())
423 }
424
425 /// Log keystroke for debugging
426 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
427 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
428 event_log.log_keystroke(key_code, modifiers);
429 }
430 }
431
432 /// Set up warning log monitoring
433 ///
434 /// When warnings/errors are logged, they will be written to the specified path
435 /// and the editor will be notified via the receiver.
436 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
437 self.warning_log = Some((receiver, path));
438 }
439
440 /// Take the warning-log receiver+path out of this editor.
441 ///
442 /// The receiver is single-consumer and lives for the process's
443 /// lifetime; on a destructive editor restart (e.g. authority swap)
444 /// `main.rs` lifts it from the old editor and re-installs it on the
445 /// new one so warnings keep flowing post-restart instead of vanishing
446 /// with the dropped editor.
447 pub fn take_warning_log(&mut self) -> Option<(std::sync::mpsc::Receiver<()>, PathBuf)> {
448 self.warning_log.take()
449 }
450
451 /// Set the status message log path
452 pub fn set_status_log_path(&mut self, path: PathBuf) {
453 self.status_log_path = Some(path);
454 }
455
456 /// Queue a new authority and restart the editor.
457 ///
458 /// Per the design decision in `docs/internal/AUTHORITY_DESIGN.md`,
459 /// authority transitions piggy-back on the existing
460 /// `change_working_dir` restart path. The caller never sees an
461 /// editor that is half-transitioned: the current `Editor` is
462 /// dropped, `main.rs` rebuilds a fresh one with the queued
463 /// authority, and session restore reopens buffers against the new
464 /// backend. This is slower than an in-place pointer swap but is
465 /// far more robust — every cached `Arc<dyn FileSystem>`, LSP
466 /// handle, terminal PTY, plugin state, and in-flight task is
467 /// dropped cleanly by the existing restart machinery.
468 pub fn install_authority(&mut self, authority: crate::services::authority::Authority) {
469 self.pending_authority = Some(authority);
470 // Re-open the same working directory; `main.rs` picks up the
471 // pending authority from the old editor just before dropping it.
472 self.request_restart(self.working_dir.clone());
473 }
474
475 /// Restore the default local authority. Same destructive-restart
476 /// semantics as `install_authority` — the caller never observes a
477 /// half-transitioned editor.
478 pub fn clear_authority(&mut self) {
479 self.install_authority(crate::services::authority::Authority::local());
480 }
481
482 /// Take the queued authority (if any). Called by `main.rs` on
483 /// restart to move the queued authority into the fresh editor.
484 pub fn take_pending_authority(&mut self) -> Option<crate::services::authority::Authority> {
485 self.pending_authority.take()
486 }
487
488 /// Directly replace the active authority without triggering a
489 /// restart. Intended for the post-construction wiring in `main.rs`
490 /// only, where the editor is still being set up and there is no
491 /// user-visible state to preserve. Do not call this from the event
492 /// loop — use `install_authority` for that.
493 ///
494 /// Also refreshes the plugin state snapshot so hooks that fire after
495 /// this call (notably `plugins_loaded`, fired by `main.rs` right
496 /// after `set_boot_authority`) see the real `authority_label` instead
497 /// of the empty string the temporary `Authority::local()` carried
498 /// during construction.
499 pub fn set_boot_authority(&mut self, authority: crate::services::authority::Authority) {
500 self.authority = authority;
501 // Propagate the authority's long-running spawner into the LSP
502 // manager so `force_spawn` can route server processes through
503 // the right backend. The editor rebuilds on every authority
504 // transition (AUTHORITY_DESIGN.md principle 7), so this is the
505 // single wiring point — no need for a hot-swap API. Path
506 // translation rides along for the same reason — LSP URIs need
507 // to be host↔container-translated under the new authority.
508 if let Some(lsp) = self.lsp.as_mut() {
509 lsp.set_long_running_spawner(self.authority.long_running_spawner.clone());
510 lsp.set_path_translation(self.authority.path_translation.clone());
511 }
512 #[cfg(feature = "plugins")]
513 {
514 self.update_plugin_state_snapshot();
515 // Notify plugins so they can re-register state-gated
516 // commands (e.g. devcontainer `Attach` only when not
517 // attached). Production transitions also trigger a full
518 // editor restart that re-runs plugin init, but firing
519 // here keeps in-process transitions and the test harness
520 // (which simulates the restart inline) consistent.
521 let label = self.authority.display_label.clone();
522 self.plugin_manager.run_hook(
523 "authority_changed",
524 crate::services::plugins::hooks::HookArgs::AuthorityChanged { label },
525 );
526 }
527 }
528
529 /// Read-only access to the active authority.
530 pub fn authority(&self) -> &crate::services::authority::Authority {
531 &self.authority
532 }
533
534 /// The editor's current working directory. This is the project
535 /// root; individual buffers may live elsewhere.
536 pub fn working_dir(&self) -> &std::path::Path {
537 &self.working_dir
538 }
539
540 /// Return buffer ids whose on-disk path sits at or under `root`.
541 /// Used by file-explorer operations that need to react when a file
542 /// or directory on disk goes away or moves.
543 pub fn buffer_ids_under_path(&self, root: &std::path::Path) -> Vec<BufferId> {
544 self.buffers
545 .iter()
546 .filter_map(|(id, state)| {
547 let p = state.buffer.file_path()?;
548 if p == root || p.starts_with(root) {
549 Some(*id)
550 } else {
551 None
552 }
553 })
554 .collect()
555 }
556
557 /// Get remote connection info if editing remote files
558 ///
559 /// Returns `Some("user@host")` for remote editing, `None` for local.
560 pub fn remote_connection_info(&self) -> Option<&str> {
561 self.authority.filesystem.remote_connection_info()
562 }
563
564 /// Get connection string for display in status bar and file explorer.
565 ///
566 /// Per principle 9, identity lives in the authority. The label set
567 /// by whoever constructed the authority wins; if it is empty (the
568 /// SSH constructor leaves it that way) we fall back to the
569 /// filesystem's `remote_connection_info()`, which knows how to
570 /// annotate disconnected SSH sessions.
571 pub fn connection_display_string(&self) -> Option<String> {
572 if !self.authority.display_label.is_empty() {
573 return Some(self.authority.display_label.clone());
574 }
575 self.remote_connection_info().map(|conn| {
576 if self.authority.filesystem.is_remote_connected() {
577 conn.to_string()
578 } else {
579 format!("{} (Disconnected)", conn)
580 }
581 })
582 }
583
584 /// Get the status log path
585 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
586 self.status_log_path.as_ref()
587 }
588
589 /// Open the status log file (user clicked on status message)
590 pub fn open_status_log(&mut self) {
591 if let Some(path) = self.status_log_path.clone() {
592 // Use open_local_file since log files are always local
593 match self.open_local_file(&path) {
594 Ok(buffer_id) => {
595 self.mark_buffer_read_only(buffer_id, true);
596 }
597 Err(e) => {
598 tracing::error!("Failed to open status log: {}", e);
599 }
600 }
601 } else {
602 self.set_status_message("Status log not available".to_string());
603 }
604 }
605
606 /// Check for and handle any new warnings in the warning log
607 ///
608 /// Updates the general warning domain for the status bar.
609 /// Returns true if new warnings were found.
610 pub fn check_warning_log(&mut self) -> bool {
611 let Some((receiver, path)) = &self.warning_log else {
612 return false;
613 };
614
615 // Non-blocking check for any warnings
616 let mut new_warning_count = 0usize;
617 while receiver.try_recv().is_ok() {
618 new_warning_count += 1;
619 }
620
621 if new_warning_count > 0 {
622 // Update general warning domain (don't auto-open file)
623 self.warning_domains.general.add_warnings(new_warning_count);
624 self.warning_domains.general.set_log_path(path.clone());
625 }
626
627 new_warning_count > 0
628 }
629
630 /// Get the warning domain registry
631 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
632 &self.warning_domains
633 }
634
635 /// Get the warning log path (for opening when user clicks indicator)
636 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
637 self.warning_domains.general.log_path.as_ref()
638 }
639
640 /// Open the warning log file (user-initiated action)
641 pub fn open_warning_log(&mut self) {
642 if let Some(path) = self.warning_domains.general.log_path.clone() {
643 // Use open_local_file since log files are always local
644 match self.open_local_file(&path) {
645 Ok(buffer_id) => {
646 self.mark_buffer_read_only(buffer_id, true);
647 }
648 Err(e) => {
649 tracing::error!("Failed to open warning log: {}", e);
650 }
651 }
652 }
653 }
654
655 /// Clear the general warning indicator (user dismissed)
656 pub fn clear_warning_indicator(&mut self) {
657 self.warning_domains.general.clear();
658 }
659
660 /// Clear all warning indicators (user dismissed via command)
661 pub fn clear_warnings(&mut self) {
662 self.warning_domains.general.clear();
663 self.warning_domains.lsp.clear();
664 self.status_message = Some("Warnings cleared".to_string());
665 }
666
667 /// Check if any LSP server is in error state
668 pub fn has_lsp_error(&self) -> bool {
669 self.warning_domains.lsp.level() == WarningLevel::Error
670 }
671
672 /// Get the effective warning level for the status bar (LSP indicator)
673 /// Returns Error if LSP has errors, Warning if there are warnings, None otherwise
674 pub fn get_effective_warning_level(&self) -> WarningLevel {
675 self.warning_domains.lsp.level()
676 }
677
678 /// Get the general warning level (for the general warning badge)
679 pub fn get_general_warning_level(&self) -> WarningLevel {
680 self.warning_domains.general.level()
681 }
682
683 /// Get the general warning count
684 pub fn get_general_warning_count(&self) -> usize {
685 self.warning_domains.general.count
686 }
687
688 /// Update LSP warning domain from server statuses
689 pub fn update_lsp_warning_domain(&mut self) {
690 self.warning_domains
691 .lsp
692 .update_from_statuses(&self.lsp_server_statuses);
693 }
694
695 /// Check if mouse hover timer has expired and trigger LSP hover request
696 ///
697 /// This implements debounced hover - we wait for the configured delay before
698 /// sending the request to avoid spamming the LSP server on every mouse move.
699 /// Returns true if a hover request was triggered.
700 pub fn check_mouse_hover_timer(&mut self) -> bool {
701 // Check if mouse hover is enabled
702 if !self.config.editor.mouse_hover_enabled {
703 return false;
704 }
705
706 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
707
708 // Get hover state without borrowing self
709 let hover_info = match self.mouse_state.lsp_hover_state {
710 Some((byte_pos, start_time, screen_x, screen_y)) => {
711 if self.mouse_state.lsp_hover_request_sent {
712 return false; // Already sent request for this position
713 }
714 if start_time.elapsed() < hover_delay {
715 return false; // Timer hasn't expired yet
716 }
717 Some((byte_pos, screen_x, screen_y))
718 }
719 None => return false,
720 };
721
722 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
723 return false;
724 };
725
726 // Store mouse position for popup positioning
727 self.hover.set_screen_position((screen_x, screen_y));
728
729 // Request hover at the byte position — only mark as sent if dispatched
730 match self.request_hover_at_position(byte_pos) {
731 Ok(true) => {
732 self.mouse_state.lsp_hover_request_sent = true;
733 true
734 }
735 Ok(false) => false, // no server ready, timer will retry
736 Err(e) => {
737 tracing::debug!("Failed to request hover: {}", e);
738 false
739 }
740 }
741 }
742
743 /// Check if semantic highlight debounce timer has expired
744 ///
745 /// Returns true if a redraw is needed because the debounce period has elapsed
746 /// and semantic highlights need to be recomputed.
747 pub fn check_semantic_highlight_timer(&self) -> bool {
748 // Check all buffers for pending semantic highlight redraws
749 for state in self.buffers.values() {
750 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
751 if remaining.is_zero() {
752 return true;
753 }
754 }
755 }
756 false
757 }
758
759 /// Check if diagnostic pull timer has expired and trigger re-pull if so.
760 ///
761 /// Debounced diagnostic re-pull after document changes — waits 500ms after
762 /// the last edit before requesting fresh diagnostics from the LSP server.
763 pub fn check_diagnostic_pull_timer(&mut self) -> bool {
764 let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
765 return false;
766 };
767
768 if Instant::now() < trigger_time {
769 return false;
770 }
771
772 self.scheduled_diagnostic_pull = None;
773
774 // Get URI and language for this buffer
775 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
776 return false;
777 };
778 let Some(uri) = metadata.file_uri().cloned() else {
779 return false;
780 };
781 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
782 return false;
783 };
784
785 let Some(lsp) = self.lsp.as_mut() else {
786 return false;
787 };
788 let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
789 else {
790 return false;
791 };
792 let client = &mut sh.handle;
793
794 let request_id = self.next_lsp_request_id;
795 self.next_lsp_request_id += 1;
796 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
797 if let Err(e) =
798 client.document_diagnostic(request_id, uri.as_uri().clone(), previous_result_id)
799 {
800 tracing::debug!(
801 "Failed to pull diagnostics after edit for {}: {}",
802 uri.as_str(),
803 e
804 );
805 } else {
806 tracing::debug!(
807 "Pulling diagnostics after edit for {} (request_id={})",
808 uri.as_str(),
809 request_id
810 );
811 }
812
813 false // no immediate redraw needed; diagnostics arrive asynchronously
814 }
815
816 /// Check if completion trigger timer has expired and trigger completion if so
817 ///
818 /// This implements debounced completion - we wait for quick_suggestions_delay_ms
819 /// before sending the completion request to avoid spamming the LSP server.
820 /// Returns true if a completion request was triggered.
821 pub fn check_completion_trigger_timer(&mut self) -> bool {
822 // Check if we have a scheduled completion trigger
823 let Some(trigger_time) = self.scheduled_completion_trigger else {
824 return false;
825 };
826
827 // Check if the timer has expired
828 if Instant::now() < trigger_time {
829 return false;
830 }
831
832 // Clear the scheduled trigger
833 self.scheduled_completion_trigger = None;
834
835 // Don't trigger if a popup is already visible
836 if self.active_state().popups.is_visible() {
837 return false;
838 }
839
840 // Trigger the completion request
841 self.request_completion();
842
843 true
844 }
845}