Skip to main content

fresh/app/window/
mod.rs

1//! Editor `Window` — a project-rooted unit of editor state.
2//!
3//! A `Window` bundles the state that is logically scoped to one
4//! project root: the file tree, ignore matcher, LSP client set,
5//! file watchers, split layout, and buffer membership. Switching the
6//! active window re-targets the entire editor UI (file explorer,
7//! quick-open, LSP roots) without recreating buffers, terminals, or
8//! plugin state — those live on the `Editor` and survive switches.
9//!
10//! See `docs/internal/orchestrator-sessions-design.md` for the full
11//! design rationale.
12//!
13//! ## Naming
14//!
15//! Internally we call these "windows" (modelled on VS Code windows)
16//! to disambiguate from Fresh's pre-existing workspace-recovery and
17//! config-layer "session" concepts. Orchestrator presents windows as
18//! "agent sessions" in its UX, since the parallel-agents domain
19//! language is what users see — but the editor types are `Window`,
20//! `WindowId`, etc.
21//!
22//! ## Migration status
23//!
24//! Steps 0a–0f, 0j, 0k phases 1–3, and 0l shipped. Per-subsystem
25//! state that used to warm-swap on `setActiveWindow` —
26//! `panel_ids`, `file_mod_times`, `file_explorer`, `lsp`, the
27//! `splits` pair, `buffers`, `buffer_metadata`, the terminal
28//! subsystem (`terminal_manager` + `terminal_buffers` +
29//! `terminal_backing_files` + `terminal_log_files`),
30//! `event_logs`, `position_history` (with its `in_navigation` /
31//! `suppress_position_history_once` companion flags),
32//! `bookmarks`, `grouped_subtrees`, `composite_buffers`,
33//! `composite_view_states`, all 23 LSP-request-tracking maps
34//! (pending-/in-flight/applied, debounce timers,
35//! `next_lsp_request_id`, `completion_items`, `dabbrev_state`,
36//! code-action attribution), the per-window async `bridge`, and
37//! the chrome surfaces (`status_message`, `plugin_status_message`,
38//! `prompt`) — all live directly on `Window`. `set_active_window`
39//! is a pointer write (plus first-dive seed allocation for
40//! windows that have never been activated).
41
42pub mod buffers;
43pub mod process_group;
44
45pub use buffers::WindowBuffers;
46pub use process_group::{LocalSignaller, ProcessGroupEntry, ProcessGroups, Signaller};
47
48use crate::app::types::{ChromeLayout, WindowLayoutCache};
49use crate::app::window_resources::WindowResources;
50use crate::model::event::{Event, LeafId};
51use crate::services::lsp::manager::LspManager;
52use crate::types::LspFeature;
53use crate::view::file_tree::FileTreeView;
54use crate::view::split::{SplitManager, SplitViewState};
55use fresh_core::{BufferId, WindowId};
56use std::collections::HashMap;
57use std::path::PathBuf;
58use std::sync::Arc;
59
60/// A project-rooted unit of editor state.
61///
62/// After Step 0b every per-subsystem field listed below is owned
63/// outright by the window — there are no warm-swap stashes.
64/// `setActiveWindow` is a pointer write; reads of the active
65/// window's state route through Editor accessors
66/// (`active_layout()`, `split_manager()`, `file_explorer()`, `lsp()`,
67/// `panel_ids()`, `file_mod_times()`, …). Cross-window access goes
68/// through `Editor.windows.get(&id)` directly.
69pub struct Window {
70    /// Stable identifier. The base window is always `WindowId(1)`.
71    pub id: WindowId,
72
73    /// User-visible label. Defaults to the basename of `root` (or
74    /// "main" when the root is the original process cwd). Not
75    /// required to be unique.
76    pub label: String,
77
78    /// Canonical absolute path of the project root. Read-only after
79    /// construction; closing a window and creating a new one is the
80    /// way to "rename" the root.
81    pub root: PathBuf,
82
83    /// File-explorer view (expansion, scroll, selection). `None`
84    /// means "never opened" — the caller rebuilds at `root` on first
85    /// toggle. Each window has its own view; switching windows shows
86    /// the new window's tree (or none, if it hasn't been opened yet).
87    pub file_explorer: Option<FileTreeView>,
88
89    /// Polling-based mtime cache for auto-revert. Auto-revert only
90    /// fires for the active window's files; inactive windows' mtimes
91    /// stay frozen at dive-out time and resync on dive-back —
92    /// matching the user's mental model that a dormant window "is
93    /// paused".
94    pub file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
95
96    /// LSP manager (running language servers, configs, per-language
97    /// root URIs). Each window owns its own LSP set, rooted at its
98    /// project root; inactive windows' servers remain running in the
99    /// background — that's the warm-LSP property the design's
100    /// trade-off discussion calls out as a memory cost worth paying
101    /// so dive-back is instant.
102    ///
103    /// `None` means "this window has never spawned any LSP"; the
104    /// next LSP feature trigger will lazily create one.
105    /// This window's language-server manager. Every window owns one,
106    /// built in [`Window::new`] rooted at the window's project root —
107    /// there is no "window without a manager" state (that was the
108    /// "No LSP manager available" bug). Servers are still spawned
109    /// lazily on demand; an idle window's manager holds only config.
110    pub lsp: LspManager,
111
112    /// Utility-dock panel-id → buffer-id occupancy. Each window
113    /// gets its own dock — when one window has the search panel
114    /// claimed and the user dives elsewhere, the new window starts
115    /// with an empty dock and rebuilds on demand.
116    pub panel_ids: HashMap<String, BufferId>,
117
118    /// Buffers attached to this window. Each window owns its
119    /// `EditorState`s outright; closing the window drops them.
120    /// Opening the same file in two windows produces two independent
121    /// buffers.
122    pub buffers: WindowBuffers,
123
124    /// Per-buffer metadata (display name, file path / LSP URI,
125    /// virtual-buffer mode, read-only flag, LSP-opened set, preview
126    /// flag, etc.) for the buffers in `Window.buffers`. Lives next
127    /// to the buffer storage it describes; closing a window drops
128    /// every metadata entry along with the buffers themselves.
129    pub buffer_metadata: HashMap<BufferId, crate::app::types::BufferMetadata>,
130
131    /// Per-buffer undo/redo event log. Lives next to `buffers`
132    /// because undo history is buffer-scoped — closing a window
133    /// drops the buffer and its log together.
134    pub event_logs: HashMap<BufferId, crate::model::event::EventLog>,
135
136    /// Status message (shown in this window's status bar). Per-window
137    /// because each window has its own context — a save in window A
138    /// shouldn't flash a status message into window B's UI. Only the
139    /// active window's chrome renders, so background-window status
140    /// messages are naturally invisible.
141    pub status_message: Option<String>,
142
143    /// Plugin-provided status message (displayed alongside the core
144    /// status, also per-window).
145    pub plugin_status_message: Option<String>,
146
147    /// Active prompt (minibuffer) for this window. Each window can
148    /// have its own prompt mid-flight; switching windows preserves
149    /// each window's prompt state independently.
150    pub prompt: Option<crate::view::prompt::Prompt>,
151
152    /// Per-window async bridge — the (Sender, Receiver) pair the
153    /// LSP manager (and per-window terminal/file-explorer tasks
154    /// once they migrate) uses to deliver async responses back to
155    /// the main loop. Each window owns its own channel so cleanup
156    /// on `closeWindow` is automatic (the receiver drops, senders
157    /// error and stop). Editor-global async messages (plugin
158    /// runtime callbacks, file-open dialog) flow through
159    /// `Editor.async_bridge` instead.
160    pub bridge: crate::services::async_bridge::AsyncBridge,
161
162    // ---- LSP request-tracking state (moved from Editor in Step 0k) ----
163    /// Per-window LSP request-id allocator. Each window's LspManager
164    /// talks to its own server connections, and each connection only
165    /// requires per-connection request-id uniqueness — no global
166    /// namespace needed. Starts at 0 per window.
167    pub next_lsp_request_id: u64,
168
169    /// Pending LSP completion request ids (multi-server).
170    pub pending_completion_requests: std::collections::HashSet<u64>,
171
172    /// Original LSP completion items (for type-to-filter).
173    pub completion_items: Option<Vec<lsp_types::CompletionItem>>,
174
175    /// Scheduled completion-trigger time (debounced quick-suggestions).
176    pub scheduled_completion_trigger: Option<std::time::Instant>,
177
178    /// Dabbrev cycling state (Alt+/ session).
179    pub dabbrev_state: Option<crate::app::DabbrevCycleState>,
180
181    /// Pending LSP go-to-definition request id.
182    pub pending_goto_definition_request: Option<u64>,
183
184    /// Pending LSP find-references request id and the symbol name.
185    pub pending_references_request: Option<u64>,
186    pub pending_references_symbol: String,
187
188    /// Pending LSP signature-help request id.
189    pub pending_signature_help_request: Option<u64>,
190
191    /// Pending LSP code-actions request ids and per-request server-name
192    /// attribution + the selected-from list.
193    pub pending_code_actions_requests: std::collections::HashSet<u64>,
194    pub pending_code_actions_server_names: std::collections::HashMap<u64, String>,
195    pub pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
196
197    /// Pending inlay-hints requests keyed by request id.
198    pub(crate) pending_inlay_hints_requests:
199        std::collections::HashMap<u64, crate::app::InlayHintsRequest>,
200
201    /// Pending folding-range requests + per-buffer in-flight tracking + debounce.
202    pub(crate) pending_folding_range_requests:
203        std::collections::HashMap<u64, crate::app::FoldingRangeRequest>,
204    pub folding_ranges_in_flight: std::collections::HashMap<BufferId, (u64, u64)>,
205    pub folding_ranges_debounce: std::collections::HashMap<BufferId, std::time::Instant>,
206
207    /// Pending semantic-tokens-full requests + per-buffer in-flight tracking +
208    /// the next-allowed-refresh debounce.
209    pub(crate) pending_semantic_token_requests:
210        std::collections::HashMap<u64, crate::app::SemanticTokenFullRequest>,
211    pub(crate) semantic_tokens_in_flight:
212        std::collections::HashMap<BufferId, (u64, u64, crate::app::SemanticTokensFullRequestKind)>,
213    pub semantic_tokens_full_debounce: std::collections::HashMap<BufferId, std::time::Instant>,
214
215    /// Pending semantic-tokens-range requests + per-buffer in-flight,
216    /// last-request, and last-applied tracking.
217    pub(crate) pending_semantic_token_range_requests:
218        std::collections::HashMap<u64, crate::app::SemanticTokenRangeRequest>,
219    pub semantic_tokens_range_in_flight:
220        std::collections::HashMap<BufferId, (u64, usize, usize, u64)>,
221    pub semantic_tokens_range_last_request:
222        std::collections::HashMap<BufferId, (usize, usize, u64, std::time::Instant)>,
223    pub semantic_tokens_range_applied: std::collections::HashMap<BufferId, (usize, usize, u64)>,
224
225    /// Back/forward navigation stack (cursor jumps, file switches)
226    /// scoped to this window. Each window has its own history so
227    /// switching windows doesn't pollute the other window's
228    /// back-stack — diving back into a window resumes navigation
229    /// where you left it.
230    pub position_history: crate::input::position_history::PositionHistory,
231
232    /// `true` while a back/forward jump is in progress. Suppresses
233    /// `track_cursor_movement` from recording the jump itself as a
234    /// new entry. Per-window so windows don't fight over the flag
235    /// during cross-window orchestration.
236    pub in_navigation: bool,
237
238    /// One-shot suppression of position-history recording for the
239    /// next buffer-switch (used by file-open paths that don't want
240    /// to leave a trail entry for the about-to-be-loaded file).
241    pub suppress_position_history_once: bool,
242
243    /// Bookmarks (single-char register → buffer + byte position) for
244    /// this window. Bookmarks point at this window's buffers and
245    /// follow the window across `setActiveWindow` switches — every
246    /// window has its own register set.
247    pub(crate) bookmarks: crate::app::bookmarks::BookmarkState,
248
249    /// Composite buffers in this window (separate from regular
250    /// buffers). These display multiple source buffers in a single
251    /// tab — Live Grep results, References, Diagnostics list,
252    /// etc. Owned per-window so the panel state follows the window
253    /// that opened it.
254    pub composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
255
256    /// Per-split view state for composite buffers in this window.
257    /// Keyed by (split_id, buffer_id) — each split that hosts a
258    /// composite buffer gets its own scroll-row tracking.
259    pub composite_view_states:
260        HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
261
262    /// Grouped `SplitNode` subtrees for this window, keyed by their
263    /// `LeafId` (which is what `TabTarget::Group(leaf_id)`
264    /// references). Each entry is a `SplitNode::Grouped` node
265    /// holding the layout for one buffer group (Live Grep, References,
266    /// Diagnostics, etc.). These subtrees are NOT part of the main
267    /// split tree — they live here and are dispatched to at render
268    /// time when the current split's active target is a `Group`.
269    /// Per-window because a buffer-group panel belongs to the window
270    /// that opened it.
271    pub grouped_subtrees: HashMap<LeafId, crate::view::split::SplitNode>,
272
273    /// Terminal subsystem (PTY processes + render-state grids) for
274    /// this window. Owned per-window so closing a window joins its
275    /// PTY threads — no orphan agents survive a `closeWindow`.
276    pub terminal_manager: crate::services::terminal::TerminalManager,
277
278    /// Maps a terminal-buffer id to its PTY id, scoped to this window.
279    pub terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
280
281    /// Backing files for terminal buffers (the rendered visible-screen
282    /// + scrollback content the buffer actually displays).
283    pub terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
284
285    /// Raw log files for terminal buffers (the unfiltered byte stream
286    /// from the PTY, used for replay / save-history).
287    pub terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
288
289    /// Terminal buffers whose tab title was set explicitly (plugin- or
290    /// command-derived). These are excluded from foreground-process
291    /// auto-naming so a program running inside doesn't clobber the chosen
292    /// title; an OSC title emitted by the program still takes precedence.
293    pub terminal_explicit_titles: std::collections::HashSet<BufferId>,
294
295    /// Last time foreground-process names were polled for terminal tab
296    /// auto-naming. Throttles the `tcgetpgrp` + `/proc` reads to roughly
297    /// once a second rather than every frame. `None` until the first poll.
298    pub(crate) terminal_fg_poll_at: Option<std::time::Instant>,
299
300    /// Cached foreground-process name per terminal buffer, refreshed on the
301    /// [`FG_POLL_INTERVAL`] poll. Present means a name was read; absent
302    /// means none was available (so callers fall back to the OSC title or
303    /// default). Applied to the tab every frame so the title stays put
304    /// between polls without re-running the syscall.
305    pub(crate) terminal_fg_cache: HashMap<BufferId, String>,
306
307    /// Plugin-managed per-window state. Outer key is plugin name,
308    /// inner is the plugin-defined key. Read via
309    /// `editor.getWindowState(key)` and written via
310    /// `editor.setWindowState(key, value)`. Persisted to the
311    /// orchestrator's global `windows.json` under the platform
312    /// data dir so it survives editor restarts.
313    pub plugin_state: HashMap<String, HashMap<String, serde_json::Value>>,
314
315    /// Window-scoped layout hit-test cache: split-leaf rects, tab
316    /// rects, the file-explorer rect, separators, scrollbars, and
317    /// per-leaf `view_line_mappings` that mouse positioning and
318    /// visual-line motion read. Repopulated by the renderer on every
319    /// frame; stale until the next render after a window switch (the
320    /// post-switch render fills it in before any input handling).
321    /// Editor-chrome rects (status bar, menu, popups, prompt overlay)
322    /// live on `Window::chrome_layout` (also per-window).
323    pub(crate) layout_cache: WindowLayoutCache,
324
325    /// Per-window editor-chrome layout cache: status bar, menu,
326    /// popups, prompt overlay, full-frame cell-theme map. Each
327    /// window has its own status bar / prompt / popup state, so the
328    /// cache is per-window. Repopulated by the renderer for the
329    /// active window every frame.
330    pub(crate) chrome_layout: ChromeLayout,
331
332    /// Last-known terminal screen dimensions, mirrored from
333    /// `Editor::terminal_width` / `Editor::terminal_height` whenever
334    /// `Editor::resize` loops over windows. Per-window because
335    /// `Window::resize_visible_terminals` and other per-window resize
336    /// logic need the screen size without reaching back to `Editor`.
337    pub(crate) terminal_width: u16,
338    pub(crate) terminal_height: u16,
339
340    /// Effective width (cols) of the editor-global left dock, pushed
341    /// down by `Editor::relayout` (the single layout funnel). Mirrored
342    /// here — like `terminal_width` — so per-window terminal sizing
343    /// (`resize_visible_terminals`) can subtract the dock without
344    /// reaching back to `Editor`. `0` when no dock is shown. This is a
345    /// derived cache, never a source of truth: `Editor::dock` owns the
346    /// real placement and `relayout` recomputes this from it.
347    pub(crate) dock_cols: u16,
348
349    /// Editor-global resources shared by `Arc` clone (config, theme
350    /// registry, keybindings, command registry, filesystem authority,
351    /// the buffer-id allocator, …). See [`WindowResources`] for the
352    /// full inventory and rationale.
353    pub(crate) resources: WindowResources,
354
355    /// Buffer currently opened in "preview" (ephemeral) mode, together
356    /// with the split (pane) it lives in. At most one preview exists
357    /// per window. Pre Step-0 this lived on `Editor`; moved here so
358    /// preview tracking follows the window's other view-state.
359    ///
360    /// Invariants:
361    /// - The `is_preview` flag on the referenced buffer's metadata is
362    ///   true iff this tuple is `Some` and points at that buffer.
363    /// - The preview is anchored to the split it was opened in.
364    /// - Cleared when the buffer is closed or promoted.
365    pub preview: Option<(LeafId, BufferId)>,
366
367    /// Whether terminal mode is active in this window (input goes to
368    /// the active terminal buffer). Per-window because each window
369    /// has its own terminal set + active buffer.
370    pub terminal_mode: bool,
371
372    /// Set of terminal buffer ids that should auto-resume terminal
373    /// mode when switched back to. Per-window because terminal
374    /// buffers are per-window (Step 0d).
375    pub terminal_mode_resume: std::collections::HashSet<BufferId>,
376
377    /// Track which byte ranges have been seen per buffer (for the
378    /// `lines_changed` plugin-hook optimisation). Keyed by `BufferId`,
379    /// follows the buffers onto Window.
380    pub seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
381
382    /// Previous viewport states for `viewport_changed` hook detection.
383    /// Stores `(top_byte, width, height)` from the end of the last
384    /// render frame. Keyed by `LeafId`, per-window because the splits
385    /// it tracks are per-window.
386    pub previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
387
388    /// Whether scroll syncing applies to splits showing the same
389    /// buffer. Per-window UX toggle.
390    pub same_buffer_scroll_sync: bool,
391
392    /// Per-window interactive search-and-replace session state.
393    /// Drives the F+y/n/!/q UX during `replace_in_buffer` /
394    /// `replace_all`. Per-window because the search target buffer
395    /// and the visible matches are window-scoped.
396    pub(crate) interactive_replace_state: Option<crate::app::types::InteractiveReplaceState>,
397
398    /// Cross-split scroll-sync manager for side-by-side diff views.
399    /// Per-window because the splits it pairs are per-window.
400    pub scroll_sync_manager: crate::view::scroll_sync::ScrollSyncManager,
401
402    /// Whether the file-explorer panel is visible in this window.
403    pub file_explorer_visible: bool,
404
405    /// Whether a file-explorer rebuild is in flight (debounce flag).
406    pub file_explorer_sync_in_progress: bool,
407
408    /// Width of the file-explorer panel.
409    pub file_explorer_width: crate::config::ExplorerWidth,
410
411    /// Side (left/right) the file-explorer panel docks on.
412    pub file_explorer_side: crate::config::FileExplorerSide,
413
414    /// Pending toggles for show-hidden/show-gitignored that apply on
415    /// the next file-explorer rebuild.
416    pub pending_file_explorer_show_hidden: Option<bool>,
417    pub pending_file_explorer_show_gitignored: Option<bool>,
418
419    /// Decorations supplied by plugins for the file explorer (badges,
420    /// status icons, etc.) keyed by absolute path.
421    pub file_explorer_decorations:
422        HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
423
424    /// Compiled decoration lookup cache invalidated when
425    /// `file_explorer_decorations` changes.
426    pub file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
427
428    /// Hover-popup correlation state (which buffer / cursor a hover
429    /// request was issued from). Per-window because hover requests
430    /// route through the active window's LSP.
431    pub(crate) hover: crate::app::hover::HoverState,
432
433    /// Active find-in-buffer search session (if any).
434    pub(crate) search_state: Option<crate::app::types::SearchState>,
435
436    /// Overlay namespace used for search-result highlights. Per-window
437    /// because the overlays it scopes are per-buffer (per-window).
438    pub search_namespace: crate::view::overlay::OverlayNamespace,
439
440    /// Range that should be reused when the next search is confirmed
441    /// (e.g. after the user picks a hit in the search overlay).
442    pub pending_search_range: Option<std::ops::Range<usize>>,
443
444    /// Last live-grep panel state (cached so re-opening the panel
445    /// preserves the user's query / scroll / selection).
446    pub live_grep_last_state: Option<crate::services::live_grep_state::LiveGrepLastState>,
447
448    /// Overlay-preview state used by the floating-prompt preview pane
449    /// when it's showing a buffer view.
450    pub overlay_preview_state: Option<crate::app::types::OverlayPreviewState>,
451
452    /// Whether auto-revert (poll-based file-mtime watching) is enabled
453    /// for buffers in this window.
454    pub auto_revert_enabled: bool,
455
456    /// Tracks rapid file-change events for debouncing the auto-revert
457    /// reload trigger.
458    pub file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
459
460    /// Cursor-position snapshot captured when the user opens the
461    /// goto-line prompt, restored on Esc.
462    pub(crate) goto_line_preview: Option<crate::app::GotoLinePreviewSnapshot>,
463
464    /// Pending plugin-issued prompt callback id (used by
465    /// `editor.startPrompt` to deliver the prompt result back).
466    pub pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
467
468    /// Buffer ids the user picked "save before quit" for via the
469    /// modified-buffers prompt; consumed in order on quit.
470    pub pending_quit_unnamed_save: Vec<BufferId>,
471
472    /// Per-window search UX toggles. Each window has its own search
473    /// session, so these flags follow the search state.
474    pub search_case_sensitive: bool,
475    pub search_whole_word: bool,
476    pub search_use_regex: bool,
477    pub search_confirm_each: bool,
478
479    /// Scheduled (debounced) per-buffer LSP feature requests for the
480    /// active window's LSP. Per-window because the LSP they target is
481    /// per-window (Step 0k).
482    pub scheduled_diagnostic_pull: Option<(BufferId, std::time::Instant)>,
483    pub scheduled_inlay_hints_request: Option<(BufferId, std::time::Instant)>,
484
485    /// LSP languages the user dismissed the "do you want to enable
486    /// LSP for this language?" popup for. Per-window because LSP is
487    /// per-window — different windows can prompt independently.
488    pub user_dismissed_lsp_languages: std::collections::HashSet<String>,
489
490    /// Active editor mode (e.g. "search", "replace", "macro-record").
491    /// Per-window because the modes drive UI affordances that belong
492    /// to one window's UX flow.
493    pub editor_mode: Option<String>,
494
495    /// Per-window prompt histories (one ring per `PromptType`). Each
496    /// window has its own minibuffer, so each maintains its own
497    /// history.
498    pub prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
499
500    /// Buffer id pending close-confirmation prompt resolution.
501    /// Per-window because the prompt that produced this is per-window.
502    pub pending_close_buffer: Option<BufferId>,
503
504    /// Pluggable completion service that orchestrates this window's
505    /// completion providers (dabbrev, buffer words, LSP, plugin
506    /// providers). Per-window because the providers it orchestrates
507    /// (notably the LSP set) are per-window.
508    pub completion_service: crate::services::completion::CompletionService,
509
510    /// Overlay namespace for LSP diagnostic overlays in this window
511    /// (filter / bulk-remove key). The diagnostics it scopes are buffer
512    /// overlays, and buffers are per-window, so the namespace follows.
513    pub lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
514
515    /// Last `result_id` seen from the LSP server per URI for incremental
516    /// pull diagnostics. Per-window because each window has its own
517    /// LSP manager and therefore its own result-id stream.
518    pub diagnostic_result_ids: HashMap<String, String>,
519
520    /// `$/progress` token → progress info for this window's LSP servers.
521    /// Drives the spinner in the status bar's LSP pill. Per-window
522    /// because the LspManager that emits these tokens is per-window.
523    pub(crate) lsp_progress: HashMap<String, crate::app::LspProgressInfo>,
524
525    /// Status of each `(language, server_name)` pair attached to this
526    /// window's LspManager (running, errored, restarting, …).
527    pub lsp_server_statuses:
528        HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
529
530    /// Plugin-contributed menu items merged into the LSP-Servers popup
531    /// (the one opened by clicking the LSP indicator). Keyed by
532    /// `(language, plugin_id)` so each plugin owns its own slice and
533    /// can refresh it independently. The items render as an extra
534    /// section in `build_and_show_lsp_status_popup` between the
535    /// built-in actions and the trailing "View Log / Dismiss" rows.
536    /// Selecting one fires `action_popup_result` with `popup_id =
537    /// "lsp_status"` and `action_id = "{plugin_id}|{item_id}"` so the
538    /// contributing plugin can react.
539    ///
540    /// See #1941 follow-up "Option B": instead of plugins pushing
541    /// their own separate popup (which created the stacked-popup UX
542    /// problem), they contribute items into the single LSP-Servers
543    /// popup.
544    pub lsp_menu_contributions: HashMap<(String, String), Vec<crate::app::LspMenuItem>>,
545
546    /// Recent `window/showMessage` payloads from this window's LSP
547    /// servers. Bounded ring (newest entries kept, drops the oldest
548    /// when the soft cap is exceeded).
549    pub(crate) lsp_window_messages: Vec<crate::app::LspMessageEntry>,
550
551    /// Recent `window/logMessage` payloads from this window's LSP
552    /// servers, on the same bounded-ring pattern as `lsp_window_messages`.
553    pub(crate) lsp_log_messages: Vec<crate::app::LspMessageEntry>,
554
555    /// Push-model diagnostics keyed by URI, then by server name. Each
556    /// `publishDiagnostics` from a server replaces that server's slice
557    /// for the URI; the merged view is materialised in
558    /// `stored_diagnostics`.
559    pub stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
560
561    /// Pull-model diagnostics (rust-analyzer-style native pull)
562    /// keyed by URI. Independent of `stored_push_diagnostics`; the
563    /// two are merged into `stored_diagnostics` for plugin / overlay
564    /// consumption.
565    pub stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
566
567    /// Merged view of push + pull diagnostics, exposed to plugins.
568    /// `Arc` wrapper so plugin snapshots can hold a refcount-bumped
569    /// reference; mutation goes through `Arc::make_mut` (CoW).
570    pub stored_diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
571
572    /// Per-URI folding ranges from `textDocument/foldingRange`. Same
573    /// `Arc` + CoW pattern as `stored_diagnostics` so plugin snapshots
574    /// don't pin the underlying map across mutations.
575    pub stored_folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
576
577    /// Per-directory mtime cache (paired with `file_mod_times`) for
578    /// detecting file-tree changes in this window. Per-window because
579    /// the file tree is per-window.
580    pub dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
581
582    /// Last time auto-revert polled this window's open buffers.
583    pub last_auto_revert_poll: std::time::Instant,
584
585    /// Last time the file-tree change-detection poll fired for this window.
586    pub last_file_tree_poll: std::time::Instant,
587
588    /// Whether this window has resolved and seeded the `.git/index`
589    /// path in `dir_mod_times`.
590    pub git_index_resolved: bool,
591
592    /// Receiver for background file change poll results for this window.
593    /// `Some` while a metadata poll is in flight.
594    #[allow(clippy::type_complexity)]
595    pub pending_file_poll_rx:
596        Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
597
598    /// Receiver for background directory change poll results for this window.
599    #[allow(clippy::type_complexity)]
600    pub pending_dir_poll_rx: Option<
601        std::sync::mpsc::Receiver<(
602            Vec<(
603                crate::view::file_tree::NodeId,
604                PathBuf,
605                Option<std::time::SystemTime>,
606            )>,
607            Option<(PathBuf, std::time::SystemTime)>,
608        )>,
609    >,
610
611    /// Terminals in this window that should not persist to the
612    /// workspace file. Plugin-created terminals default to ephemeral;
613    /// user-opened terminals are absent and persist as before.
614    pub ephemeral_terminals: std::collections::HashSet<crate::services::terminal::TerminalId>,
615
616    /// Plugin-development workspace per buffer (temp dir + LSP
617    /// configuration for plugin buffers). Buffer-keyed and buffers
618    /// are per-window, so the workspace map follows.
619    pub plugin_dev_workspaces:
620        HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
621
622    /// Per-buffer plugin status-bar token values. Outer key: BufferId;
623    /// inner key: "plugin_name:token_name"; inner value: current text
624    /// to render. The registry of which tokens exist lives globally on
625    /// `Editor.status_bar_token_registry`; this map holds only the
626    /// values plugins have pushed for individual buffers.
627    pub status_bar_values: HashMap<BufferId, HashMap<String, String>>,
628
629    /// Mouse drag/selection/scrollbar state for this window. Drag
630    /// targets reference per-window LeafIds and BufferIds.
631    pub(crate) mouse_state: crate::app::types::MouseState,
632
633    /// Currently focused widget context (Normal / FileExplorer /
634    /// Terminal / Prompt …). Per-window because each window has its
635    /// own focus state — switching windows preserves each window's
636    /// focused widget.
637    pub key_context: crate::input::keybindings::KeyContext,
638
639    /// Pending chord sequence for multi-key bindings (e.g. C-x C-s).
640    /// Each window tracks its own in-progress chord.
641    pub chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
642
643    /// Multi-click detection state (per-window because clicks land
644    /// inside a window).
645    pub previous_click_time: Option<std::time::Instant>,
646    pub previous_click_position: Option<(u16, u16)>,
647    pub click_count: u8,
648
649    /// Whether mouse capture is enabled in this window.
650    pub mouse_enabled: bool,
651
652    /// GPM software-cursor position for this window (when GPM is
653    /// active and we draw our own cursor).
654    pub mouse_cursor_position: Option<(u16, u16)>,
655    pub gpm_active: bool,
656
657    /// Per-window chrome toggles. Each window can independently show
658    /// or hide its menu bar / tab bar / status bar / prompt line.
659    pub menu_bar_visible: bool,
660    pub menu_bar_auto_shown: bool,
661    pub tab_bar_visible: bool,
662    pub status_bar_visible: bool,
663    pub prompt_line_visible: bool,
664
665    /// Timing state for auto-recovery saves and persistent auto-saves
666    /// in this window.
667    pub last_auto_recovery_save: std::time::Instant,
668    pub last_persistent_auto_save: std::time::Instant,
669
670    /// Warning domain registry for this window's status indicator.
671    pub warning_domains: crate::app::warning_domains::WarningDomainRegistry,
672
673    /// Tab context menu state (right-click on a tab in this window).
674    pub tab_context_menu: Option<crate::app::types::TabContextMenu>,
675
676    /// File-explorer context menu state (right-click in the explorer).
677    pub file_explorer_context_menu: Option<crate::app::types::FileExplorerContextMenu>,
678
679    /// Theme inspector popup (Ctrl+Right-Click) anchored in this window.
680    pub theme_info_popup: Option<crate::app::types::ThemeInfoPopup>,
681
682    /// Event debug dialog state (when the event-debug modal is open in
683    /// this window). The dialog records keystrokes for the window's
684    /// input pipeline so it's logically per-window.
685    pub event_debug: Option<crate::app::event_debug::EventDebug>,
686
687    /// File-open dialog state (when PromptType::OpenFile is active in
688    /// this window's prompt).
689    pub file_open_state: Option<crate::app::file_open::FileOpenState>,
690
691    /// Cached layout for the file browser (mouse hit-testing).
692    pub file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
693
694    /// Buffer groups (multiple buffers shown as one tab) in this window.
695    pub buffer_groups: HashMap<crate::app::types::BufferGroupId, crate::app::types::BufferGroup>,
696    /// Reverse index: buffer ID → group ID.
697    pub buffer_to_group: HashMap<BufferId, crate::app::types::BufferGroupId>,
698    /// Next buffer group id within this window.
699    pub next_buffer_group_id: usize,
700
701    /// Plugin keystroke-callback queue (in-flight `getNextKey()` callbacks).
702    pub pending_next_key_callbacks: std::collections::VecDeque<fresh_core::api::JsCallbackId>,
703
704    /// Whether a plugin currently has key-capture active in this window.
705    pub key_capture_active: bool,
706
707    /// Keys queued while `key_capture_active` was set but no callback
708    /// was pending — drained on the next `AwaitNextKey`.
709    pub pending_key_capture_buffer: std::collections::VecDeque<fresh_core::api::KeyEventPayload>,
710
711    /// Macro state (record/playback/registers) — one window's macro
712    /// session at a time.
713    pub(crate) macros: crate::app::macros::MacroState,
714
715    /// Plugin-defined custom contexts active in this window (drives
716    /// command palette visibility, e.g. "config-editor").
717    pub active_custom_contexts: std::collections::HashSet<String>,
718
719    /// Whether keyboard capture is active for the terminal in this
720    /// window (terminal mode swallows non-toggle keys).
721    pub keyboard_capture: bool,
722
723    /// In-flight review session hunks for this window.
724    pub review_hunks: Vec<fresh_core::api::ReviewHunk>,
725
726    /// Pending file-open queue (PendingFileOpen) for this window.
727    pub pending_file_opens: Vec<crate::app::PendingFileOpen>,
728
729    /// Whether this window has a hot-exit recovery prompt pending.
730    pub pending_hot_exit_recovery: bool,
731
732    /// Plugin "wait until file opens" tracking (buffer_id → (wait_id, …)).
733    pub wait_tracking: HashMap<BufferId, (u64, bool)>,
734
735    /// Wait ids that have completed and need to be reported back to plugins.
736    pub completed_waits: Vec<u64>,
737
738    /// Background line-scan state for this window (line counts for
739    /// large files).
740    pub(crate) line_scan: crate::app::line_scan::LineScan,
741
742    /// Background search-scan state for this window.
743    pub(crate) search_scan: crate::app::search_scan::SearchScan,
744
745    /// Anchor for the search-result overlay in this window.
746    pub search_overlay_top_byte: Option<usize>,
747
748    /// Per-window UI animation runner.
749    pub animations: crate::view::animation::AnimationRunner,
750
751    /// Plugin error log (populated when plugin status messages match
752    /// error patterns; tests assert against this).
753    pub plugin_errors: Vec<String>,
754
755    /// Cut/copy clipboard for file-explorer ops in this window. Each
756    /// window has its own paste buffer; cross-window file ops would
757    /// require a separately-shared clipboard.
758    pub file_explorer_clipboard: Option<crate::app::file_explorer::FileExplorerClipboard>,
759
760    /// Process-group tracking for everything this window owns
761    /// (today: pty children from `terminal_manager.spawn`).
762    /// Exposed through `signal_all` so window-level lifecycle
763    /// operations can terminate every spawned process in one
764    /// call regardless of how many terminals the window owns —
765    /// see [`process_group`] module docs for the authority-
766    /// pluggable `Signaller` design.
767    pub process_groups: ProcessGroups,
768}
769
770/// Apply language-server configuration to a freshly-created
771/// [`LspManager`]: per-language configs, the universal (global)
772/// servers, and the Deno auto-detection override. Shared by every
773/// window's construction so the server set is identical regardless of
774/// how the window came to exist (boot, orchestrator new-session,
775/// disk-restored shell).
776pub(crate) fn configure_lsp_servers(
777    lsp: &mut LspManager,
778    root: &std::path::Path,
779    config: &crate::config::Config,
780) {
781    use crate::types::{LspServerConfig, ProcessLimits};
782
783    // Per-language servers from config.
784    for (language, lsp_configs) in &config.lsp {
785        lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
786    }
787
788    // Universal (global) servers — spawned once, shared across languages.
789    let universal_servers: Vec<LspServerConfig> = config
790        .universal_lsp
791        .values()
792        .flat_map(|lc| lc.as_slice().to_vec())
793        .filter(|c| c.enabled)
794        .collect();
795    lsp.set_universal_configs(universal_servers);
796
797    // Auto-detect Deno projects: if deno.json or deno.jsonc exists in the
798    // window root, override JS/TS LSP to use `deno lsp` (#1191). Checked
799    // against the window's own root so each session gets the detection for
800    // its actual project rather than the process cwd.
801    if root.join("deno.json").exists() || root.join("deno.jsonc").exists() {
802        tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
803        let deno_config = LspServerConfig {
804            command: "deno".to_string(),
805            args: vec!["lsp".to_string()],
806            enabled: true,
807            auto_start: false,
808            process_limits: ProcessLimits::default(),
809            initialization_options: Some(serde_json::json!({"enable": true})),
810            ..Default::default()
811        };
812        lsp.set_language_config("javascript".to_string(), deno_config.clone());
813        lsp.set_language_config("typescript".to_string(), deno_config);
814    }
815}
816
817/// Build the [`LspManager`] every window owns: rooted at the window's
818/// own `root`, wired to its own `bridge` (which
819/// `process_async_messages` drains every frame) and the shared tokio
820/// runtime, and configured with the full server set. Called from
821/// [`Window::new`] so the manager is present *by construction* — there
822/// is no window without one, and no "No LSP manager available" state to
823/// represent.
824pub(crate) fn build_window_lsp(
825    id: WindowId,
826    root: &std::path::Path,
827    resources: &crate::app::window_resources::WindowResources,
828    bridge: &crate::services::async_bridge::AsyncBridge,
829) -> LspManager {
830    let root_uri = crate::app::types::file_path_to_lsp_uri(root);
831    let mut lsp = LspManager::new(id, root_uri);
832
833    // No runtime means async features are disabled (matches the
834    // historical base-window path when the tokio runtime fails to build).
835    if let Some(runtime) = resources.tokio_runtime.as_ref() {
836        lsp.set_runtime(runtime.handle().clone(), bridge.clone());
837    }
838
839    // Wire the LSP backend from the window's authority at construction:
840    // `force_spawn` routes server processes through the long-running
841    // spawner, URIs are host↔container-translated via path translation, and
842    // trust gates spawning. Doing this here (rather than via a later
843    // `set_boot_authority`) means the manager is never left pointing at a
844    // backend that doesn't match the authority the window was built with.
845    lsp.set_long_running_spawner(resources.authority.long_running_spawner.clone());
846    lsp.set_path_translation(resources.authority.path_translation.clone());
847    lsp.set_workspace_trust(resources.authority.workspace_trust.clone());
848
849    configure_lsp_servers(&mut lsp, root, &resources.config);
850    lsp
851}
852
853impl Window {
854    /// Apply LSP folding ranges to the named buffer's `folding_ranges`
855    /// store. Pure window mutation — no editor-global state touched.
856    /// Used by the LSP folding-ranges response dispatcher after the
857    /// editor-global URI-keyed map has been updated.
858    pub fn apply_folding_ranges_response(
859        &mut self,
860        buffer_id: BufferId,
861        lsp_ranges: Vec<lsp_types::FoldingRange>,
862    ) {
863        let Some(state) = self.buffers.get_mut(&buffer_id) else {
864            return;
865        };
866        state
867            .folding_ranges
868            .set_from_lsp(&state.buffer, &mut state.marker_list, lsp_ranges);
869    }
870
871    /// Allocate a fresh per-window LSP request id and return it. The
872    /// counter is per-window because each window's `LspManager` talks
873    /// to its own server connections — no global namespace needed.
874    pub fn alloc_lsp_request_id(&mut self) -> u64 {
875        let id = self.next_lsp_request_id;
876        self.next_lsp_request_id += 1;
877        id
878    }
879
880    /// True if this window has any in-flight LSP completion or
881    /// goto-definition request whose response would still be relevant.
882    pub fn has_pending_lsp_requests(&self) -> bool {
883        !self.pending_completion_requests.is_empty()
884            || self.pending_goto_definition_request.is_some()
885    }
886
887    /// Cancel any in-flight LSP requests on this window. Called when
888    /// the user does something that would make the response stale
889    /// (cursor movement, text edit, scroll). Drains the pending
890    /// completion id set, clears the goto-definition slot, and sends
891    /// `$/cancelRequest` to the appropriate server for each.
892    pub(crate) fn cancel_pending_lsp_requests(&mut self) {
893        self.scheduled_completion_trigger = None;
894        if !self.pending_completion_requests.is_empty() {
895            let ids: Vec<u64> = self.pending_completion_requests.drain().collect();
896            for request_id in ids {
897                tracing::debug!("Canceling pending LSP completion request {}", request_id);
898                self.send_lsp_cancel_request(request_id);
899            }
900        }
901        if let Some(request_id) = self.pending_goto_definition_request.take() {
902            tracing::debug!(
903                "Canceling pending LSP goto-definition request {}",
904                request_id
905            );
906            self.send_lsp_cancel_request(request_id);
907        }
908    }
909
910    /// Send `$/cancelRequest` to the LSP server backing the active
911    /// buffer's language, if a server is already running. Called only
912    /// from cancel paths — does not spawn a server just to cancel.
913    pub(crate) fn send_lsp_cancel_request(&mut self, request_id: u64) {
914        let buffer_id = self.active_buffer();
915        let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
916            return;
917        };
918        {
919            let lsp = &mut self.lsp;
920            if let Some(handle) = lsp.get_handle_mut(&language) {
921                if let Err(e) = handle.cancel_request(request_id) {
922                    tracing::warn!("Failed to send LSP cancel request: {}", e);
923                } else {
924                    tracing::debug!("Sent $/cancelRequest for request_id={}", request_id);
925                }
926            }
927        }
928    }
929
930    /// Toggle this window's tab-bar visibility and post a status message.
931    pub fn toggle_tab_bar(&mut self) {
932        self.tab_bar_visible = !self.tab_bar_visible;
933        let key = if self.tab_bar_visible {
934            "toggle.tab_bar_shown"
935        } else {
936            "toggle.tab_bar_hidden"
937        };
938        self.set_status_message(rust_i18n::t!(key).to_string());
939    }
940
941    /// Toggle this window's status-bar visibility and post a status message.
942    pub fn toggle_status_bar(&mut self) {
943        self.status_bar_visible = !self.status_bar_visible;
944        let key = if self.status_bar_visible {
945            "toggle.status_bar_shown"
946        } else {
947            "toggle.status_bar_hidden"
948        };
949        self.set_status_message(rust_i18n::t!(key).to_string());
950    }
951
952    /// Toggle this window's prompt-line visibility and post a status message.
953    pub fn toggle_prompt_line(&mut self) {
954        self.prompt_line_visible = !self.prompt_line_visible;
955        let key = if self.prompt_line_visible {
956            "toggle.prompt_line_shown"
957        } else {
958            "toggle.prompt_line_hidden"
959        };
960        self.set_status_message(rust_i18n::t!(key).to_string());
961    }
962
963    /// Toggle this window's same-buffer scroll-sync flag and post a
964    /// status message announcing the new state.
965    pub fn toggle_scroll_sync(&mut self) {
966        self.same_buffer_scroll_sync = !self.same_buffer_scroll_sync;
967        let key = if self.same_buffer_scroll_sync {
968            "toggle.scroll_sync_enabled"
969        } else {
970            "toggle.scroll_sync_disabled"
971        };
972        self.set_status_message(rust_i18n::t!(key).to_string());
973    }
974
975    /// Toggle the active buffer's `debug_highlight_mode` (shows byte
976    /// positions and highlight-span info on screen). No-op if there is
977    /// no active buffer.
978    pub fn toggle_debug_highlights(&mut self) {
979        let buffer_id = self.active_buffer();
980        if let Some(state) = self.buffers.get_mut(&buffer_id) {
981            state.debug_highlight_mode = !state.debug_highlight_mode;
982            let key = if state.debug_highlight_mode {
983                "toggle.debug_mode_on"
984            } else {
985                "toggle.debug_mode_off"
986            };
987            self.set_status_message(rust_i18n::t!(key).to_string());
988        }
989    }
990
991    /// Build a compiled `regex::Regex` from this window's current
992    /// search-flags (`use_regex`, `whole_word`, `case_sensitive`)
993    /// applied to `query`. Returns the compiled regex or a
994    /// human-readable error string.
995    pub(crate) fn build_search_regex(&self, query: &str) -> Result<regex::Regex, String> {
996        crate::app::regex_replace::build_search_regex(
997            query,
998            self.search_use_regex,
999            self.search_whole_word,
1000            self.search_case_sensitive,
1001        )
1002    }
1003
1004    /// True iff editing should be disabled for the active buffer
1005    /// (e.g. read-only virtual buffers like the help manual).
1006    pub fn is_editing_disabled(&self) -> bool {
1007        self.active_state().editing_disabled
1008    }
1009
1010    /// Recompute the active buffer's `modified` flag from the event log's
1011    /// position relative to its last-saved point. Called after undo/redo
1012    /// to correctly report "buffer is dirty / clean" in the status bar.
1013    pub(super) fn update_modified_from_event_log(&mut self) {
1014        let buffer_id = self.active_buffer();
1015        let is_at_saved = self
1016            .event_logs
1017            .get(&buffer_id)
1018            .map(|log| log.is_at_saved_position())
1019            .unwrap_or(false);
1020        if let Some(state) = self.buffers.get_mut(&buffer_id) {
1021            state.buffer.set_modified(!is_at_saved);
1022        }
1023    }
1024
1025    /// True iff `language` is currently user-dismissed in this window's
1026    /// LSP status pill.
1027    pub fn is_lsp_language_user_dismissed(&self, language: &str) -> bool {
1028        self.user_dismissed_lsp_languages.contains(language)
1029    }
1030
1031    /// Dismiss the LSP pill for `language` in this window until the user
1032    /// re-enables it (or the editor restarts).
1033    pub fn dismiss_lsp_language(&mut self, language: &str) {
1034        self.user_dismissed_lsp_languages
1035            .insert(language.to_string());
1036    }
1037
1038    /// Undo a previous dismissal — the pill for `language` returns to its
1039    /// normal style.
1040    pub fn undismiss_lsp_language(&mut self, language: &str) {
1041        self.user_dismissed_lsp_languages.remove(language);
1042    }
1043
1044    /// True iff at least one LSP server attached to the active buffer's
1045    /// language advertises `codeAction/resolve`.
1046    pub(crate) fn server_supports_code_action_resolve(&self) -> bool {
1047        let Some(language) = self
1048            .buffers
1049            .get(&self.active_buffer())
1050            .map(|s| s.language.clone())
1051        else {
1052            return false;
1053        };
1054        {
1055            let lsp = &self.lsp;
1056            for sh in lsp.get_handles(&language) {
1057                if sh.capabilities.code_action_resolve {
1058                    return true;
1059                }
1060            }
1061        }
1062        false
1063    }
1064
1065    /// True iff at least one LSP server attached to the active buffer's
1066    /// language advertises `completionItem/resolve`.
1067    pub(crate) fn server_supports_completion_resolve(&self) -> bool {
1068        let Some(language) = self
1069            .buffers
1070            .get(&self.active_buffer())
1071            .map(|s| s.language.clone())
1072        else {
1073            return false;
1074        };
1075        {
1076            let lsp = &self.lsp;
1077            for sh in lsp.get_handles(&language) {
1078                if sh.capabilities.completion_resolve {
1079                    return true;
1080                }
1081            }
1082        }
1083        false
1084    }
1085
1086    /// True iff at least one LSP server attached to the active buffer's
1087    /// language advertises `textDocument/rename` (and therefore the
1088    /// `prepareRename` request, which the editor surfaces only through
1089    /// the rename feature flag).
1090    pub(crate) fn server_supports_prepare_rename(&self) -> bool {
1091        let Some(language) = self
1092            .buffers
1093            .get(&self.active_buffer())
1094            .map(|s| s.language.clone())
1095        else {
1096            return false;
1097        };
1098        {
1099            let lsp = &self.lsp;
1100            for sh in lsp.get_handles(&language) {
1101                if sh.capabilities.rename {
1102                    return true;
1103                }
1104            }
1105        }
1106        false
1107    }
1108
1109    /// Send `textDocument/prepareRename` for the symbol at the active
1110    /// cursor. No-op if the buffer has no LSP metadata, no language, or
1111    /// no rename-capable handle. The response is dispatched to
1112    /// `handle_prepare_rename_response`.
1113    pub(crate) fn send_prepare_rename(&mut self) {
1114        let cursor_pos = self.active_cursors().primary().position;
1115        let (line, character) = self
1116            .active_state()
1117            .buffer
1118            .position_to_lsp_position(cursor_pos);
1119
1120        let buffer_id = self.active_buffer();
1121        let metadata = match self.buffer_metadata.get(&buffer_id) {
1122            Some(m) if m.lsp_enabled => m,
1123            _ => return,
1124        };
1125        let uri = match metadata.file_uri() {
1126            Some(u) => u.clone(),
1127            None => return,
1128        };
1129        let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
1130            return;
1131        };
1132
1133        let request_id = self.alloc_lsp_request_id();
1134
1135        {
1136            let lsp = &mut self.lsp;
1137            if let Some(sh) = lsp.handle_for_feature_mut(&language, LspFeature::Rename) {
1138                if let Err(e) = sh.handle.prepare_rename(
1139                    request_id,
1140                    uri.as_uri().clone(),
1141                    line as u32,
1142                    character as u32,
1143                ) {
1144                    tracing::warn!("Failed to send prepareRename: {}", e);
1145                }
1146            }
1147        }
1148    }
1149
1150    /// Send `completionItem/resolve` for `item` to the first LSP server
1151    /// (in language order) that advertises `completion_resolve` for the
1152    /// active buffer's language. No-op if no server is running or no
1153    /// server supports the resolve.
1154    pub(crate) fn send_completion_resolve(&mut self, item: lsp_types::CompletionItem) {
1155        let Some(language) = self
1156            .buffers
1157            .get(&self.active_buffer())
1158            .map(|s| s.language.clone())
1159        else {
1160            return;
1161        };
1162        let request_id = self.alloc_lsp_request_id();
1163        {
1164            let lsp = &mut self.lsp;
1165            for sh in lsp.get_handles_mut(&language) {
1166                if sh.capabilities.completion_resolve {
1167                    if let Err(e) = sh.handle.completion_resolve(request_id, item.clone()) {
1168                        tracing::warn!(
1169                            "Failed to send completionItem/resolve to '{}': {}",
1170                            sh.name,
1171                            e
1172                        );
1173                    }
1174                    return;
1175                }
1176            }
1177        }
1178    }
1179
1180    /// Apply an event to a buffer + the cursors of a split inside this
1181    /// window. Window-level method (not Editor-level) so the borrow
1182    /// checker can split-borrow `self.buffers` and `self.splits`
1183    /// cleanly without inline `self.windows.get_mut(...)` boilerplate
1184    /// at the call site. No-op if the buffer or split is missing.
1185    pub fn apply_event_to_buffer(
1186        &mut self,
1187        buffer_id: BufferId,
1188        split_id: LeafId,
1189        event: &crate::model::event::Event,
1190    ) {
1191        self.buffers
1192            .with_buffer_and_split(buffer_id, split_id, |state, vs| {
1193                state.apply(&mut vs.cursors, event);
1194            });
1195    }
1196
1197    /// Same as [`apply_event_to_buffer`] but operates on a buffer-group
1198    /// panel's keyed cursor (the `keyed_states[buffer_id].cursors`
1199    /// inside the host split's view state, not the host's own cursors).
1200    /// Used by event-apply paths that target a focused inner panel of
1201    /// a Grouped split rather than the outer split's leaf buffer.
1202    pub fn apply_event_to_keyed_buffer(
1203        &mut self,
1204        buffer_id: BufferId,
1205        split_id: LeafId,
1206        event: &crate::model::event::Event,
1207    ) {
1208        self.buffers
1209            .with_buffer_and_split(buffer_id, split_id, |state, vs| {
1210                if let Some(keyed) = vs.keyed_states.get_mut(&buffer_id) {
1211                    state.apply(&mut keyed.cursors, event);
1212                }
1213            });
1214    }
1215
1216    /// Scroll the named split's viewport so the buffer's primary cursor
1217    /// is visible. Calls into `SplitViewState::ensure_cursor_visible`
1218    /// with the buffer's text + marker list. No-op if buffer/split is
1219    /// missing.
1220    pub fn ensure_cursor_visible_for_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
1221        self.buffers
1222            .with_buffer_and_split(buffer_id, split_id, |state, vs| {
1223                vs.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
1224            });
1225    }
1226
1227    /// Scroll a split's viewport to the given line, given a buffer to
1228    /// resolve the line→byte offset. No-op if buffer/split is missing.
1229    /// `lock_against_ensure_visible`: when true, sets the
1230    /// skip-ensure-visible flag so the next render's cursor-visibility
1231    /// pass doesn't undo this scroll. Plugin-driven jumps want true;
1232    /// scroll-sync-from-active-to-other-splits wants false.
1233    pub fn scroll_split_viewport_to(
1234        &mut self,
1235        buffer_id: BufferId,
1236        split_id: LeafId,
1237        target_line: usize,
1238        lock_against_ensure_visible: bool,
1239    ) {
1240        self.buffers
1241            .with_buffer_and_split(buffer_id, split_id, |state, vs| {
1242                vs.viewport.scroll_to(&mut state.buffer, target_line);
1243                if lock_against_ensure_visible {
1244                    vs.viewport.set_skip_ensure_visible();
1245                }
1246            });
1247    }
1248
1249    /// Add a collapsed fold range on `buffer_id`'s marker list and on
1250    /// every view state hosting the buffer. Returns `true` when the
1251    /// buffer was found (so the caller knows to flag a render). No-op
1252    /// when the buffer is missing.
1253    pub fn add_fold(
1254        &mut self,
1255        buffer_id: BufferId,
1256        start: usize,
1257        end: usize,
1258        placeholder: Option<String>,
1259    ) -> bool {
1260        self.buffers
1261            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
1262                for vs in vs_map.values_mut() {
1263                    if vs.keyed_states.contains_key(&buffer_id) {
1264                        let buf_state = vs.ensure_buffer_state(buffer_id);
1265                        buf_state.folds.add(
1266                            &mut state.marker_list,
1267                            start,
1268                            end,
1269                            placeholder.clone(),
1270                        );
1271                    }
1272                }
1273            })
1274            .is_some()
1275    }
1276
1277    /// Clear every fold range on `buffer_id` across the window's view
1278    /// states. Returns `true` when the buffer was found.
1279    pub fn clear_folds(&mut self, buffer_id: BufferId) -> bool {
1280        self.buffers
1281            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
1282                for vs in vs_map.values_mut() {
1283                    if vs.keyed_states.contains_key(&buffer_id) {
1284                        let buf_state = vs.ensure_buffer_state(buffer_id);
1285                        buf_state.folds.clear(&mut state.marker_list);
1286                    }
1287                }
1288            })
1289            .is_some()
1290    }
1291
1292    /// Move every supplied split's primary cursor to `position` in
1293    /// `buffer_id` and re-anchor the viewport to keep it visible.
1294    /// Caller is responsible for computing `splits` (typically by
1295    /// walking the split tree plus any grouped subtrees on the
1296    /// editor — those live outside the window). No-op for missing
1297    /// buffer/splits.
1298    pub fn set_buffer_cursor_in_splits(
1299        &mut self,
1300        buffer_id: BufferId,
1301        position: usize,
1302        splits: &[LeafId],
1303    ) {
1304        self.buffers
1305            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
1306                for leaf_id in splits {
1307                    let Some(view_state) = vs_map.get_mut(leaf_id) else {
1308                        continue;
1309                    };
1310                    view_state.cursors.primary_mut().move_to(position, false);
1311                    view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
1312                }
1313            });
1314    }
1315
1316    /// Scroll `leaf_id`'s viewport so the byte position `top_byte` is
1317    /// the new top line, using `buffer_id` to resolve byte→line. Sets
1318    /// `skip_ensure_visible` so the next render's cursor-visibility
1319    /// pass doesn't undo the plugin-driven scroll. No-op for missing
1320    /// buffer/split.
1321    pub fn set_split_scroll_to_byte(
1322        &mut self,
1323        buffer_id: BufferId,
1324        leaf_id: LeafId,
1325        top_byte: usize,
1326    ) {
1327        self.buffers
1328            .with_buffer_and_split(buffer_id, leaf_id, |state, view_state| {
1329                let total_bytes = state.buffer.len();
1330                let clamped_byte = top_byte.min(total_bytes);
1331                let target_line = state
1332                    .buffer
1333                    .offset_to_position(clamped_byte)
1334                    .map(|p| p.line)
1335                    .unwrap_or(0);
1336                view_state
1337                    .viewport
1338                    .scroll_to(&mut state.buffer, target_line);
1339                view_state.viewport.top_byte = clamped_byte;
1340                view_state.viewport.top_view_line_offset = 0;
1341                view_state.viewport.set_skip_ensure_visible();
1342            });
1343    }
1344
1345    /// Scroll every supplied split so `line` is roughly a third
1346    /// from the top of the viewport, using `buffer_id` for line
1347    /// resolution. Used for plugin-driven "scroll buffer to line"
1348    /// where the caller has already collected target leaves
1349    /// (including those from grouped subtrees).
1350    pub fn scroll_buffer_to_line_in_splits(
1351        &mut self,
1352        buffer_id: BufferId,
1353        target_leaves: &[LeafId],
1354        line: usize,
1355    ) {
1356        self.buffers
1357            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
1358                for leaf_id in target_leaves {
1359                    let Some(view_state) = vs_map.get_mut(leaf_id) else {
1360                        continue;
1361                    };
1362                    let viewport_height = view_state.viewport.height as usize;
1363                    let lines_above = viewport_height / 3;
1364                    let target = line.saturating_sub(lines_above);
1365                    view_state.viewport.scroll_to(&mut state.buffer, target);
1366                    view_state.viewport.set_skip_ensure_visible();
1367                }
1368            });
1369    }
1370
1371    /// Apply a previously-saved cursor + scroll position to a
1372    /// specific buffer's keyed view state inside a specific split.
1373    /// Restoration must NOT go through `view_state.viewport` /
1374    /// `view_state.cursors` — those Deref to the split's *active*
1375    /// buffer's view, which for `open_file_no_focus` is still the
1376    /// previously-active buffer; writing through the Deref would
1377    /// scroll the unrelated active buffer. After restoring the
1378    /// fields, reconciles cursor visibility against viewport
1379    /// (#1689 follow-up). No-op if buffer/split is missing.
1380    pub fn restore_buffer_state_in_split(
1381        &mut self,
1382        buffer_id: BufferId,
1383        split_id: LeafId,
1384        file_state: &crate::workspace::SerializedFileState,
1385    ) {
1386        self.buffers
1387            .with_buffer_and_split(buffer_id, split_id, |buffer_state, vs| {
1388                let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) else {
1389                    return;
1390                };
1391                let max_pos = buffer_state.buffer.len();
1392                let cursor_pos = file_state.cursor.position.min(max_pos);
1393                buf_state.cursors.primary_mut().position = cursor_pos;
1394                buf_state.cursors.primary_mut().anchor =
1395                    file_state.cursor.anchor.map(|a| a.min(max_pos));
1396                buf_state.viewport.top_byte = file_state.scroll.top_byte;
1397                buf_state.viewport.left_column = file_state.scroll.left_column;
1398                crate::app::navigation::reconcile_restored_buffer_view(
1399                    buf_state,
1400                    &mut buffer_state.buffer,
1401                );
1402            });
1403    }
1404
1405    /// Configure `leaf_id`'s viewport for a terminal-buffer
1406    /// scrollback view: disable line wrap, clear any pending
1407    /// skip-ensure-visible flag, then scroll so the buffer's primary
1408    /// cursor (positioned at end-of-buffer when entering scrollback)
1409    /// is visible. No-op if the buffer or split is missing.
1410    pub fn enter_terminal_scrollback_view(&mut self, buffer_id: BufferId, leaf_id: LeafId) {
1411        self.buffers
1412            .with_buffer_and_split(buffer_id, leaf_id, |state, view_state| {
1413                view_state.viewport.line_wrap_enabled = false;
1414                view_state.viewport.clear_skip_ensure_visible();
1415                view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
1416            });
1417    }
1418
1419    /// Install a freshly-loaded `EditorState` for a terminal buffer:
1420    /// replace the slot's state, push every per-split cursor showing
1421    /// the buffer to end-of-buffer (scrollback start), clear the
1422    /// modified flag (terminals are never user-modified), disable
1423    /// editing (scrollback mode), and turn off line-number margins.
1424    /// Used by workspace restore when re-loading the on-disk
1425    /// rendering of a previously-running terminal.
1426    pub fn install_terminal_buffer_state(
1427        &mut self,
1428        buffer_id: BufferId,
1429        new_state: crate::state::EditorState,
1430    ) {
1431        self.buffers
1432            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
1433                *state = new_state;
1434                let total = state.buffer.total_bytes();
1435                for vs in vs_map.values_mut() {
1436                    if vs.has_buffer(buffer_id) {
1437                        vs.cursors.primary_mut().position = total;
1438                        // Disable gutter + current-line highlight for the
1439                        // terminal buffer's per-buffer view state so that
1440                        // exiting terminal mode on a restored terminal
1441                        // doesn't flash a line-number column. The render
1442                        // path overwrites the buffer's margin config from
1443                        // this flag every frame, so the buffer-level
1444                        // `configure_for_line_numbers(false)` below isn't
1445                        // enough on its own.
1446                        let buf_state = vs.ensure_buffer_state(buffer_id);
1447                        buf_state.show_line_numbers = false;
1448                        buf_state.highlight_current_line = false;
1449                        buf_state.viewport.line_wrap_enabled = false;
1450                    }
1451                }
1452                state.buffer.set_modified(false);
1453                state.editing_disabled = true;
1454                state.margins.configure_for_line_numbers(false);
1455            });
1456    }
1457
1458    /// Scroll `leaf_id`'s viewport by `delta` lines (negative = up,
1459    /// positive = down). Honours `view_transform_tokens` when present
1460    /// (uses view-aware scrolling) and falls back to buffer-based
1461    /// `scroll_up` / `scroll_down`. After scrolling, skips
1462    /// ensure_visible and snaps the viewport top to a fold boundary
1463    /// if the new top byte landed inside a collapsed fold.
1464    /// `tab_size` is needed for view-line tokenization.
1465    pub fn scroll_split_by_lines(
1466        &mut self,
1467        buffer_id: BufferId,
1468        leaf_id: LeafId,
1469        delta: i32,
1470        view_transform_tokens: Option<Vec<fresh_core::api::ViewTokenWire>>,
1471        tab_size: usize,
1472    ) {
1473        self.buffers
1474            .with_buffer_and_split(buffer_id, leaf_id, |state, view_state| {
1475                let soft_breaks = state.collect_soft_break_positions();
1476                let virtual_lines = state.collect_virtual_line_positions();
1477                let buffer = &mut state.buffer;
1478                let top_byte_before = view_state.viewport.top_byte;
1479                if let Some(tokens) = view_transform_tokens {
1480                    use crate::view::ui::view_pipeline::ViewLineIterator;
1481                    let view_lines: Vec<_> =
1482                        ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
1483                    view_state
1484                        .viewport
1485                        .scroll_view_lines(&view_lines, delta as isize);
1486                } else if delta < 0 {
1487                    let lines_to_scroll = delta.unsigned_abs() as usize;
1488                    view_state.viewport.scroll_up(
1489                        buffer,
1490                        &soft_breaks,
1491                        &virtual_lines,
1492                        lines_to_scroll,
1493                    );
1494                } else {
1495                    let lines_to_scroll = delta as usize;
1496                    view_state.viewport.scroll_down(
1497                        buffer,
1498                        &soft_breaks,
1499                        &virtual_lines,
1500                        lines_to_scroll,
1501                    );
1502                }
1503                view_state.viewport.set_skip_ensure_visible();
1504
1505                if let Some(folds) = view_state.keyed_states.get(&buffer_id).map(|bs| &bs.folds) {
1506                    if !folds.is_empty() {
1507                        let top_line = buffer.get_line_number(view_state.viewport.top_byte);
1508                        if let Some(range) = folds
1509                            .resolved_ranges(buffer, &state.marker_list)
1510                            .iter()
1511                            .find(|r| top_line >= r.start_line && top_line <= r.end_line)
1512                        {
1513                            let target_line = if delta >= 0 {
1514                                range.end_line.saturating_add(1)
1515                            } else {
1516                                range.header_line
1517                            };
1518                            let target_byte = buffer
1519                                .line_start_offset(target_line)
1520                                .unwrap_or_else(|| buffer.len());
1521                            view_state.viewport.top_byte = target_byte;
1522                            view_state.viewport.top_view_line_offset = 0;
1523                        }
1524                    }
1525                }
1526                tracing::trace!(
1527                    "scroll_split_by_lines: delta={}, top_byte {} -> {}",
1528                    delta,
1529                    top_byte_before,
1530                    view_state.viewport.top_byte
1531                );
1532            });
1533    }
1534
1535    /// Scroll the Live Grep overlay's preview pane by `delta` lines
1536    /// (issue #2119). The preview lives in `overlay_preview_state` (not in
1537    /// the split tree), so it needs its own scroll path rather than going
1538    /// through `scroll_split_by_lines`. Returns true if a preview was present
1539    /// and scrolled.
1540    pub fn scroll_overlay_preview_by_lines(&mut self, delta: i32) -> bool {
1541        let buffer_id = match self.overlay_preview_state.as_ref() {
1542            Some(ps) if !ps.blanked => ps.buffer_id,
1543            _ => return false,
1544        };
1545        // Gather buffer-derived inputs first so the mutable borrows below stay
1546        // disjoint (buffer store vs. overlay preview state).
1547        let (soft_breaks, virtual_lines) = match self.buffers.get(&buffer_id) {
1548            Some(s) => (
1549                s.collect_soft_break_positions(),
1550                s.collect_virtual_line_positions(),
1551            ),
1552            None => return false,
1553        };
1554        let Some(state) = self.buffers.get_mut(&buffer_id) else {
1555            return false;
1556        };
1557        let buffer = &mut state.buffer;
1558        let Some(ps) = self.overlay_preview_state.as_mut() else {
1559            return false;
1560        };
1561        let viewport = &mut ps.view_state.active_state_mut().viewport;
1562        if delta < 0 {
1563            viewport.scroll_up(
1564                buffer,
1565                &soft_breaks,
1566                &virtual_lines,
1567                delta.unsigned_abs() as usize,
1568            );
1569        } else {
1570            viewport.scroll_down(buffer, &soft_breaks, &virtual_lines, delta as usize);
1571        }
1572        viewport.set_skip_ensure_visible();
1573        true
1574    }
1575
1576    /// Clear LSP-related overlays (diagnostics, virtual texts,
1577    /// folding ranges, and folds) for `buffer_id`, used when LSP is
1578    /// being disabled for the buffer. Pure window-state mutation.
1579    pub fn clear_lsp_overlays_for_buffer(
1580        &mut self,
1581        buffer_id: BufferId,
1582        diagnostic_namespace: &crate::model::event::OverlayNamespace,
1583    ) {
1584        self.buffers
1585            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
1586                state
1587                    .overlays
1588                    .clear_namespace(diagnostic_namespace, &mut state.marker_list);
1589                state.virtual_texts.clear(&mut state.marker_list);
1590                state.folding_ranges.clear(&mut state.marker_list);
1591                for view_state in vs_map.values_mut() {
1592                    if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
1593                        buf_state.folds.clear(&mut state.marker_list);
1594                    }
1595                }
1596            });
1597    }
1598
1599    /// Mutable handle to this window's split tree (or `None` when
1600    /// the layout hasn't been seeded yet). Useful at sites where
1601    /// the caller already has a `&mut Window` from a direct
1602    /// `self.windows.get_mut(&id)` and wants the split layout
1603    /// without going back through Editor's accessor.
1604    pub fn split_manager_mut(&mut self) -> Option<&mut SplitManager> {
1605        self.buffers.split_manager_mut()
1606    }
1607
1608    /// Mutable handle to this window's per-leaf view state map.
1609    pub fn split_view_states_mut(&mut self) -> Option<&mut HashMap<LeafId, SplitViewState>> {
1610        self.buffers.split_view_states_mut()
1611    }
1612
1613    /// Both halves of the split layout at once. Returns `None` if
1614    /// the layout hasn't been seeded yet.
1615    pub fn splits_mut(
1616        &mut self,
1617    ) -> Option<(&mut SplitManager, &mut HashMap<LeafId, SplitViewState>)> {
1618        self.buffers.splits_mut().map(|(m, vs)| (m, vs))
1619    }
1620
1621    /// Construct a window.
1622    ///
1623    /// `root` is taken as-is (the caller is responsible for
1624    /// canonicalisation). `label` defaults to the basename of
1625    /// `root` when empty. `resources` is the editor-global service
1626    /// bundle every window holds an `Arc`-cloned reference to — see
1627    /// [`WindowResources`] for the rationale.
1628    pub fn new(
1629        id: WindowId,
1630        label: impl Into<String>,
1631        root: PathBuf,
1632        resources: WindowResources,
1633    ) -> Self {
1634        let mut label = label.into();
1635        if label.is_empty() {
1636            label = root
1637                .file_name()
1638                .and_then(|n| n.to_str())
1639                .map(str::to_owned)
1640                .unwrap_or_else(|| "main".to_owned());
1641        }
1642        // Seed every poll/throttle timestamp with the *editor's* time
1643        // source rather than real wall-clock — otherwise tests using
1644        // `TestTimeSource::advance` see a misaligned baseline and
1645        // `elapsed_since` returns less than the configured interval
1646        // (broke auto-save / auto-recovery tests after these fields
1647        // moved off `Editor`).
1648        let now = resources.time_source.now();
1649        // Build this window's bridge and LSP manager up front so the
1650        // manager is wired to the window's own channel and present by
1651        // construction (see `build_window_lsp`). `&root`/`&resources`
1652        // are borrowed here, then moved into the struct below.
1653        let bridge = crate::services::async_bridge::AsyncBridge::new();
1654        let lsp = build_window_lsp(id, &root, &resources, &bridge);
1655        Self {
1656            id,
1657            label,
1658            root,
1659            file_explorer: None,
1660            file_mod_times: HashMap::new(),
1661            plugin_state: HashMap::new(),
1662            lsp,
1663            panel_ids: HashMap::new(),
1664            buffers: WindowBuffers::new(),
1665            buffer_metadata: HashMap::new(),
1666            terminal_manager: crate::services::terminal::TerminalManager::new(id),
1667            terminal_buffers: HashMap::new(),
1668            terminal_backing_files: HashMap::new(),
1669            terminal_log_files: HashMap::new(),
1670            terminal_explicit_titles: std::collections::HashSet::new(),
1671            terminal_fg_poll_at: None,
1672            terminal_fg_cache: HashMap::new(),
1673            event_logs: HashMap::new(),
1674            status_message: None,
1675            plugin_status_message: None,
1676            prompt: None,
1677            bridge,
1678            next_lsp_request_id: 0,
1679            pending_completion_requests: std::collections::HashSet::new(),
1680            completion_items: None,
1681            scheduled_completion_trigger: None,
1682            dabbrev_state: None,
1683            pending_goto_definition_request: None,
1684            pending_references_request: None,
1685            pending_references_symbol: String::new(),
1686            pending_signature_help_request: None,
1687            pending_code_actions_requests: std::collections::HashSet::new(),
1688            pending_code_actions_server_names: std::collections::HashMap::new(),
1689            pending_code_actions: None,
1690            pending_inlay_hints_requests: std::collections::HashMap::new(),
1691            pending_folding_range_requests: std::collections::HashMap::new(),
1692            folding_ranges_in_flight: std::collections::HashMap::new(),
1693            folding_ranges_debounce: std::collections::HashMap::new(),
1694            pending_semantic_token_requests: std::collections::HashMap::new(),
1695            semantic_tokens_in_flight: std::collections::HashMap::new(),
1696            semantic_tokens_full_debounce: std::collections::HashMap::new(),
1697            pending_semantic_token_range_requests: std::collections::HashMap::new(),
1698            semantic_tokens_range_in_flight: std::collections::HashMap::new(),
1699            semantic_tokens_range_last_request: std::collections::HashMap::new(),
1700            semantic_tokens_range_applied: std::collections::HashMap::new(),
1701            position_history: crate::input::position_history::PositionHistory::new(),
1702            in_navigation: false,
1703            suppress_position_history_once: false,
1704            bookmarks: crate::app::bookmarks::BookmarkState::default(),
1705            grouped_subtrees: HashMap::new(),
1706            composite_buffers: HashMap::new(),
1707            composite_view_states: HashMap::new(),
1708            layout_cache: WindowLayoutCache::default(),
1709            chrome_layout: ChromeLayout::default(),
1710            terminal_width: 80,
1711            terminal_height: 24,
1712            dock_cols: 0,
1713            preview: None,
1714            terminal_mode: false,
1715            terminal_mode_resume: std::collections::HashSet::new(),
1716            seen_byte_ranges: HashMap::new(),
1717            previous_viewports: HashMap::new(),
1718            same_buffer_scroll_sync: false,
1719            interactive_replace_state: None,
1720            scroll_sync_manager: crate::view::scroll_sync::ScrollSyncManager::new(),
1721            file_explorer_visible: false,
1722            file_explorer_sync_in_progress: false,
1723            file_explorer_width: resources.config.file_explorer.width,
1724            file_explorer_side: resources.config.file_explorer.side,
1725            pending_file_explorer_show_hidden: None,
1726            pending_file_explorer_show_gitignored: None,
1727            file_explorer_decorations: HashMap::new(),
1728            file_explorer_decoration_cache:
1729                crate::view::file_tree::FileExplorerDecorationCache::default(),
1730            hover: crate::app::hover::HoverState::default(),
1731            search_state: None,
1732            search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1733                "search".to_string(),
1734            ),
1735            pending_search_range: None,
1736            live_grep_last_state: None,
1737            overlay_preview_state: None,
1738            auto_revert_enabled: true,
1739            file_rapid_change_counts: HashMap::new(),
1740            goto_line_preview: None,
1741            pending_async_prompt_callback: None,
1742            pending_quit_unnamed_save: Vec::new(),
1743            search_case_sensitive: true,
1744            search_whole_word: false,
1745            search_use_regex: false,
1746            search_confirm_each: false,
1747            scheduled_diagnostic_pull: None,
1748            scheduled_inlay_hints_request: None,
1749            user_dismissed_lsp_languages: std::collections::HashSet::new(),
1750            editor_mode: None,
1751            prompt_histories: HashMap::new(),
1752            pending_close_buffer: None,
1753            completion_service: crate::services::completion::CompletionService::new(),
1754            lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1755                "lsp-diagnostic".to_string(),
1756            ),
1757            diagnostic_result_ids: HashMap::new(),
1758            lsp_progress: HashMap::new(),
1759            lsp_server_statuses: HashMap::new(),
1760            lsp_menu_contributions: HashMap::new(),
1761            lsp_window_messages: Vec::new(),
1762            lsp_log_messages: Vec::new(),
1763            stored_push_diagnostics: HashMap::new(),
1764            stored_pull_diagnostics: HashMap::new(),
1765            stored_diagnostics: Arc::new(HashMap::new()),
1766            stored_folding_ranges: Arc::new(HashMap::new()),
1767            dir_mod_times: HashMap::new(),
1768            last_auto_revert_poll: now,
1769            last_file_tree_poll: now,
1770            git_index_resolved: false,
1771            pending_file_poll_rx: None,
1772            pending_dir_poll_rx: None,
1773            ephemeral_terminals: std::collections::HashSet::new(),
1774            plugin_dev_workspaces: HashMap::new(),
1775            status_bar_values: HashMap::new(),
1776            mouse_state: crate::app::types::MouseState::default(),
1777            key_context: crate::input::keybindings::KeyContext::Normal,
1778            chord_state: Vec::new(),
1779            previous_click_time: None,
1780            previous_click_position: None,
1781            click_count: 0,
1782            mouse_enabled: false,
1783            mouse_cursor_position: None,
1784            gpm_active: false,
1785            menu_bar_visible: resources.config.editor.show_menu_bar,
1786            menu_bar_auto_shown: false,
1787            tab_bar_visible: resources.config.editor.show_tab_bar,
1788            status_bar_visible: resources.config.editor.show_status_bar,
1789            prompt_line_visible: resources.config.editor.show_prompt_line,
1790            last_auto_recovery_save: now,
1791            last_persistent_auto_save: now,
1792            warning_domains: crate::app::warning_domains::WarningDomainRegistry::default(),
1793            tab_context_menu: None,
1794            file_explorer_context_menu: None,
1795            theme_info_popup: None,
1796            event_debug: None,
1797            file_open_state: None,
1798            file_browser_layout: None,
1799            buffer_groups: HashMap::new(),
1800            buffer_to_group: HashMap::new(),
1801            next_buffer_group_id: 0,
1802            pending_next_key_callbacks: std::collections::VecDeque::new(),
1803            key_capture_active: false,
1804            pending_key_capture_buffer: std::collections::VecDeque::new(),
1805            macros: crate::app::macros::MacroState::default(),
1806            active_custom_contexts: std::collections::HashSet::new(),
1807            keyboard_capture: false,
1808            review_hunks: Vec::new(),
1809            pending_file_opens: Vec::new(),
1810            pending_hot_exit_recovery: false,
1811            wait_tracking: HashMap::new(),
1812            completed_waits: Vec::new(),
1813            line_scan: crate::app::line_scan::LineScan::default(),
1814            search_scan: crate::app::search_scan::SearchScan::default(),
1815            search_overlay_top_byte: None,
1816            animations: crate::view::animation::AnimationRunner::default(),
1817            plugin_errors: Vec::new(),
1818            file_explorer_clipboard: None,
1819            process_groups: ProcessGroups::default(),
1820            resources,
1821        }
1822    }
1823
1824    // ---- Resource accessors (canonical reading API) ----
1825    //
1826    // These are thin wrappers around `self.resources.X` for the most
1827    // commonly-read resources. Use them at sites where the borrow
1828    // checker is happy with a method call; fall back to direct
1829    // `self.resources.X` field access at sites that need to split-borrow
1830    // alongside other Window sub-fields.
1831
1832    /// Read-only handle to editor configuration.
1833    pub fn config(&self) -> &crate::config::Config {
1834        &self.resources.config
1835    }
1836
1837    /// Active filesystem authority (local / devcontainer / remote).
1838    pub fn authority(&self) -> &crate::services::authority::Authority {
1839        &self.resources.authority
1840    }
1841
1842    /// Allocate the next globally-unique `BufferId`.
1843    pub fn alloc_buffer_id(&self) -> BufferId {
1844        self.resources.buffer_id_alloc.next()
1845    }
1846
1847    /// Set this window's status-bar message. Mirrors
1848    /// `Editor::set_status_message` — moved here so handlers on
1849    /// `impl Window` can post status without an `Editor` reference.
1850    /// Clears any plugin-supplied status (matches Editor behaviour).
1851    pub fn set_status_message(&mut self, message: String) {
1852        tracing::info!(target: "status", "{}", message);
1853        self.plugin_status_message = None;
1854        self.status_message = Some(message);
1855    }
1856
1857    /// Clear this window's status-bar message.
1858    pub fn clear_status_message(&mut self) {
1859        self.status_message = None;
1860    }
1861
1862    /// Resolve the effective (split, buffer) pair for the currently-
1863    /// focused target inside this window. Returned invariant: the split
1864    /// id is in `splits.1` (view_states), its `active_buffer` equals
1865    /// the returned buffer id, `self.buffers` contains the buffer id,
1866    /// and the split's `keyed_states` contains an entry for the buffer.
1867    ///
1868    /// Falls back to the outer split when a buffer-group panel is
1869    /// focused but any of those invariants doesn't hold for the inner
1870    /// leaf. Mirrors `Editor::effective_active_pair`.
1871    pub fn effective_active_pair(&self) -> (LeafId, BufferId) {
1872        let (mgr, vs_map) = self
1873            .buffers
1874            .splits()
1875            .expect("active window must have a populated split layout");
1876        let active_split = mgr.active_split();
1877        if let Some(vs) = vs_map.get(&active_split) {
1878            if vs.active_group_tab.is_some() {
1879                if let Some(inner_leaf) = vs.focused_group_leaf {
1880                    if let Some(inner_vs) = vs_map.get(&inner_leaf) {
1881                        let inner_buf = inner_vs.active_buffer;
1882                        if self.buffers.get(&inner_buf).is_some()
1883                            && inner_vs.keyed_states.contains_key(&inner_buf)
1884                        {
1885                            return (inner_leaf, inner_buf);
1886                        }
1887                    }
1888                }
1889            }
1890        }
1891        let outer_buf = mgr
1892            .active_buffer_id()
1893            .expect("Editor always has at least one buffer");
1894        // Validate against `self.buffers` — the group-tab branch above
1895        // already does this for its return; the outer fallback used to
1896        // skip the check and any caller that then did
1897        // `self.buffers.get(&active_buf).unwrap()` would panic. Issue
1898        // #1939: `set_pane_buffer` writes the leaf's `buffer_id` +
1899        // `vs.active_buffer` without touching `vs.open_buffers`, so
1900        // `clean_orphaned_buffers` (which filters by `buffer_tab_ids`)
1901        // can remove a buffer the split manager still points at.
1902        // When that happens, fall back to any live buffer and warn
1903        // loudly — the split manager pointer is stale until something
1904        // repairs it, and we want the underlying state corruption
1905        // visible in logs even though render itself no longer crashes.
1906        if self.buffers.get(&outer_buf).is_some() {
1907            (active_split, outer_buf)
1908        } else if let Some(any) = self.buffers.find_id(|_, _| true) {
1909            tracing::warn!(
1910                stale_buffer_id = ?outer_buf,
1911                fallback_buffer_id = ?any,
1912                active_split = ?active_split,
1913                "effective_active_pair: split manager's active leaf points at \
1914                 a BufferId missing from window.buffers (issue #1939). Falling \
1915                 back to any live buffer; the split tree is in an inconsistent \
1916                 state and should be repaired"
1917            );
1918            (active_split, any)
1919        } else {
1920            // `self.buffers` empty: a bigger invariant violation than
1921            // this helper can recover from. Preserve old behaviour so
1922            // the panic surfaces at the next `.unwrap()` site.
1923            tracing::error!(
1924                stale_buffer_id = ?outer_buf,
1925                active_split = ?active_split,
1926                "effective_active_pair: window.buffers is empty AND the split \
1927                 manager has a stale active buffer — no recovery possible, \
1928                 next render will panic"
1929            );
1930            (active_split, outer_buf)
1931        }
1932    }
1933
1934    /// The id of the buffer currently focused in this window.
1935    #[inline]
1936    pub fn active_buffer(&self) -> BufferId {
1937        let (_, buf) = self.effective_active_pair();
1938        buf
1939    }
1940
1941    /// Width available for tabs in this window. When the file explorer is
1942    /// visible the tabs row only spans the editor area; otherwise it spans
1943    /// the full terminal width.
1944    pub fn effective_tabs_width(&self) -> u16 {
1945        // Start from the chrome left after the editor-global dock, then
1946        // subtract the file explorer — same carve-out order as the
1947        // renderer and `editor_content_area`, so tab-scroll math matches
1948        // the width the tabs actually paint into when the dock is shown.
1949        let chrome = self.terminal_width.saturating_sub(self.dock_cols);
1950        if self.file_explorer_visible && self.file_explorer.is_some() {
1951            let explorer = self.file_explorer_width.to_cols(chrome);
1952            chrome.saturating_sub(explorer)
1953        } else {
1954            chrome
1955        }
1956    }
1957
1958    /// The split id whose `SplitViewState` owns the currently-focused
1959    /// cursors/viewport for this window.
1960    #[inline]
1961    pub fn effective_active_split(&self) -> LeafId {
1962        let (split, _) = self.effective_active_pair();
1963        split
1964    }
1965
1966    /// Read-only handle to this window's active buffer state. Panics
1967    /// if the active buffer is missing — the invariants on
1968    /// `effective_active_pair` guarantee it's present.
1969    pub fn active_state(&self) -> &crate::state::EditorState {
1970        let buf = self.active_buffer();
1971        self.buffers
1972            .get(&buf)
1973            .expect("active buffer must be present in window")
1974    }
1975
1976    /// Mutable handle to this window's active buffer state.
1977    pub fn active_state_mut(&mut self) -> &mut crate::state::EditorState {
1978        let buf = self.active_buffer();
1979        self.buffers
1980            .get_mut(&buf)
1981            .expect("active buffer must be present in window")
1982    }
1983
1984    /// Mutable handle to a specific buffer's editor state, if it is loaded in this window.
1985    pub fn buffer_state_mut(&mut self, id: BufferId) -> Option<&mut crate::state::EditorState> {
1986        self.buffers.get_mut(&id)
1987    }
1988
1989    /// Read-only handle to a specific buffer's editor state, if it is loaded in this window.
1990    pub fn buffer_state(&self, id: BufferId) -> Option<&crate::state::EditorState> {
1991        self.buffers.get(&id)
1992    }
1993
1994    /// Read-only cursor set for the active buffer in the active split.
1995    /// Group panels return their own cursors, not the outer split's
1996    /// stale ones.
1997    pub fn active_cursors(&self) -> &crate::model::cursor::Cursors {
1998        let split_id = self.effective_active_split();
1999        &self
2000            .buffers
2001            .splits()
2002            .expect("active window must have a populated split layout")
2003            .1
2004            .get(&split_id)
2005            .expect("active split must be in view-state map")
2006            .cursors
2007    }
2008
2009    /// Mutable cursor set for the active buffer in the active split.
2010    pub fn active_cursors_mut(&mut self) -> &mut crate::model::cursor::Cursors {
2011        let split_id = self.effective_active_split();
2012        &mut self
2013            .buffers
2014            .splits_mut()
2015            .expect("active window must have a populated split layout")
2016            .1
2017            .get_mut(&split_id)
2018            .expect("active split must be in view-state map")
2019            .cursors
2020    }
2021
2022    /// Read-only event log for the active buffer.
2023    pub fn active_event_log(&self) -> &crate::model::event::EventLog {
2024        let buf = self.active_buffer();
2025        self.event_logs
2026            .get(&buf)
2027            .expect("active buffer must have an event log")
2028    }
2029
2030    /// Mutable event log for the active buffer.
2031    pub fn active_event_log_mut(&mut self) -> &mut crate::model::event::EventLog {
2032        let buf = self.active_buffer();
2033        self.event_logs
2034            .get_mut(&buf)
2035            .expect("active buffer must have an event log")
2036    }
2037
2038    // ---- Preview-tab methods ----
2039
2040    /// Promote a specific buffer from preview to permanent, if it was
2041    /// in preview mode. No-op if the buffer is not currently a preview.
2042    pub fn promote_buffer_from_preview(&mut self, buffer_id: BufferId) {
2043        if let Some(m) = self.buffer_metadata.get_mut(&buffer_id) {
2044            m.is_preview = false;
2045        }
2046        if let Some((_, id)) = self.preview {
2047            if id == buffer_id {
2048                self.preview = None;
2049            }
2050        }
2051    }
2052
2053    /// Promote the active buffer from preview to permanent. Called on
2054    /// any buffer mutation so touching a preview buffer commits it.
2055    pub fn promote_active_buffer_from_preview(&mut self) {
2056        let id = self.active_buffer();
2057        self.promote_buffer_from_preview(id);
2058    }
2059
2060    /// Promote the current preview, regardless of which buffer it
2061    /// points at. Used before layout changes (split, close-split,
2062    /// move-tab) where the preview invariant ("anchored to a specific
2063    /// split") would otherwise be broken by the operation itself.
2064    pub fn promote_current_preview(&mut self) {
2065        if let Some((_, id)) = self.preview.take() {
2066            if let Some(m) = self.buffer_metadata.get_mut(&id) {
2067                m.is_preview = false;
2068            }
2069        }
2070    }
2071
2072    /// Promote the current preview if it belongs to a split other
2073    /// than `new_split`. Called from split-focus-change paths so
2074    /// that moving focus away from the preview's pane commits it.
2075    pub fn promote_preview_if_not_in_split(&mut self, new_split: LeafId) {
2076        if let Some((preview_split, _)) = self.preview {
2077            if preview_split != new_split {
2078                self.promote_current_preview();
2079            }
2080        }
2081    }
2082
2083    /// Whether the given buffer is currently in preview (ephemeral)
2084    /// mode. Primarily for tests; production code reads
2085    /// `self.preview` or relies on the `is_preview` flag in the
2086    /// buffer's metadata.
2087    pub fn is_buffer_preview(&self, buffer_id: BufferId) -> bool {
2088        self.buffer_metadata
2089            .get(&buffer_id)
2090            .map(|m| m.is_preview)
2091            .unwrap_or(false)
2092    }
2093
2094    /// The (split, buffer) tuple of the current preview tab, if any.
2095    /// Intended for tests that verify preview anchoring semantics.
2096    pub fn current_preview(&self) -> Option<(LeafId, BufferId)> {
2097        self.preview
2098    }
2099
2100    // ---- Terminal-buffer query helpers ----
2101
2102    /// Check if a buffer is a terminal buffer (in this window).
2103    pub fn is_terminal_buffer(&self, buffer_id: BufferId) -> bool {
2104        self.terminal_buffers.contains_key(&buffer_id)
2105    }
2106
2107    /// Get the terminal ID for a buffer (if it's a terminal buffer in
2108    /// this window).
2109    pub fn get_terminal_id(
2110        &self,
2111        buffer_id: BufferId,
2112    ) -> Option<crate::services::terminal::TerminalId> {
2113        self.terminal_buffers.get(&buffer_id).copied()
2114    }
2115
2116    /// Clear the visual search overlays for the active buffer,
2117    /// preserving search state so F3/Shift+F3 still work.
2118    pub fn clear_search_overlays(&mut self) {
2119        let ns = self.search_namespace.clone();
2120        let state = self.active_state_mut();
2121        state.overlays.clear_namespace(&ns, &mut state.marker_list);
2122    }
2123
2124    /// Clear all search highlights from the active buffer and reset
2125    /// search state.
2126    pub fn clear_search_highlights(&mut self) {
2127        self.clear_search_overlays();
2128        self.search_state = None;
2129    }
2130
2131    /// List the languages with currently-running LSP server handles in
2132    /// this window. Wraps `LspManager::running_servers`.
2133    pub fn running_lsp_servers(&self) -> Vec<String> {
2134        self.lsp.running_servers()
2135    }
2136
2137    /// Number of in-flight completion requests for this window.
2138    pub fn pending_completion_requests_count(&self) -> usize {
2139        self.pending_completion_requests.len()
2140    }
2141
2142    /// Number of stored completion items currently visible in this
2143    /// window's completion popup.
2144    pub fn completion_items_count(&self) -> usize {
2145        self.completion_items.as_ref().map_or(0, |v| v.len())
2146    }
2147
2148    /// Number of initialized (handshake-complete) LSP servers for
2149    /// `language` in this window.
2150    pub fn initialized_lsp_server_count(&self, language: &str) -> usize {
2151        self.lsp
2152            .get_handles(language)
2153            .iter()
2154            .filter(|sh| sh.capabilities.initialized)
2155            .count()
2156    }
2157
2158    /// Shutdown the LSP server for `language` in this window (marks it
2159    /// disabled until manual restart). Returns true if a server was
2160    /// shutdown, false if no server was running for that language.
2161    pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
2162        self.lsp.shutdown_server(language)
2163    }
2164
2165    /// Enable event-log streaming to `path` for every buffer's event
2166    /// log in this window.
2167    pub fn enable_event_streaming<P: AsRef<std::path::Path>>(
2168        &mut self,
2169        path: P,
2170    ) -> anyhow::Result<()> {
2171        for event_log in self.event_logs.values_mut() {
2172            event_log.enable_streaming(&path)?;
2173        }
2174        Ok(())
2175    }
2176
2177    /// Log a keystroke against the active buffer's event log. No-op if
2178    /// the active buffer has no log entry.
2179    pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
2180        let buffer_id = self.active_buffer();
2181        if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
2182            event_log.log_keystroke(key_code, modifiers);
2183        }
2184    }
2185
2186    /// Check if LSP has any active progress tasks (e.g., indexing) in
2187    /// this window.
2188    pub fn has_active_lsp_progress(&self) -> bool {
2189        !self.lsp_progress.is_empty()
2190    }
2191
2192    /// Snapshot of the current LSP progress entries for this window:
2193    /// `(token, title, message)` tuples.
2194    pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
2195        self.lsp_progress
2196            .iter()
2197            .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
2198            .collect()
2199    }
2200
2201    /// Check if any LSP server for `language` is running in this
2202    /// window. Includes servers registered under another language whose
2203    /// scope accepts `language` (universal servers).
2204    pub fn is_lsp_server_ready(&self, language: &str) -> bool {
2205        use crate::services::async_bridge::LspServerStatus;
2206        self.lsp_server_statuses
2207            .iter()
2208            .any(|((lang, server_name), status)| {
2209                if !matches!(status, LspServerStatus::Running) {
2210                    return false;
2211                }
2212                if lang == language {
2213                    return true;
2214                }
2215                self.lsp
2216                    .server_scope(server_name)
2217                    .map(|scope| scope.accepts(language))
2218                    .unwrap_or(false)
2219            })
2220    }
2221
2222    /// If the active leaf carries `SplitRole::UtilityDock`, move the
2223    /// active leaf back to the user's last regular editor leaf (or any
2224    /// non-dock leaf as a fallback). Called from the file-open path so
2225    /// that opening a file while a utility panel holds focus doesn't
2226    /// turn the dock into a tab strip for ordinary files.
2227    pub fn redirect_active_split_away_from_dock_if_needed(&mut self) {
2228        use crate::view::split::SplitRole;
2229        let Some((mgr, _)) = self.buffers.splits() else {
2230            return;
2231        };
2232        let active = mgr.active_split();
2233        if mgr.leaf_role(active) != Some(SplitRole::UtilityDock) {
2234            return;
2235        }
2236        let is_editor_leaf = |leaf| mgr.leaf_role(leaf) != Some(SplitRole::UtilityDock);
2237        let target = mgr.last_focused_where(is_editor_leaf).or_else(|| {
2238            mgr.root()
2239                .leaf_split_ids()
2240                .into_iter()
2241                .find(|leaf| is_editor_leaf(*leaf))
2242        });
2243        let Some(target) = target else {
2244            return;
2245        };
2246        if target == active {
2247            return;
2248        }
2249        self.split_manager_mut()
2250            .expect("active window must have a populated split layout")
2251            .set_active_split(target);
2252    }
2253
2254    /// Restore per-file state (cursors, scroll, etc.) for a buffer in a
2255    /// specific split, lazily loaded from disk via
2256    /// `PersistedFileWorkspace::load`. No-op if there's no saved state
2257    /// for this path.
2258    pub fn restore_global_file_state(
2259        &mut self,
2260        buffer_id: BufferId,
2261        path: &std::path::Path,
2262        split_id: LeafId,
2263    ) {
2264        use crate::workspace::PersistedFileWorkspace;
2265
2266        let file_state = match PersistedFileWorkspace::load(path) {
2267            Some(state) => state,
2268            None => return,
2269        };
2270
2271        self.restore_buffer_state_in_split(buffer_id, split_id, &file_state);
2272    }
2273
2274    /// Save file state when a buffer is closed (for per-file session
2275    /// persistence). Walks this window's splits to find one that has
2276    /// the buffer; no-op if no split contains it or the buffer isn't
2277    /// a real on-disk file.
2278    pub fn save_file_state_on_close(&self, buffer_id: BufferId) {
2279        use crate::workspace::{
2280            PersistedFileWorkspace, SerializedCursor, SerializedFileState, SerializedScroll,
2281        };
2282
2283        let abs_path = match self.buffer_metadata.get(&buffer_id) {
2284            Some(metadata) => match metadata.file_path() {
2285                Some(path) => path.to_path_buf(),
2286                None => return,
2287            },
2288            None => return,
2289        };
2290
2291        let view_state = self
2292            .buffers
2293            .splits()
2294            .expect("active window must have a populated split layout")
2295            .1
2296            .values()
2297            .find(|vs| vs.has_buffer(buffer_id));
2298
2299        let view_state = match view_state {
2300            Some(vs) => vs,
2301            None => return,
2302        };
2303
2304        let buf_state = match view_state.keyed_states.get(&buffer_id) {
2305            Some(bs) => bs,
2306            None => return,
2307        };
2308
2309        let primary_cursor = buf_state.cursors.primary();
2310        let file_state = SerializedFileState {
2311            cursor: SerializedCursor {
2312                position: primary_cursor.position,
2313                anchor: primary_cursor.anchor,
2314                sticky_column: primary_cursor.sticky_column,
2315            },
2316            additional_cursors: buf_state
2317                .cursors
2318                .iter()
2319                .skip(1)
2320                .map(|(_, cursor)| SerializedCursor {
2321                    position: cursor.position,
2322                    anchor: cursor.anchor,
2323                    sticky_column: cursor.sticky_column,
2324                })
2325                .collect(),
2326            scroll: SerializedScroll {
2327                top_byte: buf_state.viewport.top_byte,
2328                top_view_line_offset: buf_state.viewport.top_view_line_offset,
2329                left_column: buf_state.viewport.left_column,
2330            },
2331            view_mode: Default::default(),
2332            compose_width: None,
2333            plugin_state: std::collections::HashMap::new(),
2334            folds: Vec::new(),
2335        };
2336
2337        PersistedFileWorkspace::save(&abs_path, file_state);
2338        tracing::debug!("Saved file state on close for {:?}", abs_path);
2339    }
2340
2341    /// Remove a pending semantic-token request from this window's tracking maps.
2342    pub(crate) fn take_pending_semantic_token_request(
2343        &mut self,
2344        request_id: u64,
2345    ) -> Option<crate::app::SemanticTokenFullRequest> {
2346        if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
2347            self.semantic_tokens_in_flight.remove(&request.buffer_id);
2348            Some(request)
2349        } else {
2350            None
2351        }
2352    }
2353
2354    /// Remove a pending semantic-token range request from this window's tracking maps.
2355    pub(crate) fn take_pending_semantic_token_range_request(
2356        &mut self,
2357        request_id: u64,
2358    ) -> Option<crate::app::SemanticTokenRangeRequest> {
2359        if let Some(request) = self
2360            .pending_semantic_token_range_requests
2361            .remove(&request_id)
2362        {
2363            self.semantic_tokens_range_in_flight
2364                .remove(&request.buffer_id);
2365            Some(request)
2366        } else {
2367            None
2368        }
2369    }
2370
2371    /// Move the cursor to a visible position within the current viewport.
2372    /// Called after scrollbar operations to ensure the cursor is in view.
2373    pub fn move_cursor_to_visible_area(&mut self, split_id: LeafId, buffer_id: BufferId) {
2374        let (top_byte, viewport_height) =
2375            if let Some(view_state) = self.buffers.splits().and_then(|(_, vs)| vs.get(&split_id)) {
2376                (
2377                    view_state.viewport.top_byte,
2378                    view_state.viewport.height as usize,
2379                )
2380            } else {
2381                return;
2382            };
2383
2384        if let Some(state) = self.buffers.get_mut(&buffer_id) {
2385            let buffer_len = state.buffer.len();
2386
2387            let mut iter = state.buffer.line_iterator(top_byte, 80);
2388            let mut bottom_byte = buffer_len;
2389
2390            for _ in 0..viewport_height {
2391                if let Some((pos, line)) = iter.next_line() {
2392                    bottom_byte = pos + line.len();
2393                } else {
2394                    bottom_byte = buffer_len;
2395                    break;
2396                }
2397            }
2398
2399            if let Some(view_state) = self
2400                .split_view_states_mut()
2401                .and_then(|vs| vs.get_mut(&split_id))
2402            {
2403                let cursor_pos = view_state.cursors.primary().position;
2404                if cursor_pos < top_byte || cursor_pos > bottom_byte {
2405                    let cursor = view_state.cursors.primary_mut();
2406                    cursor.position = top_byte;
2407                    // Keep the existing sticky_column value so vertical
2408                    // navigation preserves column.
2409                }
2410            }
2411        }
2412    }
2413
2414    /// Calculate the maximum allowed scroll position so the last line
2415    /// is always at the bottom unless the buffer is smaller than the
2416    /// viewport. Pure function on `Buffer`; lives on `Window` so the
2417    /// scrollbar helpers (also on `Window`) can reach it.
2418    pub fn calculate_max_scroll_position(
2419        buffer: &mut crate::model::buffer::Buffer,
2420        viewport_height: usize,
2421    ) -> usize {
2422        if viewport_height == 0 {
2423            return 0;
2424        }
2425
2426        let buffer_len = buffer.len();
2427        if buffer_len == 0 {
2428            return 0;
2429        }
2430
2431        let mut line_count = 0;
2432        let mut iter = buffer.line_iterator(0, 80);
2433        while iter.next_line().is_some() {
2434            line_count += 1;
2435        }
2436
2437        if line_count <= viewport_height {
2438            return 0;
2439        }
2440
2441        let scrollable_lines = line_count.saturating_sub(viewport_height);
2442
2443        let mut iter = buffer.line_iterator(0, 80);
2444        let mut current_line = 0;
2445        let mut max_byte_pos = 0;
2446
2447        while current_line < scrollable_lines {
2448            if let Some((pos, _content)) = iter.next_line() {
2449                max_byte_pos = pos;
2450                current_line += 1;
2451            } else {
2452                break;
2453            }
2454        }
2455
2456        max_byte_pos
2457    }
2458
2459    /// Find the split whose content or scrollbar area contains the
2460    /// screen cell `(col, row)`. Returns the split id and its buffer
2461    /// id, or `None` when the position falls outside every split's
2462    /// content rect and outside every scrollbar gutter.
2463    pub fn split_at_position(&self, col: u16, row: u16) -> Option<(LeafId, BufferId)> {
2464        for &(split_id, buffer_id, content_rect, scrollbar_rect, _, _) in
2465            &self.layout_cache.split_areas
2466        {
2467            let in_content = col >= content_rect.x
2468                && col < content_rect.x + content_rect.width
2469                && row >= content_rect.y
2470                && row < content_rect.y + content_rect.height;
2471            let in_scrollbar = scrollbar_rect.width > 0
2472                && scrollbar_rect.height > 0
2473                && col >= scrollbar_rect.x
2474                && col < scrollbar_rect.x + scrollbar_rect.width
2475                && row >= scrollbar_rect.y
2476                && row < scrollbar_rect.y + scrollbar_rect.height;
2477            if in_content || in_scrollbar {
2478                return Some((split_id, buffer_id));
2479            }
2480        }
2481        None
2482    }
2483
2484    /// If a per-edit diagnostic-pull debounce has fired, send a fresh
2485    /// `textDocument/diagnostic` request to the language server for the
2486    /// scheduled buffer. Returns false because the new diagnostics arrive
2487    /// asynchronously — the response handler will trigger any redraw.
2488    pub fn check_diagnostic_pull_timer(&mut self) -> bool {
2489        let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
2490            return false;
2491        };
2492
2493        if std::time::Instant::now() < trigger_time {
2494            return false;
2495        }
2496
2497        self.scheduled_diagnostic_pull = None;
2498
2499        let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2500            return false;
2501        };
2502        let Some(uri) = metadata.file_uri().cloned() else {
2503            return false;
2504        };
2505        let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2506            return false;
2507        };
2508
2509        let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
2510        let request_id = self.next_lsp_request_id;
2511        self.next_lsp_request_id += 1;
2512
2513        let lsp = &mut self.lsp;
2514        let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
2515        else {
2516            return false;
2517        };
2518        if let Err(e) =
2519            sh.handle
2520                .document_diagnostic(request_id, uri.as_uri().clone(), previous_result_id)
2521        {
2522            tracing::debug!(
2523                "Failed to pull diagnostics after edit for {}: {}",
2524                uri.as_str(),
2525                e
2526            );
2527        } else {
2528            tracing::debug!(
2529                "Pulling diagnostics after edit for {} (request_id={})",
2530                uri.as_str(),
2531                request_id
2532            );
2533        }
2534
2535        false
2536    }
2537
2538    /// Open a local file in this window (always uses local filesystem,
2539    /// not remote). Used for opening files like the warning log when
2540    /// the editor is connected to a remote server. Returns the buffer
2541    /// id and switches the active buffer to it (via
2542    /// [`Window::set_active_buffer`], so no plugin hook fires — the
2543    /// Editor caller is responsible for re-firing
2544    /// `buffer_activated` if the hook is required).
2545    pub fn open_local_file(&mut self, path: &std::path::Path) -> anyhow::Result<BufferId> {
2546        // Resolve relative paths against this window's root.
2547        let resolved_path = if path.is_relative() {
2548            self.root.join(path)
2549        } else {
2550            path.to_path_buf()
2551        };
2552
2553        // Save user-visible path for language detection before canonicalizing.
2554        let display_path = resolved_path.clone();
2555
2556        // Canonicalize the path.
2557        let canonical_path = resolved_path
2558            .canonicalize()
2559            .unwrap_or_else(|_| resolved_path.clone());
2560        let path = canonical_path.as_path();
2561
2562        // Check if already open.
2563        let already_open = self
2564            .buffers
2565            .iter()
2566            .find(|(_, state)| state.buffer.file_path() == Some(path))
2567            .map(|(id, _)| *id);
2568
2569        if let Some(id) = already_open {
2570            self.set_active_buffer(id);
2571            return Ok(id);
2572        }
2573
2574        // Create new buffer.
2575        let buffer_id = self.alloc_buffer_id();
2576
2577        // Load from canonical path (for I/O and dedup), detect language from
2578        // display path (for glob pattern matching against user-visible names).
2579        let buffer = crate::model::buffer::Buffer::load_from_file(
2580            &canonical_path,
2581            self.config().editor.large_file_threshold_bytes as usize,
2582            std::sync::Arc::clone(&self.resources.local_filesystem),
2583        )?;
2584        let first_line = buffer.first_line_lossy();
2585        let detected =
2586            crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
2587                &display_path,
2588                first_line.as_deref(),
2589                &self.resources.grammar_registry,
2590                &self.config().languages,
2591                self.config().default_language.as_deref(),
2592            );
2593        let state = crate::state::EditorState::from_buffer_with_language(buffer, detected);
2594
2595        self.buffers.insert(buffer_id, state);
2596        self.event_logs
2597            .insert(buffer_id, crate::model::event::EventLog::new());
2598
2599        // Create metadata.
2600        let metadata = crate::app::types::BufferMetadata::with_file(
2601            path.to_path_buf(),
2602            &display_path,
2603            &self.root,
2604            self.authority().path_translation.as_ref(),
2605        );
2606        self.buffer_metadata.insert(buffer_id, metadata);
2607
2608        // Add to preferred split's tabs (avoids labeled splits like sidebars).
2609        let target_split = self.preferred_split_for_file();
2610        let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
2611        let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
2612        // Snapshot config values before taking the mutable view-states borrow
2613        // so the closure body doesn't have to re-borrow `self`.
2614        let cfg = self.config().editor.clone();
2615        if let Some(view_state) = self
2616            .split_view_states_mut()
2617            .expect("active window must have a populated split layout")
2618            .get_mut(&target_split)
2619        {
2620            view_state.add_buffer(buffer_id);
2621            let buf_state = view_state.ensure_buffer_state(buffer_id);
2622            buf_state.apply_config_defaults(
2623                cfg.line_numbers,
2624                cfg.highlight_current_line,
2625                line_wrap,
2626                cfg.wrap_indent,
2627                wrap_column,
2628                cfg.rulers,
2629                cfg.scroll_offset,
2630            );
2631        }
2632
2633        self.set_active_buffer(buffer_id);
2634
2635        let display_name = path.display().to_string();
2636        self.set_status_message(rust_i18n::t!("buffer.opened", name = display_name).to_string());
2637
2638        Ok(buffer_id)
2639    }
2640
2641    /// Mark a buffer in this window as read-only (or writable), keeping
2642    /// the per-buffer metadata `read_only` flag and the editor state's
2643    /// `editing_disabled` flag in sync.
2644    pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
2645        if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
2646            metadata.read_only = read_only;
2647        }
2648        if let Some(state) = self.buffers.get_mut(&buffer_id) {
2649            state.editing_disabled = read_only;
2650        }
2651    }
2652
2653    /// Clear all warning indicators for this window (general + LSP) and
2654    /// post a "Warnings cleared" status message.
2655    pub fn clear_warnings(&mut self) {
2656        self.warning_domains.general.clear();
2657        self.warning_domains.lsp.clear();
2658        self.set_status_message("Warnings cleared".to_string());
2659    }
2660
2661    /// Recompute the LSP warning-domain level for this window from its
2662    /// `lsp_server_statuses` map. Called whenever a server transitions
2663    /// state.
2664    pub fn update_lsp_warning_domain(&mut self) {
2665        // Clone to release the immutable borrow before mutating warning_domains.
2666        let statuses = self.lsp_server_statuses.clone();
2667        self.warning_domains.lsp.update_from_statuses(&statuses);
2668    }
2669
2670    /// Check if semantic highlight debounce timer has expired for any
2671    /// buffer in this window. Returns true if a redraw is needed because
2672    /// the debounce period has elapsed and semantic highlights need to
2673    /// be recomputed.
2674    pub fn check_semantic_highlight_timer(&self) -> bool {
2675        self.buffers.any_needs_semantic_redraw()
2676    }
2677
2678    /// If an active search has placed the cursor inside a match, return that
2679    /// match's byte range.  Used by Ctrl-D ("Add cursor at next match") so a
2680    /// substring search drives the selection — instead of expanding to the
2681    /// whole word — when the user presses Ctrl-D right after searching
2682    /// (issue #1697).
2683    pub fn search_match_at_primary_cursor(&self) -> Option<std::ops::Range<usize>> {
2684        let search_state = self.search_state.as_ref()?;
2685        let pos = self.active_cursors().primary().position;
2686        let idx = match search_state.matches.binary_search(&pos) {
2687            Ok(i) => i,
2688            Err(0) => return None,
2689            Err(i) => i - 1,
2690        };
2691        let start = search_state.matches[idx];
2692        let len = *search_state.match_lengths.get(idx)?;
2693        if pos < start + len {
2694            Some(start..start + len)
2695        } else {
2696            None
2697        }
2698    }
2699
2700    /// Update search highlights in the visible viewport for the active
2701    /// buffer. Caller passes theme colors as parameters because `theme`
2702    /// is editor-global (not yet on `Window.resources`).
2703    pub fn update_search_highlights(
2704        &mut self,
2705        query: &str,
2706        search_fg: ratatui::style::Color,
2707        search_bg: ratatui::style::Color,
2708    ) {
2709        if query.is_empty() {
2710            self.clear_search_highlights();
2711            return;
2712        }
2713
2714        let case_sensitive = self.search_case_sensitive;
2715        let whole_word = self.search_whole_word;
2716        let use_regex = self.search_use_regex;
2717        let ns = self.search_namespace.clone();
2718
2719        let regex_pattern = if use_regex {
2720            if whole_word {
2721                format!(r"\b{}\b", query)
2722            } else {
2723                query.to_string()
2724            }
2725        } else {
2726            let escaped = regex::escape(query);
2727            if whole_word {
2728                format!(r"\b{}\b", escaped)
2729            } else {
2730                escaped
2731            }
2732        };
2733
2734        let regex = regex::RegexBuilder::new(&regex_pattern)
2735            .case_insensitive(!case_sensitive)
2736            .build();
2737        let regex = match regex {
2738            Ok(r) => r,
2739            Err(_) => {
2740                self.clear_search_highlights();
2741                return;
2742            }
2743        };
2744
2745        let active_split = self.effective_active_split();
2746        let (top_byte, visible_height) = self
2747            .buffers
2748            .splits()
2749            .expect("active window must have a populated split layout")
2750            .1
2751            .get(&active_split)
2752            .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2753            .unwrap_or((0, 20));
2754
2755        let state = self.active_state_mut();
2756        state.overlays.clear_namespace(&ns, &mut state.marker_list);
2757
2758        let visible_start = top_byte;
2759        let mut visible_end = top_byte;
2760        {
2761            let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2762            for _ in 0..visible_height {
2763                if let Some((line_start, line_content)) = line_iter.next_line() {
2764                    visible_end = line_start + line_content.len();
2765                } else {
2766                    break;
2767                }
2768            }
2769        }
2770        visible_end = visible_end.min(state.buffer.len());
2771        let visible_text = state.get_text_range(visible_start, visible_end);
2772
2773        for mat in regex.find_iter(&visible_text) {
2774            let absolute_pos = visible_start + mat.start();
2775            let match_len = mat.end() - mat.start();
2776            let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2777            let overlay = crate::view::overlay::Overlay::with_namespace_fixed_end(
2778                &mut state.marker_list,
2779                absolute_pos..(absolute_pos + match_len),
2780                crate::view::overlay::OverlayFace::Style {
2781                    style: search_style,
2782                },
2783                ns.clone(),
2784            )
2785            .with_priority_value(10);
2786            state.overlays.add(overlay);
2787        }
2788    }
2789
2790    /// Re-evaluate committed search highlights around an edited region.
2791    ///
2792    /// Search-match overlays are anchored by markers that merely *track*
2793    /// byte positions through edits; they never re-check whether the text
2794    /// they cover still matches the query. So editing inside a highlighted
2795    /// match (or typing against its boundary, which can break a `\b`
2796    /// whole-word rule) would leave a stale highlight on text that no
2797    /// longer matches. This recomputes matches on just the line(s) touched
2798    /// by the edit and swaps the search overlays in that span, so highlights
2799    /// drop and appear exactly where the text starts/stops matching.
2800    ///
2801    /// `edit_start` / `edit_new_len` are in post-edit byte coordinates (for
2802    /// a deletion, `edit_new_len` is 0). Bounded to the affected lines to
2803    /// keep it viewport-localized rather than a full-buffer rescan.
2804    pub fn reevaluate_search_overlays_around(
2805        &mut self,
2806        edit_start: usize,
2807        edit_new_len: usize,
2808        search_fg: ratatui::style::Color,
2809        search_bg: ratatui::style::Color,
2810    ) {
2811        let query = match self.search_state.as_ref() {
2812            Some(ss) if !ss.query.is_empty() => ss.query.clone(),
2813            _ => return,
2814        };
2815
2816        let case_sensitive = self.search_case_sensitive;
2817        let whole_word = self.search_whole_word;
2818        let use_regex = self.search_use_regex;
2819        let ns = self.search_namespace.clone();
2820
2821        let regex_pattern = if use_regex {
2822            if whole_word {
2823                format!(r"\b{}\b", query)
2824            } else {
2825                query
2826            }
2827        } else {
2828            let escaped = regex::escape(&query);
2829            if whole_word {
2830                format!(r"\b{}\b", escaped)
2831            } else {
2832                escaped
2833            }
2834        };
2835
2836        let regex = match regex::RegexBuilder::new(&regex_pattern)
2837            .case_insensitive(!case_sensitive)
2838            .build()
2839        {
2840            Ok(r) => r,
2841            Err(_) => return,
2842        };
2843
2844        let state = self.active_state_mut();
2845        let buf_len = state.buffer.len();
2846        let edit_end = edit_start.saturating_add(edit_new_len).min(buf_len);
2847
2848        // Expand the edited byte span to the full line(s) it touches so that
2849        // word-boundary context on either side of the edit is included.
2850        let start_line = state.buffer.get_line_number(edit_start.min(buf_len));
2851        let end_line = state.buffer.get_line_number(edit_end);
2852        let win_start = state.buffer.line_start_offset(start_line).unwrap_or(0);
2853        let win_end = state
2854            .buffer
2855            .line_start_offset(end_line + 1)
2856            .unwrap_or(buf_len)
2857            .min(buf_len);
2858
2859        let text = state.get_text_range(win_start, win_end);
2860
2861        let mut new_overlays = Vec::new();
2862        for mat in regex.find_iter(&text) {
2863            let absolute_pos = win_start + mat.start();
2864            let match_len = mat.end() - mat.start();
2865            let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2866            new_overlays.push(
2867                crate::view::overlay::Overlay::with_namespace_fixed_end(
2868                    &mut state.marker_list,
2869                    absolute_pos..(absolute_pos + match_len),
2870                    crate::view::overlay::OverlayFace::Style {
2871                        style: search_style,
2872                    },
2873                    ns.clone(),
2874                )
2875                .with_priority_value(10),
2876            );
2877        }
2878
2879        state.overlays.replace_range_in_namespace(
2880            &ns,
2881            &(win_start..win_end),
2882            new_overlays,
2883            &mut state.marker_list,
2884        );
2885    }
2886
2887    // ---- File-explorer leaf delegators ----
2888
2889    /// Whether this window's file-explorer panel is visible.
2890    pub fn file_explorer_is_visible(&self) -> bool {
2891        self.file_explorer_visible && self.file_explorer.is_some()
2892    }
2893
2894    /// Extend the file-explorer selection upward.
2895    pub fn file_explorer_extend_selection_up(&mut self) {
2896        if let Some(explorer) = self.file_explorer.as_mut() {
2897            explorer.extend_selection_up();
2898        }
2899    }
2900
2901    /// Extend the file-explorer selection downward.
2902    pub fn file_explorer_extend_selection_down(&mut self) {
2903        if let Some(explorer) = self.file_explorer.as_mut() {
2904            explorer.extend_selection_down();
2905        }
2906    }
2907
2908    /// Toggle the selection state of the focused file-explorer entry.
2909    pub fn file_explorer_toggle_select(&mut self) {
2910        if let Some(explorer) = self.file_explorer.as_mut() {
2911            explorer.toggle_select();
2912        }
2913    }
2914
2915    /// Select every visible entry in the file explorer.
2916    pub fn file_explorer_select_all(&mut self) {
2917        if let Some(explorer) = self.file_explorer.as_mut() {
2918            explorer.select_all();
2919        }
2920    }
2921
2922    /// Push a character onto the file-explorer search filter.
2923    pub fn file_explorer_search_push_char(&mut self, c: char) {
2924        if let Some(explorer) = self.file_explorer.as_mut() {
2925            explorer.search_push_char(c);
2926            explorer.update_scroll_for_selection();
2927        }
2928    }
2929
2930    /// Pop the last character from the file-explorer search filter.
2931    pub fn file_explorer_search_pop_char(&mut self) {
2932        if let Some(explorer) = self.file_explorer.as_mut() {
2933            explorer.search_pop_char();
2934            explorer.update_scroll_for_selection();
2935        }
2936    }
2937
2938    // ---- LSP scheduling helpers ----
2939
2940    /// Schedule a folding-range refresh for a buffer (debounced). The
2941    /// debounce window timestamp is stored on the window's per-buffer
2942    /// folding-ranges debounce map.
2943    pub fn schedule_folding_ranges_refresh(&mut self, buffer_id: BufferId) {
2944        const FOLDING_RANGES_DEBOUNCE_MS: u64 = 300;
2945        let next_time = std::time::Instant::now()
2946            + std::time::Duration::from_millis(FOLDING_RANGES_DEBOUNCE_MS);
2947        self.folding_ranges_debounce.insert(buffer_id, next_time);
2948    }
2949
2950    /// Schedule a full semantic-tokens refresh for a buffer (debounced).
2951    /// No-op when `enable_semantic_tokens_full` is off in the active
2952    /// config.
2953    pub fn schedule_semantic_tokens_full_refresh(&mut self, buffer_id: BufferId) {
2954        const SEMANTIC_TOKENS_FULL_DEBOUNCE_MS: u64 = 500;
2955        if !self.resources.config.editor.enable_semantic_tokens_full {
2956            return;
2957        }
2958        let next_time = std::time::Instant::now()
2959            + std::time::Duration::from_millis(SEMANTIC_TOKENS_FULL_DEBOUNCE_MS);
2960        self.semantic_tokens_full_debounce
2961            .insert(buffer_id, next_time);
2962    }
2963
2964    /// Forward incremental LSP `didChange` notifications for `buffer_id`
2965    /// to every server registered for the buffer's language. Sends
2966    /// `didOpen` first when a server hasn't yet seen this buffer, and
2967    /// reschedules diagnostic / inlay-hint pulls.
2968    ///
2969    /// Pure per-window operation: every piece of state it touches
2970    /// (`buffer_metadata`, `buffers`, the LSP manager, debounce maps)
2971    /// lives on `Window`. Editor-side wrappers exist only as forwarding
2972    /// shims for legacy call sites.
2973    pub(crate) fn send_lsp_changes_for_buffer(
2974        &mut self,
2975        buffer_id: BufferId,
2976        changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
2977    ) {
2978        const INLAY_HINTS_DEBOUNCE_MS: u64 = 500;
2979
2980        if changes.is_empty() {
2981            return;
2982        }
2983
2984        let metadata = match self.buffer_metadata.get(&buffer_id) {
2985            Some(m) => m,
2986            None => {
2987                tracing::debug!(
2988                    "send_lsp_changes_for_buffer: no metadata for buffer {:?}",
2989                    buffer_id
2990                );
2991                return;
2992            }
2993        };
2994
2995        if !metadata.lsp_enabled {
2996            tracing::debug!("send_lsp_changes_for_buffer: LSP disabled for this buffer");
2997            return;
2998        }
2999
3000        let uri = match metadata.file_uri() {
3001            Some(u) => u.clone(),
3002            None => {
3003                tracing::debug!(
3004                    "send_lsp_changes_for_buffer: no URI for buffer (not a file or URI creation failed)"
3005                );
3006                return;
3007            }
3008        };
3009        let file_path = metadata.file_path().cloned();
3010
3011        let language = match self.buffers.get(&buffer_id).map(|s| s.language.clone()) {
3012            Some(l) => l,
3013            None => {
3014                tracing::debug!(
3015                    "send_lsp_changes_for_buffer: no buffer state for {:?}",
3016                    buffer_id
3017                );
3018                return;
3019            }
3020        };
3021
3022        tracing::trace!(
3023            "send_lsp_changes_for_buffer: sending {} changes to {} in single didChange notification",
3024            changes.len(),
3025            uri.as_str()
3026        );
3027
3028        use crate::services::lsp::manager::LspSpawnResult;
3029        let lsp = &mut self.lsp;
3030
3031        if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
3032            tracing::debug!(
3033                "send_lsp_changes_for_buffer: LSP not running for {} (auto_start disabled)",
3034                language
3035            );
3036            return;
3037        }
3038
3039        let handles_needing_open: Vec<_> = {
3040            let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
3041                return;
3042            };
3043            lsp.get_handles(&language)
3044                .into_iter()
3045                .filter(|sh| !metadata.lsp_opened_with.contains(&sh.handle.id()))
3046                .map(|sh| (sh.name.clone(), sh.handle.id()))
3047                .collect()
3048        };
3049
3050        if !handles_needing_open.is_empty() {
3051            let text = match self
3052                .buffers
3053                .get(&buffer_id)
3054                .and_then(|s| s.buffer.to_string())
3055            {
3056                Some(t) => t,
3057                None => {
3058                    tracing::debug!(
3059                        "send_lsp_changes_for_buffer: buffer text not available for didOpen"
3060                    );
3061                    return;
3062                }
3063            };
3064
3065            let lsp = &mut self.lsp;
3066            for sh in lsp.get_handles_mut(&language) {
3067                if handles_needing_open
3068                    .iter()
3069                    .any(|(_, id)| *id == sh.handle.id())
3070                {
3071                    if let Err(e) =
3072                        sh.handle
3073                            .did_open(uri.as_uri().clone(), text.clone(), language.clone())
3074                    {
3075                        tracing::warn!(
3076                            "Failed to send didOpen to '{}' before didChange: {}",
3077                            sh.name,
3078                            e
3079                        );
3080                    } else {
3081                        tracing::debug!(
3082                            "Sent didOpen for {} to LSP handle '{}' before didChange",
3083                            uri.as_str(),
3084                            sh.name
3085                        );
3086                    }
3087                }
3088            }
3089
3090            if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
3091                for (_, handle_id) in &handles_needing_open {
3092                    metadata.lsp_opened_with.insert(*handle_id);
3093                }
3094            }
3095
3096            // didOpen already contains the full current buffer content, so we must
3097            // NOT also send didChange (which carries pre-edit incremental changes).
3098            // Sending both would corrupt the server's view of the document.
3099            return;
3100        }
3101
3102        let lsp = &mut self.lsp;
3103        let mut any_sent = false;
3104        for sh in lsp.get_handles_mut(&language) {
3105            if let Err(e) = sh.handle.did_change(uri.as_uri().clone(), changes.clone()) {
3106                tracing::warn!("Failed to send didChange to '{}': {}", sh.name, e);
3107            } else {
3108                any_sent = true;
3109            }
3110        }
3111        if any_sent {
3112            tracing::trace!("Successfully sent batched didChange to LSP");
3113
3114            if let Some(state) = self.buffers.get(&buffer_id) {
3115                if let Some(path) = state.buffer.file_path() {
3116                    crate::services::lsp::diagnostics::invalidate_cache_for_file(
3117                        &path.to_string_lossy(),
3118                    );
3119                }
3120            }
3121
3122            self.scheduled_diagnostic_pull = Some((
3123                buffer_id,
3124                std::time::Instant::now() + std::time::Duration::from_millis(1000),
3125            ));
3126
3127            if self.resources.config.editor.enable_inlay_hints {
3128                self.scheduled_inlay_hints_request = Some((
3129                    buffer_id,
3130                    std::time::Instant::now()
3131                        + std::time::Duration::from_millis(INLAY_HINTS_DEBOUNCE_MS),
3132                ));
3133            }
3134        }
3135    }
3136
3137    /// Invalidate cached layouts and view transforms for every split
3138    /// that displays `buffer_id`. Pure window-state mutation: walks
3139    /// the window's split tree and view-state map.
3140    pub fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
3141        let Some((mgr, vs_map)) = self.buffers.splits_mut() else {
3142            return;
3143        };
3144        let splits_for_buffer = mgr.splits_for_buffer(buffer_id);
3145        for split_id in splits_for_buffer {
3146            if let Some(view_state) = vs_map.get_mut(&split_id) {
3147                view_state.invalidate_layout();
3148                view_state.view_transform = None;
3149                view_state.view_transform_stale = true;
3150            }
3151        }
3152    }
3153
3154    /// Adjust cursors in other splits that share the same buffer after
3155    /// an edit. The split that originated the event already had its
3156    /// cursors moved by `BufferState::apply`; this method walks every
3157    /// other split displaying the same buffer and shifts (or, for a
3158    /// `BulkEdit`, resets) their cursors so they don't dangle past
3159    /// freshly-deleted text.
3160    pub fn adjust_other_split_cursors_for_event(&mut self, event: &Event) {
3161        let current_buffer_id = self.active_buffer();
3162        let buffer_len = self
3163            .buffers
3164            .get(&current_buffer_id)
3165            .map(|s| s.buffer.len())
3166            .unwrap_or(0);
3167        let Some((mgr, vs_map)) = self.buffers.splits_mut() else {
3168            return;
3169        };
3170        let current_split_id = mgr.active_split();
3171        let splits_for_buffer = mgr.splits_for_buffer(current_buffer_id);
3172
3173        if let Event::BulkEdit { new_cursors, .. } = event {
3174            for split_id in splits_for_buffer {
3175                if split_id == current_split_id {
3176                    continue;
3177                }
3178                if let Some(view_state) = vs_map.get_mut(&split_id) {
3179                    if let Some((_, pos, _)) = new_cursors.first() {
3180                        let new_pos = (*pos).min(buffer_len);
3181                        view_state.cursors.primary_mut().position = new_pos;
3182                        view_state.cursors.primary_mut().anchor = None;
3183                    }
3184                }
3185            }
3186            return;
3187        }
3188
3189        let adjustments: Vec<(usize, usize, usize)> = match event {
3190            Event::Insert { position, text, .. } => {
3191                vec![(*position, 0, text.len())]
3192            }
3193            Event::Delete { range, .. } => {
3194                vec![(range.start, range.len(), 0)]
3195            }
3196            Event::Batch { events, .. } => events
3197                .iter()
3198                .filter_map(|e| match e {
3199                    Event::Insert { position, text, .. } => Some((*position, 0, text.len())),
3200                    Event::Delete { range, .. } => Some((range.start, range.len(), 0)),
3201                    _ => None,
3202                })
3203                .collect(),
3204            _ => Vec::new(),
3205        };
3206
3207        if adjustments.is_empty() {
3208            return;
3209        }
3210
3211        for split_id in splits_for_buffer {
3212            if split_id == current_split_id {
3213                continue;
3214            }
3215            if let Some(view_state) = vs_map.get_mut(&split_id) {
3216                for (edit_pos, old_len, new_len) in &adjustments {
3217                    view_state
3218                        .cursors
3219                        .adjust_for_edit(*edit_pos, *old_len, *new_len);
3220                }
3221            }
3222        }
3223    }
3224
3225    /// Handle scroll events using the active split's viewport.
3226    ///
3227    /// View events (like `Scroll`) target SplitViewState rather than
3228    /// EditorState so scroll limits are correct when view transforms
3229    /// inject extra rows.
3230    pub(crate) fn handle_scroll_event(&mut self, line_offset: isize) {
3231        use crate::view::ui::view_pipeline::ViewLineIterator;
3232
3233        let Some((mgr, _)) = self.buffers.splits() else {
3234            return;
3235        };
3236        let active_split = mgr.active_split();
3237
3238        if let Some(group) = self
3239            .scroll_sync_manager
3240            .find_group_for_split(active_split.into())
3241        {
3242            let left = group.left_split;
3243            let right = group.right_split;
3244            if let Some(vs_map) = self.split_view_states_mut() {
3245                if let Some(vs) = vs_map.get_mut(&LeafId(left)) {
3246                    vs.viewport.set_skip_ensure_visible();
3247                }
3248                if let Some(vs) = vs_map.get_mut(&LeafId(right)) {
3249                    vs.viewport.set_skip_ensure_visible();
3250                }
3251            }
3252        }
3253
3254        let (mgr, vs_map) = self.buffers.splits().expect("splits checked above");
3255        let sync_group = vs_map.get(&active_split).and_then(|vs| vs.sync_group);
3256        let splits_to_scroll = if let Some(group_id) = sync_group {
3257            mgr.get_splits_in_group(group_id, vs_map)
3258        } else {
3259            vec![active_split]
3260        };
3261
3262        let tab_size = self.resources.config.editor.tab_size;
3263        for split_id in splits_to_scroll {
3264            let (mgr, vs_map) = self.buffers.splits().expect("splits checked above");
3265            let Some(buffer_id) = mgr.buffer_for_split(split_id) else {
3266                continue;
3267            };
3268
3269            let view_transform_tokens = vs_map
3270                .get(&split_id)
3271                .and_then(|vs| vs.view_transform.as_ref())
3272                .map(|vt| vt.tokens.clone());
3273
3274            self.buffers
3275                .with_buffer_and_split(buffer_id, split_id, |state, view_state| {
3276                    let soft_breaks = state.collect_soft_break_positions();
3277                    let virtual_lines = state.collect_virtual_line_positions();
3278                    let buffer = &mut state.buffer;
3279                    if let Some(tokens) = view_transform_tokens {
3280                        let view_lines: Vec<_> =
3281                            ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
3282                        view_state
3283                            .viewport
3284                            .scroll_view_lines(&view_lines, line_offset);
3285                    } else if line_offset > 0 {
3286                        view_state.viewport.scroll_down(
3287                            buffer,
3288                            &soft_breaks,
3289                            &virtual_lines,
3290                            line_offset as usize,
3291                        );
3292                    } else {
3293                        view_state.viewport.scroll_up(
3294                            buffer,
3295                            &soft_breaks,
3296                            &virtual_lines,
3297                            line_offset.unsigned_abs(),
3298                        );
3299                    }
3300                    view_state.viewport.set_skip_ensure_visible();
3301                });
3302        }
3303    }
3304
3305    /// Handle a `SetViewport` event using the active split's viewport.
3306    pub(crate) fn handle_set_viewport_event(&mut self, top_line: usize) {
3307        let Some((mgr, _)) = self.buffers.splits() else {
3308            return;
3309        };
3310        let active_split = mgr.active_split();
3311
3312        if self
3313            .scroll_sync_manager
3314            .is_split_synced(active_split.into())
3315        {
3316            if let Some(group) = self
3317                .scroll_sync_manager
3318                .find_group_for_split_mut(active_split.into())
3319            {
3320                let scroll_line = if group.is_left_split(active_split.into()) {
3321                    top_line
3322                } else {
3323                    group.right_to_left_line(top_line)
3324                };
3325                group.set_scroll_line(scroll_line);
3326            }
3327
3328            let (left, right) = match self
3329                .scroll_sync_manager
3330                .find_group_for_split(active_split.into())
3331            {
3332                Some(group) => (group.left_split, group.right_split),
3333                None => return,
3334            };
3335            if let Some(vs_map) = self.split_view_states_mut() {
3336                if let Some(vs) = vs_map.get_mut(&LeafId(left)) {
3337                    vs.viewport.set_skip_ensure_visible();
3338                }
3339                if let Some(vs) = vs_map.get_mut(&LeafId(right)) {
3340                    vs.viewport.set_skip_ensure_visible();
3341                }
3342            }
3343            return;
3344        }
3345
3346        let (mgr, vs_map) = self.buffers.splits().expect("splits checked above");
3347        let sync_group = vs_map.get(&active_split).and_then(|vs| vs.sync_group);
3348        let splits_to_scroll = if let Some(group_id) = sync_group {
3349            mgr.get_splits_in_group(group_id, vs_map)
3350        } else {
3351            vec![active_split]
3352        };
3353
3354        for split_id in splits_to_scroll {
3355            let (mgr, _) = self.buffers.splits().expect("splits checked above");
3356            let Some(buffer_id) = mgr.buffer_for_split(split_id) else {
3357                continue;
3358            };
3359
3360            self.buffers
3361                .with_buffer_and_split(buffer_id, split_id, |state, view_state| {
3362                    view_state.viewport.scroll_to(&mut state.buffer, top_line);
3363                    view_state.viewport.set_skip_ensure_visible();
3364                });
3365        }
3366    }
3367
3368    /// Handle a `Recenter` event using the active split's viewport.
3369    pub(crate) fn handle_recenter_event(&mut self) {
3370        let Some((mgr, vs_map)) = self.buffers.splits() else {
3371            return;
3372        };
3373        let active_split = mgr.active_split();
3374
3375        let sync_group = vs_map.get(&active_split).and_then(|vs| vs.sync_group);
3376        let splits_to_recenter = if let Some(group_id) = sync_group {
3377            mgr.get_splits_in_group(group_id, vs_map)
3378        } else {
3379            vec![active_split]
3380        };
3381
3382        for split_id in splits_to_recenter {
3383            let (mgr, _) = self.buffers.splits().expect("splits checked above");
3384            let Some(buffer_id) = mgr.buffer_for_split(split_id) else {
3385                continue;
3386            };
3387
3388            self.buffers
3389                .with_buffer_and_split(buffer_id, split_id, |state, view_state| {
3390                    let buffer = &mut state.buffer;
3391                    let cursor_pos = view_state.cursors.primary().position;
3392                    // `center_on_position` counts real visual rows, so a
3393                    // recenter in a wrapped document doesn't under-scroll
3394                    // and leave the cursor below the viewport (each logical
3395                    // line above the cursor can span many rows).
3396                    view_state.viewport.center_on_position(buffer, cursor_pos);
3397                    view_state.viewport.set_skip_ensure_visible();
3398                });
3399        }
3400    }
3401
3402    /// Atomically update both sides of the pane-buffer invariant for a
3403    /// given leaf split: the split tree's stored buffer AND the matching
3404    /// `SplitViewState.active_buffer` / `keyed_states` map.
3405    ///
3406    /// This is the one place that's allowed to change "which buffer is
3407    /// shown in pane `leaf`". The two stores can never drift if every
3408    /// caller goes through here (issue #1620).
3409    ///
3410    /// If the leaf has no `SplitViewState` yet (e.g. mid-session-restore,
3411    /// when the SVS is registered later), the tree is still updated and
3412    /// the SVS sync is skipped — the caller is responsible for ensuring
3413    /// the SVS exists by the time any input is routed.
3414    pub fn set_pane_buffer(&mut self, leaf: LeafId, buffer_id: BufferId) {
3415        let (mgr, vs_map) = self
3416            .buffers
3417            .splits_mut()
3418            .expect("active window must have a populated split layout");
3419        mgr.set_split_buffer(leaf, buffer_id);
3420        if let Some(view_state) = vs_map.get_mut(&leaf) {
3421            view_state.switch_buffer(buffer_id);
3422            view_state.add_buffer(buffer_id);
3423        }
3424    }
3425}
3426
3427// Label-defaulting unit tests (`empty_label_defaults_to_root_basename`,
3428// `explicit_label_is_kept`, `empty_label_with_rootless_path_falls_back_to_main`)
3429// were removed when `Window::new` started taking a `WindowResources`
3430// argument — stubbing every editor-global service for a 3-line label
3431// assertion isn't worth the maintenance, and the same behaviour is
3432// already exercised by every `EditorTestHarness::create` path that
3433// names a window.