Skip to main content

reovim_driver_session/
runtime.rs

1//! `SessionRuntime` - concrete implementation of all session API traits.
2//!
3//! This module provides [`SessionRuntime`], which bundles `Session`, `KernelContext`,
4//! and `CommandExecutor` to implement all the focused API traits.
5//!
6//! # Design
7//!
8//! ```text
9//! SessionRuntime
10//! ├── Session (mode stack, windows, extensions)
11//! ├── KernelContext (buffers, registers, marks)
12//! ├── CommandExecutor (command lookup/execution)
13//! └── StateChanges (accumulated changes)
14//! ```
15//!
16//! # Usage
17//!
18//! ```ignore
19//! use reovim_driver_session::SessionRuntime;
20//!
21//! // In runner's event loop:
22//! let mut runtime = SessionRuntime::new(
23//!     &mut session,
24//!     &mut kernel,
25//!     &command_executor,
26//! );
27//!
28//! // Resolver uses runtime via trait bounds
29//! resolver.resolve_with_session(&key, &mut state, &mut runtime);
30//!
31//! // Take accumulated changes
32//! let changes = runtime.take_changes();
33//!
34//! // Broadcast notifications
35//! if changes.mode_changed {
36//!     notify_mode_change().await;
37//! }
38//! ```
39
40use {
41    reovim_driver_clipboard::{ClipboardKey, ClipboardProviderRegistry},
42    reovim_driver_command_types::{CommandContext, CommandResult, RuntimeSignal},
43    reovim_driver_layout::{
44        LayerId, NavigateDirection, OverlayConstraints, Rect, SplitDirection, WindowPlacement,
45    },
46    reovim_driver_undo::{UndoKey, UndoProviderRegistry},
47    reovim_kernel::api::v1::{
48        BufferId, CommandId, Edit, KernelContext, ModeId, OptionValue, Position, TabId, UndoResult,
49        WindowId,
50        events::kernel::{
51            CursorMoved, LayoutChangeKind, LayoutChanged, SplitDirection as KernelSplitDirection,
52        },
53    },
54};
55
56use crate::{
57    Selection, Session, SessionExtension, Window,
58    api::{
59        BufferApi, BufferError, ChangeTracker, ClipboardApi, CommandApi, CommandExecutor,
60        CompositorApi, CompositorError, ExtensionApi, ModeApi, ModeError, RegisterApi,
61        RegisterContent, StateChanges, UndoApi, WindowApi, WindowError,
62    },
63    transition::{PopResult, TransitionContext},
64};
65
66/// Runtime that implements all session API traits.
67///
68/// Bundles `Session` + `KernelContext` + `CommandExecutor`.
69/// Changes accumulate internally; runner takes at end via [`take_changes`].
70///
71/// # Compositor Integration
72///
73/// The compositor is accessed via `session.compositor`. When present,
74/// `CompositorApi` methods delegate to it. When absent, they return errors.
75///
76/// # Per-Client State (#471, #477)
77///
78/// `SessionRuntime` ALWAYS operates on per-client state. The per-client fields
79/// are **required** (no Option wrappers, no fallback to shared state):
80///
81/// - `mode_stack` (#471): Per-client mode (INSERT, NORMAL, etc.)
82/// - `windows` (#471): Per-client cursor positions
83/// - `extensions` (#477): Per-client module state (`VimSessionState`, etc.)
84///
85/// This enforces multi-client isolation at compile time - you cannot create
86/// a `SessionRuntime` without providing per-client state.
87///
88/// # Client Binding (#471)
89///
90/// The `owner` field tracks which client this runtime is bound to. When present,
91/// it makes explicit which client's state we're operating on. This is useful for:
92/// - Debugging and logging
93/// - Assertions in tests
94/// - Future multi-client coordination
95///
96/// [`take_changes`]: ChangeTracker::take_changes
97pub struct SessionRuntime<'a> {
98    /// The client this runtime is bound to (#471).
99    ///
100    /// When `Some`, makes explicit which client's state we're operating on.
101    /// When `None`, this is a test runtime without explicit client binding.
102    owner: Option<crate::ClientId>,
103    /// Shared session state (template compositor, home mode).
104    ///
105    /// Per-client state is stored in SEPARATE fields below, not in session.
106    session: &'a mut Session,
107    /// Per-client mode stack (REQUIRED - no Option, #471).
108    ///
109    /// Mode operations use this directly. Multi-client mode isolation is
110    /// enforced by requiring this field at construction time.
111    mode_stack: &'a mut reovim_kernel::api::v1::ModeStack,
112    /// Per-client window layout with cursors (REQUIRED - no Option, #471).
113    ///
114    /// Cursor operations use this directly. Multi-client cursor isolation
115    /// is enforced by requiring this field at construction time.
116    windows: &'a mut crate::WindowLayout,
117    /// Per-client extensions (REQUIRED - no Option, #477).
118    ///
119    /// Extension operations use this directly. Per-client module state
120    /// isolation is enforced by requiring this field at construction time.
121    extensions: &'a mut crate::ExtensionMap,
122    /// Session-wide shared extensions (optional, #543).
123    ///
124    /// When `Some`, commands can access session-wide state via `shared_ext()` /
125    /// `shared_ext_mut()`. When `None` (tests, single-player), shared access
126    /// returns `None`. Set via [`with_shared_extensions()`](Self::with_shared_extensions).
127    shared_extensions: Option<&'a mut crate::ExtensionMap>,
128    /// Per-client compositor for window layout (#474).
129    ///
130    /// Each client owns their own compositor cloned from the shared template.
131    /// `CompositorApi` methods use this instead of `session.shared.compositor`.
132    /// `None` when compositor is not set (tests, headless mode).
133    compositor: &'a mut Option<Box<dyn reovim_driver_layout::RootCompositor>>,
134    /// Per-client tab pages (#401).
135    tabs: &'a mut crate::TabPageSet,
136    /// Per-client register storage (#515).
137    ///
138    /// Each client owns their own registers (unnamed, named a-z/A-Z).
139    /// System clipboard (+, *) remains shared via `ClipboardProvider`.
140    registers: &'a mut reovim_kernel::api::v1::RegisterBank,
141    /// Per-client clipboard history ring (#515).
142    ///
143    /// Tracks yank/delete history for numbered registers 0-9.
144    clipboard_history: &'a mut reovim_kernel::api::v1::HistoryRing,
145    /// Per-client local marks (a-z, per-client special marks) (#515).
146    ///
147    /// Global marks (A-Z) remain shared in `kernel.global_marks`.
148    local_marks: &'a mut reovim_kernel::api::v1::MarkBank,
149    /// Per-client jump list for Ctrl-O / Ctrl-I navigation (#654).
150    jumplist: &'a mut reovim_kernel::api::v1::Jumplist,
151    /// Per-client active buffer (#471).
152    ///
153    /// Each client tracks which buffer they are viewing independently.
154    active_buffer: &'a mut Option<reovim_kernel::api::v1::BufferId>,
155    /// Kernel context (buffers, registers, global marks).
156    kernel: &'a KernelContext,
157    /// Command executor for looking up and running commands.
158    executor: &'a dyn CommandExecutor,
159    /// Cached screen size for compositor operations.
160    screen: Rect,
161    /// Accumulated changes - runner takes at end.
162    changes: StateChanges,
163    /// Signal queue — commands push signals during execution,
164    /// server drains after command returns (#547).
165    signals: Vec<RuntimeSignal>,
166    /// Recursion depth counter for re-entrant command execution (#547).
167    ///
168    /// Incremented before each `execute_command()` call, decremented after.
169    /// Max depth is 16 — deeper recursion returns an error.
170    command_depth: usize,
171    /// Cursor position snapshot for `CursorMoved` event emission (#664).
172    ///
173    /// Captured at construction time (pre-command cursor position).
174    /// Updated after each `CursorMoved` emission for multi-move commands.
175    cursor_snapshot: Option<(u32, u32)>,
176}
177
178impl<'a> SessionRuntime<'a> {
179    /// Create a new runtime with per-client state (#471, #477).
180    ///
181    /// All per-client state is **required**. There are no fallbacks to shared
182    /// session state. This enforces multi-client isolation at compile time.
183    ///
184    /// # Arguments
185    ///
186    /// * `session` - Shared session state (compositor, home mode)
187    /// * `mode_stack` - Per-client mode stack (source of truth for mode)
188    /// * `windows` - Per-client window layout with cursors
189    /// * `extensions` - Per-client module extensions
190    /// * `kernel` - Kernel context (buffers, registers, marks)
191    /// * `executor` - Command executor
192    ///
193    /// # Multi-Client Isolation
194    ///
195    /// Each client has independent:
196    /// - Mode state (Client A in INSERT while Client B in NORMAL)
197    /// - Cursor positions (Client A at line 5, Client B at line 10)
198    /// - Module state (Client A's `pending_count` doesn't affect Client B)
199    ///
200    /// # Example
201    ///
202    /// ```ignore
203    /// // From server level, get per-client EditingState
204    /// let editing_state = session.client_state_mut(client_id)?;
205    ///
206    /// // Create runtime with per-client state bundle
207    /// let client = editing_state.client_context();
208    /// let mut runtime = SessionRuntime::new(
209    ///     &mut driver_session,
210    ///     client,
211    ///     &kernel,
212    ///     &executor,
213    /// );
214    ///
215    /// // All operations use per-client state
216    /// runtime.push_mode(insert_mode, ctx);     // Only affects this client
217    /// runtime.windows().active();              // This client's active window
218    /// runtime.ext_mut::<VimSessionState>();    // This client's vim state
219    /// ```
220    #[allow(clippy::needless_pass_by_value)] // ClientContext fields are moved into Self
221    pub fn new(
222        session: &'a mut Session,
223        client: crate::ClientContext<'a>,
224        kernel: &'a KernelContext,
225        executor: &'a dyn CommandExecutor,
226    ) -> Self {
227        let screen = {
228            let (width, height) = *client.terminal_size;
229            Rect::new(0, 0, width, height)
230        };
231        // Snapshot cursor position before any command executes (#664).
232        // Used as `from` in CursorMoved event emission.
233        #[allow(clippy::cast_possible_truncation)]
234        let cursor_snapshot = client
235            .windows
236            .active()
237            .map(|w| (w.cursor.line as u32, w.cursor.column as u32));
238        Self {
239            owner: None,
240            session,
241            mode_stack: client.mode_stack,
242            windows: client.windows,
243            extensions: client.extensions,
244            shared_extensions: None,
245            compositor: client.compositor,
246            tabs: client.tabs,
247            registers: client.registers,
248            clipboard_history: client.clipboard_history,
249            local_marks: client.local_marks,
250            jumplist: client.jumplist,
251            active_buffer: client.active_buffer,
252            kernel,
253            executor,
254            screen,
255            changes: StateChanges::new(),
256            signals: Vec::new(),
257            command_depth: 0,
258            cursor_snapshot,
259        }
260    }
261
262    /// Create a runtime with explicit client binding (#471).
263    ///
264    /// Like [`new`], but also records which client this runtime is bound to.
265    /// The `owner` field can be retrieved via [`owner()`].
266    ///
267    /// # Arguments
268    ///
269    /// * `owner` - The `ClientId` this runtime is bound to
270    /// * `session` - Shared session infrastructure
271    /// * `client` - Per-client state bundle (mode, windows, extensions, registers, etc.)
272    /// * `kernel` - Kernel context (buffers, global marks)
273    /// * `executor` - Command executor
274    ///
275    /// # Example
276    ///
277    /// ```ignore
278    /// let client = editing_state.client_context();
279    /// let mut runtime = SessionRuntime::with_owner(
280    ///     client_id,
281    ///     &mut driver_session,
282    ///     client,
283    ///     &kernel,
284    ///     &executor,
285    /// );
286    ///
287    /// // Query which client owns this runtime
288    /// assert_eq!(runtime.owner(), Some(client_id));
289    /// ```
290    ///
291    /// [`new`]: Self::new
292    /// [`owner()`]: Self::owner
293    #[allow(clippy::needless_pass_by_value)] // ClientContext fields are moved into Self
294    pub fn with_owner(
295        owner: crate::ClientId,
296        session: &'a mut Session,
297        client: crate::ClientContext<'a>,
298        kernel: &'a KernelContext,
299        executor: &'a dyn CommandExecutor,
300    ) -> Self {
301        let screen = {
302            let (width, height) = *client.terminal_size;
303            Rect::new(0, 0, width, height)
304        };
305        #[allow(clippy::cast_possible_truncation)]
306        let cursor_snapshot = client
307            .windows
308            .active()
309            .map(|w| (w.cursor.line as u32, w.cursor.column as u32));
310        Self {
311            owner: Some(owner),
312            session,
313            mode_stack: client.mode_stack,
314            windows: client.windows,
315            extensions: client.extensions,
316            shared_extensions: None,
317            compositor: client.compositor,
318            tabs: client.tabs,
319            registers: client.registers,
320            clipboard_history: client.clipboard_history,
321            local_marks: client.local_marks,
322            jumplist: client.jumplist,
323            active_buffer: client.active_buffer,
324            kernel,
325            executor,
326            screen,
327            changes: StateChanges::new(),
328            signals: Vec::new(),
329            command_depth: 0,
330            cursor_snapshot,
331        }
332    }
333
334    /// Get the client ID this runtime is bound to (#471).
335    ///
336    /// Returns `Some(ClientId)` if created with [`with_owner`], `None` otherwise.
337    /// Use this for debugging, logging, or assertions about which client owns
338    /// this runtime.
339    ///
340    /// # Example
341    ///
342    /// ```ignore
343    /// let runtime = SessionRuntime::with_owner(client_id, ...);
344    /// assert_eq!(runtime.owner(), Some(client_id));
345    ///
346    /// let shared_runtime = SessionRuntime::new(...);
347    /// assert_eq!(shared_runtime.owner(), None);
348    /// ```
349    ///
350    /// [`with_owner`]: Self::with_owner
351    #[must_use]
352    pub const fn owner(&self) -> Option<crate::ClientId> {
353        self.owner
354    }
355
356    /// Set shared extensions for session-wide state access (#543).
357    ///
358    /// Called by server code that has access to `AppState.extensions`.
359    /// Enables `shared_ext()` / `shared_ext_mut()` in commands.
360    #[must_use]
361    pub const fn with_shared_extensions(mut self, extensions: &'a mut crate::ExtensionMap) -> Self {
362        self.shared_extensions = Some(extensions);
363        self
364    }
365
366    /// Push a lifecycle signal onto the queue (#547).
367    ///
368    /// Commands call this during execution. The server drains
369    /// the queue after the command returns and acts on the signals.
370    /// Same pattern as `StateChanges` — accumulate during execution,
371    /// drain after.
372    pub fn signal(&mut self, signal: RuntimeSignal) {
373        self.signals.push(signal);
374    }
375
376    /// Drain the signal queue, returning all accumulated signals (#547).
377    ///
378    /// Called by the server after command execution completes.
379    /// Returns the signals and empties the queue.
380    pub fn take_signals(&mut self) -> Vec<RuntimeSignal> {
381        std::mem::take(&mut self.signals)
382    }
383
384    // Note: has_client_mode_stack(), has_client_windows(), has_client_extensions()
385    // have been removed in #471 Phase 0. Per-client state is now REQUIRED,
386    // so these methods would always return true.
387
388    /// Check if compositor is available.
389    #[must_use]
390    pub fn has_compositor(&self) -> bool {
391        self.compositor.is_some()
392    }
393
394    /// Get direct access to the session.
395    #[must_use]
396    pub const fn session(&self) -> &Session {
397        self.session
398    }
399
400    /// Get mutable access to the session.
401    pub const fn session_mut(&mut self) -> &mut Session {
402        self.session
403    }
404
405    /// Get direct access to the kernel context.
406    #[must_use]
407    pub const fn kernel(&self) -> &KernelContext {
408        self.kernel
409    }
410
411    /// Execute a read-only operation on a buffer.
412    ///
413    /// This method provides temporary read access to a buffer for complex
414    /// calculations that need the full buffer interface (e.g., motion
415    /// calculations, text object matching).
416    ///
417    /// # Arguments
418    ///
419    /// * `buffer` - The buffer ID to access
420    /// * `f` - A function that receives a read guard to the buffer
421    ///
422    /// # Returns
423    ///
424    /// `Some(R)` with the function's return value if the buffer exists,
425    /// `None` if the buffer doesn't exist.
426    ///
427    /// # Example
428    ///
429    /// ```ignore
430    /// let target = runtime.with_buffer_read(buffer_id, |buffer| {
431    ///     MotionEngine::calculate(buffer, &cursor, motion, count)
432    /// });
433    /// ```
434    pub fn with_buffer_read<F, R>(&self, buffer: BufferId, f: F) -> Option<R>
435    where
436        F: FnOnce(&reovim_kernel::api::v1::Buffer) -> R,
437    {
438        let buf_arc = self.kernel.buffers.get(buffer)?;
439        let buf = buf_arc.read();
440        Some(f(&buf))
441    }
442
443    // === Option Change Tracking (#445) ===
444
445    /// Record a global option change for notification emission.
446    ///
447    /// Call this after setting a global option via `kernel.options.set()`.
448    /// The change will be emitted as an `OPTION_CHANGED` notification.
449    pub fn record_global_option_change(&mut self, name: impl Into<String>, value: OptionValue) {
450        self.changes.record_global_option_change(name, value);
451    }
452
453    /// Record a window-scoped option change for notification emission.
454    ///
455    /// Call this after setting a window option via `kernel.options.set()`.
456    /// The change will be emitted as an `OPTION_CHANGED` notification.
457    pub fn record_window_option_change(
458        &mut self,
459        name: impl Into<String>,
460        value: OptionValue,
461        window_id: WindowId,
462    ) {
463        self.changes
464            .record_window_option_change(name, value, window_id);
465    }
466
467    /// Record that a buffer was modified for notification emission.
468    ///
469    /// Call this after modifying buffer content directly (e.g., via `OperatorContext`
470    /// which bypasses `SessionRuntime`'s `BufferApi` methods like `delete_range()`).
471    pub fn record_buffer_modified(&mut self, buffer: BufferId) {
472        self.changes.record_buffer_modified(buffer);
473    }
474
475    // === Per-Client Window Accessors (#471) ===
476
477    /// Get the per-client windows.
478    ///
479    /// Phase #471: Direct access to per-client windows. No fallback, no Option.
480    /// Commands use this to access the active window's cursor position.
481    ///
482    /// # Example
483    ///
484    /// ```ignore
485    /// let Some(window) = runtime.windows().active() else {
486    ///     return CommandResult::error("No active window");
487    /// };
488    /// let pos = Position::new(window.cursor.line, window.cursor.column);
489    /// ```
490    #[must_use]
491    pub const fn windows(&self) -> &crate::WindowLayout {
492        self.windows
493    }
494
495    /// Get the per-client windows mutably.
496    ///
497    /// Phase #471: Direct access to per-client windows for cursor/selection updates.
498    ///
499    /// # Example
500    ///
501    /// ```ignore
502    /// if let Some(window) = runtime.windows_mut().active_mut() {
503    ///     window.cursor = new_pos.into();
504    ///     window.selection = Some(Selection::new(start, end, mode));
505    /// }
506    /// ```
507    #[allow(clippy::missing_const_for_fn)] // &mut self in const fn requires nightly
508    pub fn windows_mut(&mut self) -> &mut crate::WindowLayout {
509        self.windows
510    }
511
512    /// Get per-client registers (#515).
513    #[must_use]
514    pub const fn registers(&self) -> &reovim_kernel::api::v1::RegisterBank {
515        self.registers
516    }
517
518    /// Get per-client registers mutably (#515).
519    #[allow(clippy::missing_const_for_fn)]
520    pub fn registers_mut(&mut self) -> &mut reovim_kernel::api::v1::RegisterBank {
521        self.registers
522    }
523
524    /// Get per-client clipboard history (#515).
525    #[must_use]
526    pub const fn clipboard_history(&self) -> &reovim_kernel::api::v1::HistoryRing {
527        self.clipboard_history
528    }
529
530    /// Get per-client clipboard history mutably (#515).
531    #[allow(clippy::missing_const_for_fn)]
532    pub fn clipboard_history_mut(&mut self) -> &mut reovim_kernel::api::v1::HistoryRing {
533        self.clipboard_history
534    }
535
536    /// Get kernel, registers, and clipboard history together (#515).
537    ///
538    /// Splits the borrow so the caller can hold `&KernelContext` and
539    /// `&mut RegisterBank` / `&mut HistoryRing` simultaneously, which
540    /// isn't possible through separate accessor calls.
541    pub const fn kernel_and_registers(
542        &mut self,
543    ) -> (
544        &KernelContext,
545        &mut reovim_kernel::api::v1::RegisterBank,
546        &mut reovim_kernel::api::v1::HistoryRing,
547    ) {
548        (self.kernel, self.registers, self.clipboard_history)
549    }
550
551    /// Get per-client local marks (#515).
552    #[must_use]
553    pub const fn local_marks(&self) -> &reovim_kernel::api::v1::MarkBank {
554        self.local_marks
555    }
556
557    /// Get per-client local marks mutably (#515).
558    #[allow(clippy::missing_const_for_fn)]
559    pub fn local_marks_mut(&mut self) -> &mut reovim_kernel::api::v1::MarkBank {
560        self.local_marks
561    }
562
563    /// Get per-client jump list (#654).
564    #[must_use]
565    pub const fn jumplist(&self) -> &reovim_kernel::api::v1::Jumplist {
566        self.jumplist
567    }
568
569    /// Get per-client jump list mutably (#654).
570    #[allow(clippy::missing_const_for_fn)]
571    pub fn jumplist_mut(&mut self) -> &mut reovim_kernel::api::v1::Jumplist {
572        self.jumplist
573    }
574}
575
576// === ModeApi ===
577
578/// Per-client state (#471): Mode operations use the per-client mode stack directly.
579///
580/// Since per-client state is now required (no Option), all mode operations
581/// directly access `self.mode_stack` without fallback to session.
582impl ModeApi for SessionRuntime<'_> {
583    fn current_mode(&self) -> &ModeId {
584        self.mode_stack.current()
585    }
586
587    fn home_mode(&self) -> &ModeId {
588        self.mode_stack.home()
589    }
590
591    fn mode_depth(&self) -> usize {
592        self.mode_stack.depth()
593    }
594
595    fn is_mode_active(&self, mode: &ModeId) -> bool {
596        self.mode_stack.contains(mode)
597    }
598
599    fn mode_stack(&self) -> Vec<ModeId> {
600        self.mode_stack.as_slice().to_vec()
601    }
602
603    fn push_mode(&mut self, mode: ModeId, _ctx: TransitionContext) {
604        self.mode_stack.push(mode);
605        self.changes.record_mode_change();
606    }
607
608    fn pop_mode(&mut self, _result: Option<PopResult>) -> Result<(), ModeError> {
609        if self.mode_stack.depth() <= 1 {
610            return Err(ModeError::CannotPopHomeMode);
611        }
612        self.mode_stack.pop();
613        self.changes.record_mode_change();
614        Ok(())
615    }
616
617    fn set_mode(&mut self, mode: ModeId, _ctx: TransitionContext) {
618        self.mode_stack.set(mode);
619        self.changes.record_mode_change();
620    }
621}
622
623// === BufferApi ===
624
625impl BufferApi for SessionRuntime<'_> {
626    fn active_buffer(&self) -> Option<BufferId> {
627        *self.active_buffer
628    }
629
630    fn set_active_buffer(&mut self, id: Option<BufferId>) {
631        *self.active_buffer = id;
632    }
633
634    fn buffer_line(&self, buffer: BufferId, line: usize) -> Option<String> {
635        self.kernel
636            .buffers
637            .get(buffer)
638            .and_then(|buf| buf.read().line(line).map(String::from))
639    }
640
641    fn buffer_line_count(&self, buffer: BufferId) -> Option<usize> {
642        self.kernel
643            .buffers
644            .get(buffer)
645            .map(|buf| buf.read().line_count())
646    }
647
648    fn buffer_line_len(&self, buffer: BufferId, line: usize) -> Option<usize> {
649        self.kernel
650            .buffers
651            .get(buffer)
652            .and_then(|buf| buf.read().line_len(line))
653    }
654
655    #[allow(clippy::significant_drop_tightening)]
656    #[cfg_attr(coverage_nightly, coverage(off))]
657    fn buffer_text_range(
658        &self,
659        buffer: BufferId,
660        start: Position,
661        end: Position,
662    ) -> Option<String> {
663        let buf_arc = self.kernel.buffers.get(buffer)?;
664        let buf = buf_arc.read();
665
666        // Build the text from the range
667        let mut result = String::new();
668
669        if start.line == end.line {
670            // Single line case
671            if let Some(line) = buf.line(start.line) {
672                let char_len = line.chars().count();
673                let start_col = start.column.min(char_len);
674                let end_col = end.column.min(char_len);
675                if start_col < end_col {
676                    let sb = char_col_to_byte(line, start_col);
677                    let eb = char_col_to_byte(line, end_col);
678                    result.push_str(&line[sb..eb]);
679                }
680            }
681        } else {
682            // Multi-line case
683            // First line: from start column to end of line
684            if let Some(line) = buf.line(start.line) {
685                let char_len = line.chars().count();
686                let start_col = start.column.min(char_len);
687                let sb = char_col_to_byte(line, start_col);
688                result.push_str(&line[sb..]);
689                result.push('\n');
690            }
691
692            // Middle lines: full lines
693            for line_idx in (start.line + 1)..end.line {
694                if let Some(line) = buf.line(line_idx) {
695                    result.push_str(line);
696                    result.push('\n');
697                }
698            }
699
700            // Last line: from start to end column
701            if let Some(line) = buf.line(end.line) {
702                let char_len = line.chars().count();
703                let end_col = end.column.min(char_len);
704                let eb = char_col_to_byte(line, end_col);
705                result.push_str(&line[..eb]);
706            }
707        }
708
709        Some(result)
710    }
711
712    fn buffer_content(&self, buffer: BufferId) -> Option<String> {
713        self.kernel
714            .buffers
715            .get(buffer)
716            .map(|buf| buf.read().content())
717    }
718
719    fn buffer_file_path(&self, buffer: BufferId) -> Option<String> {
720        self.kernel
721            .buffers
722            .get(buffer)
723            .and_then(|buf| buf.read().file_path().map(String::from))
724    }
725
726    fn is_buffer_modified(&self, buffer: BufferId) -> Option<bool> {
727        self.kernel
728            .buffers
729            .get(buffer)
730            .map(|buf| buf.read().is_modified())
731    }
732
733    fn set_buffer_modified(&mut self, buffer: BufferId, modified: bool) {
734        if let Some(buf) = self.kernel.buffers.get(buffer) {
735            buf.write().set_modified(modified);
736        }
737    }
738
739    fn insert_text(&mut self, buffer: BufferId, pos: Position, text: &str) {
740        if let Some(buf) = self.kernel.buffers.get(buffer) {
741            // Get cursor from per-client active window (#471)
742            // Note: cursor_after will be set by runner from CommandResult
743            let cursor_before = self.windows().active().map_or_else(
744                || Position::new(0, 0),
745                |w| Position::new(w.cursor.line, w.cursor.column),
746            );
747
748            // Compute byte offset BEFORE mutation (#655)
749            let byte_offset = buf.read().position_to_byte(pos);
750
751            buf.write().insert_at(pos, text);
752
753            // For undo, use cursor_before as cursor_after too (runner will update actual cursor)
754            let cursor_after = cursor_before;
755
756            // Record edit for undo - use record_edit_mine for per-client undo (#471)
757            let edit = Edit::Insert {
758                position: pos,
759                text: text.to_string(),
760            };
761            self.record_edit_mine(buffer, vec![edit], cursor_before, cursor_after);
762
763            // Emit BufferModified event for subscribers (#655, matches delete_range pattern)
764            #[allow(clippy::cast_possible_truncation)]
765            {
766                use reovim_kernel::api::v1::events::kernel::{BufferModified, Modification};
767                let modification = Modification::Insert {
768                    start: (pos.line as u32, pos.column as u32),
769                    text: text.to_string(),
770                    start_byte: byte_offset,
771                };
772                self.kernel.event_bus.emit(BufferModified {
773                    buffer_id: buffer.as_usize() as u64,
774                    modification: modification.clone(),
775                });
776                self.changes
777                    .record_buffer_modified_with_edit(buffer, modification);
778            }
779        }
780    }
781
782    fn delete_range(&mut self, buffer: BufferId, start: Position, end: Position) {
783        if let Some(buf) = self.kernel.buffers.get(buffer) {
784            // Get cursor from per-client active window (#471)
785            // TODO(#471 Phase 6): cursor_after should come from CommandResult
786            let cursor_before = self.windows().active().map_or_else(
787                || Position::new(0, 0),
788                |w| Position::new(w.cursor.line, w.cursor.column),
789            );
790
791            // Compute byte offset BEFORE mutation (#655)
792            let byte_offset = buf.read().position_to_byte(start);
793
794            let deleted_text = {
795                let mut b = buf.write();
796                b.delete_range(start, end)
797            };
798
799            // For undo, use cursor_before as cursor_after too (runner will update actual cursor)
800            let cursor_after = cursor_before;
801
802            // Record edit for undo - use record_edit_mine for per-client undo (#471)
803            if deleted_text.is_empty() {
804                self.changes.record_buffer_modified(buffer);
805            } else {
806                let edit = Edit::Delete {
807                    position: start,
808                    text: deleted_text.clone(),
809                };
810                self.record_edit_mine(buffer, vec![edit], cursor_before, cursor_after);
811
812                // Emit BufferModified event for pair module and other subscribers (#440)
813                #[allow(clippy::cast_possible_truncation)]
814                {
815                    use reovim_kernel::api::v1::events::kernel::{BufferModified, Modification};
816                    let modification = Modification::Delete {
817                        start: (start.line as u32, start.column as u32),
818                        end: (end.line as u32, end.column as u32),
819                        text: deleted_text,
820                        start_byte: byte_offset,
821                    };
822                    self.kernel.event_bus.emit(BufferModified {
823                        buffer_id: buffer.as_usize() as u64,
824                        modification: modification.clone(),
825                    });
826                    self.changes
827                        .record_buffer_modified_with_edit(buffer, modification);
828                }
829            }
830        }
831    }
832
833    #[cfg_attr(coverage_nightly, coverage(off))]
834    fn replace_content(&mut self, buffer: BufferId, content: &str) {
835        if let Some(buf) = self.kernel.buffers.get(buffer) {
836            let cursor_before = self.windows().active().map_or_else(
837                || Position::new(0, 0),
838                |w| Position::new(w.cursor.line, w.cursor.column),
839            );
840
841            let old_content = buf.read().content();
842            buf.write().set_content(content);
843
844            // Record for undo as a delete-all + insert-all
845            let edits = vec![
846                Edit::Delete {
847                    position: Position::new(0, 0),
848                    text: old_content,
849                },
850                Edit::Insert {
851                    position: Position::new(0, 0),
852                    text: content.to_string(),
853                },
854            ];
855            self.record_edit_mine(buffer, edits, cursor_before, cursor_before);
856
857            // Emit BufferModified for subscribers
858            #[allow(clippy::cast_possible_truncation)]
859            {
860                use reovim_kernel::api::v1::events::kernel::{BufferModified, Modification};
861                let modification = Modification::FullReplace;
862                self.kernel.event_bus.emit(BufferModified {
863                    buffer_id: buffer.as_usize() as u64,
864                    modification: modification.clone(),
865                });
866                self.changes
867                    .record_buffer_modified_with_edit(buffer, modification);
868            }
869        }
870    }
871
872    fn create_buffer(&mut self, name: Option<&str>, content: &str) -> BufferId {
873        use reovim_kernel::api::v1::Buffer;
874
875        let mut buffer = Buffer::from_string(content);
876        if let Some(name) = name {
877            buffer.set_file_path(Some(name.to_string()));
878        }
879        let id = self.kernel.buffers.register(buffer);
880        self.changes.record_buffer_created(id);
881        id
882    }
883
884    fn delete_buffer(&mut self, buffer: BufferId) -> Result<(), BufferError> {
885        if self.kernel.buffers.count() <= 1 {
886            return Err(BufferError::CannotDeleteLastBuffer);
887        }
888        if self.kernel.buffers.unregister(buffer).is_err() {
889            return Err(BufferError::NotFound(buffer));
890        }
891        self.changes.record_buffer_deleted(buffer);
892        Ok(())
893    }
894
895    fn rename_buffer(&mut self, buffer: BufferId, new_name: &str) {
896        if let Some(buf) = self.kernel.buffers.get(buffer) {
897            buf.write().set_file_path(Some(new_name.to_string()));
898            self.changes
899                .record_buffer_renamed(buffer, new_name.to_string());
900        }
901    }
902}
903
904// === WindowApi ===
905
906impl WindowApi for SessionRuntime<'_> {
907    fn active_window(&self) -> Option<WindowId> {
908        self.windows.active_id() // #491: use per-client windows
909    }
910
911    fn cursor_position(&self) -> Option<Position> {
912        let window = self.windows().active()?;
913        Some(Position::new(window.cursor.line, window.cursor.column))
914    }
915
916    fn window_count(&self) -> usize {
917        self.windows.len() // #491: use per-client windows
918    }
919
920    fn window_buffer(&self, window: WindowId) -> Option<BufferId> {
921        self.windows.get(window).and_then(|w| w.buffer_id) // #491: use per-client windows
922    }
923
924    fn create_window(&mut self, buffer: Option<BufferId>) -> WindowId {
925        let mut window = Window::new();
926        window.buffer_id = buffer;
927        let id = window.id;
928        self.windows.add(window); // #491: use per-client windows
929        self.changes.record_window_created(id);
930        id
931    }
932
933    fn close_window(&mut self, window: WindowId) -> Result<(), WindowError> {
934        if self.windows.len() <= 1 {
935            // #491: use per-client windows
936            return Err(WindowError::CannotCloseLastWindow);
937        }
938        if self.windows.remove(window) {
939            self.changes.record_window_closed(window);
940            Ok(())
941        } else {
942            Err(WindowError::NotFound(window))
943        }
944    }
945
946    fn focus_window(&mut self, window: WindowId) -> Result<(), WindowError> {
947        if !self.windows.set_active(window) {
948            // #491: use per-client windows
949            return Err(WindowError::NotFound(window));
950        }
951        self.changes.record_focus_change();
952        Ok(())
953    }
954
955    fn set_window_buffer(&mut self, window: WindowId, buffer: BufferId) -> Result<(), WindowError> {
956        if self.kernel.buffers.get(buffer).is_none() {
957            return Err(WindowError::BufferNotFound(buffer));
958        }
959        if let Some(w) = self.windows.get_mut(window) {
960            // #491: use per-client windows
961            // Phase 8 (#465): Clear selection when switching buffers.
962            // Selection is per-window but associated with a specific buffer,
963            // so it makes no sense to keep selection when viewing a different buffer.
964            w.selection = None;
965            w.buffer_id = Some(buffer);
966            self.changes.window_changed = true;
967            Ok(())
968        } else {
969            Err(WindowError::NotFound(window))
970        }
971    }
972
973    fn set_active_selection(&mut self, selection: Option<Selection>) {
974        if let Some(w) = self.windows.active_mut() {
975            w.selection = selection;
976        }
977    }
978
979    fn active_selection(&self) -> Option<&Selection> {
980        self.windows.active().and_then(|w| w.selection.as_ref())
981    }
982}
983
984// === RegisterApi ===
985
986impl RegisterApi for SessionRuntime<'_> {
987    fn get_register(&self, name: Option<char>) -> Option<RegisterContent> {
988        match name {
989            // Numbered registers (0-9) - per-client yank history (#515)
990            Some(n) if n.is_ascii_digit() => self.clipboard_history.get_numbered(n),
991
992            // All other registers (a-z, A-Z, +, *, "", None) - per-client RegisterBank (#515)
993            //
994            // Note: +/* are stored locally. Callers that need OS clipboard sync
995            // must explicitly use ClipboardApi (#515 Phase 4).
996            _ => self.registers.get_by_name(name).cloned(),
997        }
998    }
999
1000    fn set_register(&mut self, name: Option<char>, content: RegisterContent) {
1001        match name {
1002            // Numbered registers (0-9) are read-only (populated by history)
1003            Some(n) if n.is_ascii_digit() => {
1004                // Ignore writes to numbered registers - they're managed by history
1005            }
1006
1007            // All other registers - per-client RegisterBank (#515)
1008            _ => {
1009                self.registers.set_by_name(name, content);
1010            }
1011        }
1012    }
1013}
1014
1015// === ClipboardApi (#515) ===
1016
1017#[cfg_attr(coverage_nightly, coverage(off))]
1018impl ClipboardApi for SessionRuntime<'_> {
1019    fn copy_to_clipboard(&self, text: &str) -> bool {
1020        if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
1021            && let Some(provider) = registry.get(&ClipboardKey::Default)
1022        {
1023            provider.copy_to_clipboard(text).is_ok()
1024        } else {
1025            false
1026        }
1027    }
1028
1029    fn paste_from_clipboard(&self) -> Option<String> {
1030        if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
1031            && let Some(provider) = registry.get(&ClipboardKey::Default)
1032        {
1033            provider.paste_from_clipboard().ok().flatten()
1034        } else {
1035            None
1036        }
1037    }
1038
1039    fn copy_to_selection(&self, text: &str) -> bool {
1040        if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
1041            && let Some(provider) = registry.get(&ClipboardKey::Default)
1042        {
1043            provider.copy_to_selection(text).is_ok()
1044        } else {
1045            false
1046        }
1047    }
1048
1049    fn paste_from_selection(&self) -> Option<String> {
1050        if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
1051            && let Some(provider) = registry.get(&ClipboardKey::Default)
1052        {
1053            provider.paste_from_selection().ok().flatten()
1054        } else {
1055            None
1056        }
1057    }
1058}
1059
1060impl SessionRuntime<'_> {
1061    /// Push content to the per-client clipboard history (#515).
1062    ///
1063    /// Should be called after every yank/delete operation to populate
1064    /// numbered registers (0-9). This is an inherent method because
1065    /// history is per-client state, not a shared service.
1066    pub fn push_to_clipboard_history(&mut self, content: RegisterContent) {
1067        self.clipboard_history.push(content);
1068    }
1069
1070    /// Store content to register with clipboard sync (#515 Phase 4).
1071    ///
1072    /// Coordinates `RegisterApi` and `ClipboardApi`:
1073    /// 1. Stores content in per-client register via `RegisterApi`
1074    /// 2. Syncs to OS clipboard for `+`/`*` registers via `ClipboardApi`
1075    /// 3. Pushes to clipboard history for numbered register rotation
1076    ///
1077    /// Use this from commands that operate on `SessionRuntime` directly
1078    /// (visual mode operators, editor commands). Vim operators that hold
1079    /// separate borrows via `OperatorContext` use `registers::store_and_sync`
1080    /// instead.
1081    pub fn store_register_with_sync(&mut self, register: Option<char>, content: RegisterContent) {
1082        self.set_register(register, content.clone());
1083        match register {
1084            Some('+') => {
1085                self.copy_to_clipboard(&content.text);
1086            }
1087            Some('*') => {
1088                self.copy_to_selection(&content.text);
1089            }
1090            _ => {}
1091        }
1092        self.push_to_clipboard_history(content);
1093    }
1094
1095    /// Get register content with clipboard fallback for `+`/`*` (#515 Phase 4).
1096    ///
1097    /// For system clipboard registers:
1098    /// - `+`: reads from OS clipboard, falls back to per-client register
1099    /// - `*`: reads from OS selection, falls back to per-client register
1100    /// - All others: reads directly from per-client `RegisterBank`
1101    pub fn get_register_with_clipboard(&self, register: Option<char>) -> Option<RegisterContent> {
1102        match register {
1103            Some('+') => self
1104                .paste_from_clipboard()
1105                .map(RegisterContent::characterwise)
1106                .or_else(|| self.get_register(register)),
1107            Some('*') => self
1108                .paste_from_selection()
1109                .map(RegisterContent::characterwise)
1110                .or_else(|| self.get_register(register)),
1111            _ => self.get_register(register),
1112        }
1113    }
1114}
1115
1116// === UndoApi ===
1117
1118impl SessionRuntime<'_> {
1119    /// Apply undo/redo edits to the kernel buffer.
1120    ///
1121    /// Extracted from the 4 undo/redo methods to deduplicate the edit-application
1122    /// loop and avoid an LLVM coverage gap-region bug on `if let` closing braces.
1123    fn apply_undo_edits(&self, buffer: BufferId, edits: &[Edit]) {
1124        let Some(buf) = self.kernel.buffers.get(buffer) else {
1125            return;
1126        };
1127        let mut buf = buf.write();
1128        for edit in edits {
1129            match edit {
1130                Edit::Insert { position, text } => {
1131                    buf.insert_at(*position, text);
1132                }
1133                Edit::Delete { position, text } => {
1134                    buf.delete_at(*position, text.chars().count());
1135                }
1136            }
1137        }
1138    }
1139}
1140
1141impl UndoApi for SessionRuntime<'_> {
1142    #[cfg_attr(coverage_nightly, coverage(off))]
1143    fn undo(&mut self, buffer: BufferId) -> Option<UndoResult> {
1144        let undo_provider = self
1145            .kernel
1146            .services
1147            .get::<UndoProviderRegistry>()?
1148            .get(&UndoKey::Buffer)?;
1149
1150        let result = undo_provider.undo(buffer)?;
1151        self.apply_undo_edits(buffer, &result.edits);
1152
1153        // Phase #471: Restore cursor to per-client active window
1154        if let Some(window) = self.windows_mut().active_mut() {
1155            window.cursor.line = result.cursor.line;
1156            window.cursor.column = result.cursor.column;
1157        }
1158
1159        self.changes.record_buffer_modified(buffer);
1160        self.changes.record_cursor_move(buffer);
1161
1162        Some(result)
1163    }
1164
1165    #[cfg_attr(coverage_nightly, coverage(off))]
1166    fn redo(&mut self, buffer: BufferId) -> Option<UndoResult> {
1167        let undo_provider = self
1168            .kernel
1169            .services
1170            .get::<UndoProviderRegistry>()?
1171            .get(&UndoKey::Buffer)?;
1172
1173        let result = undo_provider.redo(buffer)?;
1174        self.apply_undo_edits(buffer, &result.edits);
1175
1176        // Phase #471: Restore cursor to per-client active window
1177        if let Some(window) = self.windows_mut().active_mut() {
1178            window.cursor.line = result.cursor.line;
1179            window.cursor.column = result.cursor.column;
1180        }
1181
1182        self.changes.record_buffer_modified(buffer);
1183        self.changes.record_cursor_move(buffer);
1184
1185        Some(result)
1186    }
1187
1188    #[cfg_attr(coverage_nightly, coverage(off))]
1189    fn record_edit(
1190        &mut self,
1191        buffer: BufferId,
1192        edits: Vec<Edit>,
1193        cursor_before: Position,
1194        cursor_after: Position,
1195    ) {
1196        if let Some(undo_registry) = self.kernel.services.get::<UndoProviderRegistry>()
1197            && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
1198        {
1199            undo_provider.record(buffer, edits, cursor_before, cursor_after);
1200        }
1201    }
1202
1203    fn can_undo(&self, buffer: BufferId) -> bool {
1204        self.kernel
1205            .services
1206            .get::<UndoProviderRegistry>()
1207            .and_then(|registry| registry.get(&UndoKey::Buffer))
1208            .and_then(|provider| provider.get_tree(buffer))
1209            .is_some_and(|tree| tree.can_undo())
1210    }
1211
1212    fn can_redo(&self, buffer: BufferId) -> bool {
1213        self.kernel
1214            .services
1215            .get::<UndoProviderRegistry>()
1216            .and_then(|registry| registry.get(&UndoKey::Buffer))
1217            .and_then(|provider| provider.get_tree(buffer))
1218            .is_some_and(|tree| tree.can_redo())
1219    }
1220
1221    #[cfg_attr(coverage_nightly, coverage(off))]
1222    fn undo_mine(&mut self, buffer: BufferId) -> Option<UndoResult> {
1223        // #471: Get the client ID from the owner field
1224        let client_id = self.owner?.as_usize();
1225
1226        let undo_provider = self
1227            .kernel
1228            .services
1229            .get::<UndoProviderRegistry>()?
1230            .get(&UndoKey::Buffer)?;
1231
1232        let result = undo_provider.undo_for_client(buffer, client_id)?;
1233        self.apply_undo_edits(buffer, &result.edits);
1234
1235        // Restore cursor to per-client active window
1236        if let Some(window) = self.windows_mut().active_mut() {
1237            window.cursor.line = result.cursor.line;
1238            window.cursor.column = result.cursor.column;
1239        }
1240
1241        self.changes.record_buffer_modified(buffer);
1242        self.changes.record_cursor_move(buffer);
1243
1244        Some(result)
1245    }
1246
1247    #[cfg_attr(coverage_nightly, coverage(off))]
1248    fn redo_mine(&mut self, buffer: BufferId) -> Option<UndoResult> {
1249        // #471: Get the client ID from the owner field
1250        let client_id = self.owner?.as_usize();
1251
1252        let undo_provider = self
1253            .kernel
1254            .services
1255            .get::<UndoProviderRegistry>()?
1256            .get(&UndoKey::Buffer)?;
1257
1258        let result = undo_provider.redo_for_client(buffer, client_id)?;
1259        self.apply_undo_edits(buffer, &result.edits);
1260
1261        // Restore cursor to per-client active window
1262        if let Some(window) = self.windows_mut().active_mut() {
1263            window.cursor.line = result.cursor.line;
1264            window.cursor.column = result.cursor.column;
1265        }
1266
1267        self.changes.record_buffer_modified(buffer);
1268        self.changes.record_cursor_move(buffer);
1269
1270        Some(result)
1271    }
1272
1273    #[cfg_attr(coverage_nightly, coverage(off))]
1274    fn record_edit_mine(
1275        &mut self,
1276        buffer: BufferId,
1277        edits: Vec<Edit>,
1278        cursor_before: Position,
1279        cursor_after: Position,
1280    ) {
1281        // #471: If we have an owner, use record_for_client with origin tagging
1282        if let Some(client_id) = self.owner {
1283            if let Some(undo_registry) = self.kernel.services.get::<UndoProviderRegistry>()
1284                && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
1285            {
1286                undo_provider.record_for_client(
1287                    buffer,
1288                    client_id.as_usize(),
1289                    edits,
1290                    cursor_before,
1291                    cursor_after,
1292                );
1293            }
1294        } else {
1295            // Fall back to regular record without origin
1296            self.record_edit(buffer, edits, cursor_before, cursor_after);
1297        }
1298    }
1299}
1300
1301// === CommandApi ===
1302
1303impl CommandApi for SessionRuntime<'_> {
1304    fn execute_command(&mut self, cmd: CommandId, ctx: CommandContext) -> CommandResult {
1305        // Recursion guard (#547): max depth 16.
1306        if self.command_depth >= 16 {
1307            return CommandResult::Error(
1308                "command recursion limit exceeded (max depth: 16)".to_string(),
1309            );
1310        }
1311        self.command_depth += 1;
1312
1313        // get_handle() returns an owned Arc, releasing the borrow on
1314        // self.executor. This enables re-entrant execution: the returned
1315        // handle can call back into self via handle.execute(self, &ctx).
1316        let handle = self.executor.get_handle(&cmd);
1317        let result = handle.map_or_else(
1318            || CommandResult::Error(format!("command not found: {cmd:?}")),
1319            |handle| handle.execute(self, &ctx),
1320        );
1321
1322        self.command_depth -= 1;
1323        result
1324    }
1325}
1326
1327// === ExtensionApi ===
1328
1329/// Per-client extensions (#477, #471 Phase 0).
1330///
1331/// Extensions are now ALWAYS per-client (no fallback to shared session).
1332/// This enforces complete module state isolation between clients. For example,
1333/// `VimSessionState.pending_count` is per-client, so Client A pressing `5`
1334/// doesn't affect Client B's motions.
1335impl ExtensionApi for SessionRuntime<'_> {
1336    fn ext<T: SessionExtension>(&self) -> Option<&T> {
1337        // #471 Phase 0: Per-client extensions are required, direct access
1338        self.extensions.get::<T>()
1339    }
1340
1341    fn ext_mut<T: SessionExtension>(&mut self) -> &mut T {
1342        // #471 Phase 0: Per-client extensions are required, direct access
1343        self.extensions.get_or_insert::<T>()
1344    }
1345
1346    fn shared_ext<T: SessionExtension>(&self) -> Option<&T> {
1347        self.shared_extensions.as_ref().and_then(|m| m.get::<T>())
1348    }
1349
1350    fn shared_ext_mut<T: SessionExtension>(&mut self) -> Option<&mut T> {
1351        self.shared_extensions
1352            .as_mut()
1353            .map(|m| m.get_or_insert::<T>())
1354    }
1355}
1356
1357// === ChangeTracker ===
1358
1359impl ChangeTracker for SessionRuntime<'_> {
1360    fn take_changes(&mut self) -> StateChanges {
1361        std::mem::take(&mut self.changes)
1362    }
1363
1364    fn record_cursor_move(&mut self, buffer: BufferId) {
1365        // #664: Emit CursorMoved event for subscribers (illuminate, etc.).
1366        // `cursor_snapshot` holds the pre-command position (captured at construction)
1367        // or the post-previous-move position (updated after each emission).
1368        #[allow(clippy::cast_possible_truncation)]
1369        if let Some(window) = self.windows.active() {
1370            let to = (window.cursor.line as u32, window.cursor.column as u32);
1371            let from = self.cursor_snapshot.unwrap_or(to);
1372            self.kernel.event_bus.emit(CursorMoved {
1373                buffer_id: buffer.as_usize() as u64,
1374                from,
1375                to,
1376            });
1377            self.cursor_snapshot = Some(to);
1378        }
1379
1380        self.changes.record_cursor_move(buffer);
1381        // #474: Centralized visual selection extension.
1382        // When cursor moves and selection exists, auto-update sel.end
1383        // to match cursor position. This ensures ALL commands that call
1384        // record_cursor_move() automatically extend visual selection.
1385        // Note: For line-wise selections, operators normalize end via
1386        // expand_selection_range(), so the column+1 here is harmless.
1387        if let Some(window) = self.windows.active_mut()
1388            && let Some(ref mut sel) = window.selection
1389        {
1390            sel.end = Position::new(window.cursor.line, window.cursor.column + 1);
1391            self.changes.record_selection_change(buffer);
1392        }
1393    }
1394
1395    fn record_selection_change(&mut self, buffer: BufferId) {
1396        self.changes.record_selection_change(buffer);
1397    }
1398}
1399
1400// === CompositorApi ===
1401
1402/// Convert driver `SplitDirection` to kernel `SplitDirection`.
1403const fn to_kernel_split_direction(dir: SplitDirection) -> KernelSplitDirection {
1404    match dir {
1405        SplitDirection::Horizontal => KernelSplitDirection::Horizontal,
1406        SplitDirection::Vertical => KernelSplitDirection::Vertical,
1407    }
1408}
1409
1410impl SessionRuntime<'_> {
1411    /// Emit a `LayoutChanged` event via the kernel's event bus.
1412    fn emit_layout_event(&self, kind: LayoutChangeKind) {
1413        let (window_count, focused_window) = self
1414            .compositor
1415            .as_ref()
1416            .map_or((0, None), |c| (c.window_count(), c.focused().map(|id| id.as_usize() as u64)));
1417        self.kernel.event_bus.emit(LayoutChanged {
1418            kind,
1419            window_count,
1420            focused_window,
1421        });
1422    }
1423}
1424
1425impl CompositorApi for SessionRuntime<'_> {
1426    fn navigate(&self, direction: NavigateDirection) -> Result<WindowId, CompositorError> {
1427        let compositor = self
1428            .compositor
1429            .as_ref()
1430            .ok_or(CompositorError::NoActiveLayer)?;
1431
1432        let active = compositor
1433            .active_layer()
1434            .ok_or(CompositorError::NoActiveLayer)?;
1435
1436        let layer = compositor
1437            .layer_compositor(active)
1438            .ok_or(CompositorError::LayerNotFound(active))?;
1439
1440        let from = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1441
1442        layer
1443            .navigate_tiled(from, direction)
1444            .ok_or(CompositorError::NoNeighbor(direction))
1445    }
1446
1447    #[cfg_attr(coverage_nightly, coverage(off))]
1448    fn split(&mut self, direction: SplitDirection) -> Result<WindowId, CompositorError> {
1449        let compositor = self
1450            .compositor
1451            .as_mut()
1452            .ok_or(CompositorError::NoActiveLayer)?;
1453
1454        let active = compositor
1455            .active_layer()
1456            .ok_or(CompositorError::NoActiveLayer)?;
1457
1458        let layer = compositor
1459            .layer_compositor_mut(active)
1460            .ok_or(CompositorError::LayerNotFound(active))?;
1461
1462        let from = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1463
1464        let new_window = layer
1465            .split_tiled(from, direction)
1466            .ok_or(CompositorError::NotEnoughRoom)?;
1467
1468        // Add new window to per-client WindowLayout inheriting cursor/viewport
1469        // from source. The compositor tracks geometry; WindowLayout tracks
1470        // buffer/cursor/selection. (#692)
1471        if let Some(source) = self.windows.get(from) {
1472            let win = Window::split_from(new_window, source);
1473            self.windows.add(win);
1474        }
1475
1476        // Vim behavior: focus stays on original window after split.
1477        // Re-focus compositor back to original (split_tiled auto-focused new).
1478        if let Some(compositor) = self.compositor.as_mut()
1479            && let Some(active) = compositor.active_layer()
1480            && let Some(layer) = compositor.layer_compositor_mut(active)
1481        {
1482            layer.set_focus(from);
1483        }
1484        self.windows.set_active(from);
1485
1486        self.changes.record_window_created(new_window);
1487
1488        // Emit layout changed event
1489        self.emit_layout_event(LayoutChangeKind::Split {
1490            new_window: new_window.as_usize() as u64,
1491            direction: to_kernel_split_direction(direction),
1492        });
1493
1494        Ok(new_window)
1495    }
1496
1497    #[cfg_attr(coverage_nightly, coverage(off))]
1498    fn close_current_window(&mut self) -> Result<WindowId, CompositorError> {
1499        use reovim_driver_layout::Zone;
1500
1501        let compositor = self
1502            .compositor
1503            .as_mut()
1504            .ok_or(CompositorError::NoActiveLayer)?;
1505
1506        let active = compositor
1507            .active_layer()
1508            .ok_or(CompositorError::NoActiveLayer)?;
1509
1510        let layer = compositor
1511            .layer_compositor_mut(active)
1512            .ok_or(CompositorError::LayerNotFound(active))?;
1513
1514        let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1515
1516        // Check if this is the last window
1517        if layer.windows_in_zone(Zone::Tiled).len() <= 1 {
1518            return Err(CompositorError::CannotCloseLastWindow);
1519        }
1520
1521        let neighbor = layer
1522            .close_tiled(current)
1523            .ok_or(CompositorError::CannotCloseLastWindow)?;
1524
1525        // Remove closed window from per-client WindowLayout
1526        self.windows.remove(current);
1527        self.windows.set_active(neighbor);
1528
1529        self.changes.record_window_closed(current);
1530
1531        // Emit layout changed event
1532        self.emit_layout_event(LayoutChangeKind::Close {
1533            closed_window: current.as_usize() as u64,
1534            new_focus: Some(neighbor.as_usize() as u64),
1535        });
1536
1537        Ok(neighbor)
1538    }
1539
1540    #[cfg_attr(coverage_nightly, coverage(off))]
1541    fn close_others(&mut self) -> Result<(), CompositorError> {
1542        use reovim_driver_layout::Zone;
1543
1544        let compositor = self
1545            .compositor
1546            .as_mut()
1547            .ok_or(CompositorError::NoActiveLayer)?;
1548
1549        let active = compositor
1550            .active_layer()
1551            .ok_or(CompositorError::NoActiveLayer)?;
1552
1553        let layer = compositor
1554            .layer_compositor_mut(active)
1555            .ok_or(CompositorError::LayerNotFound(active))?;
1556
1557        let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1558
1559        // Get all windows except current
1560        let windows: Vec<WindowId> = layer
1561            .windows_in_zone(Zone::Tiled)
1562            .into_iter()
1563            .filter(|&w| w != current)
1564            .collect();
1565
1566        // Close all other windows and emit events
1567        for window in &windows {
1568            layer.close_tiled(*window);
1569            self.windows.remove(*window);
1570            self.changes.record_window_closed(*window);
1571        }
1572
1573        // Emit a single layout changed event for the batch close operation
1574        // Use the last closed window in the event (if any windows were closed)
1575        if let Some(&last_closed) = windows.last() {
1576            self.emit_layout_event(LayoutChangeKind::Close {
1577                closed_window: last_closed.as_usize() as u64,
1578                new_focus: Some(current.as_usize() as u64),
1579            });
1580        }
1581
1582        Ok(())
1583    }
1584
1585    fn resize(&mut self, direction: NavigateDirection, delta: i16) -> Result<(), CompositorError> {
1586        let compositor = self
1587            .compositor
1588            .as_mut()
1589            .ok_or(CompositorError::NoActiveLayer)?;
1590
1591        let active = compositor
1592            .active_layer()
1593            .ok_or(CompositorError::NoActiveLayer)?;
1594
1595        let layer = compositor
1596            .layer_compositor_mut(active)
1597            .ok_or(CompositorError::LayerNotFound(active))?;
1598
1599        let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1600
1601        layer.resize_tiled(current, direction, delta);
1602        self.changes.window_changed = true;
1603
1604        // Emit layout changed event
1605        self.emit_layout_event(LayoutChangeKind::Resize {
1606            window: current.as_usize() as u64,
1607        });
1608
1609        Ok(())
1610    }
1611
1612    fn equalize(&mut self) -> Result<(), CompositorError> {
1613        let compositor = self
1614            .compositor
1615            .as_mut()
1616            .ok_or(CompositorError::NoActiveLayer)?;
1617
1618        let active = compositor
1619            .active_layer()
1620            .ok_or(CompositorError::NoActiveLayer)?;
1621
1622        let layer = compositor
1623            .layer_compositor_mut(active)
1624            .ok_or(CompositorError::LayerNotFound(active))?;
1625
1626        layer.equalize_tiled();
1627        self.changes.window_changed = true;
1628
1629        // Emit layout changed event
1630        self.emit_layout_event(LayoutChangeKind::Equalize);
1631
1632        Ok(())
1633    }
1634
1635    fn cycle(&self, forward: bool) -> Result<WindowId, CompositorError> {
1636        let compositor = self
1637            .compositor
1638            .as_ref()
1639            .ok_or(CompositorError::NoActiveLayer)?;
1640
1641        let active = compositor
1642            .active_layer()
1643            .ok_or(CompositorError::NoActiveLayer)?;
1644
1645        let layer = compositor
1646            .layer_compositor(active)
1647            .ok_or(CompositorError::LayerNotFound(active))?;
1648
1649        let from = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1650
1651        layer
1652            .cycle_tiled(from, forward)
1653            .ok_or(CompositorError::NoFocusedWindow)
1654    }
1655
1656    #[cfg_attr(coverage_nightly, coverage(off))]
1657    fn focus(&mut self, window: WindowId) -> Result<(), CompositorError> {
1658        let compositor = self
1659            .compositor
1660            .as_mut()
1661            .ok_or(CompositorError::NoActiveLayer)?;
1662
1663        // Get the previous focused window before changing focus
1664        let previous_focus = compositor.focused();
1665
1666        // set_focus also activates the layer containing the window
1667        compositor.set_focus(window);
1668
1669        // Sync per-client WindowLayout active window and active_buffer.
1670        // Without this, cursor notifications use the old window's buffer_id
1671        // and build_cursor_notification finds the wrong window.
1672        self.windows.set_active(window);
1673        if let Some(buffer_id) = self.windows.active().and_then(|w| w.buffer_id) {
1674            *self.active_buffer = Some(buffer_id);
1675        }
1676
1677        self.changes.record_focus_change();
1678
1679        // Emit layout changed event (only if focus actually changed)
1680        if previous_focus != Some(window) {
1681            self.emit_layout_event(LayoutChangeKind::Focus {
1682                from: previous_focus.map(|w| w.as_usize() as u64),
1683                to: window.as_usize() as u64,
1684            });
1685        }
1686
1687        Ok(())
1688    }
1689
1690    fn focused_window(&self) -> Option<WindowId> {
1691        self.compositor.as_ref()?.focused()
1692    }
1693
1694    fn compositor_window_count(&self) -> usize {
1695        self.compositor.as_ref().map_or(0, |c| c.window_count())
1696    }
1697
1698    fn arrange(&self, screen: Rect) -> Vec<WindowPlacement> {
1699        self.compositor
1700            .as_ref()
1701            .map_or_else(Vec::new, |c| c.composite(screen).placements)
1702    }
1703
1704    fn active_layer(&self) -> Option<LayerId> {
1705        self.compositor.as_ref()?.active_layer()
1706    }
1707
1708    fn set_screen(&mut self, screen: Rect) {
1709        self.screen = screen;
1710        if let Some(compositor) = self.compositor.as_mut() {
1711            compositor.set_screen(screen);
1712        }
1713    }
1714
1715    // =========================================================================
1716    // Float Zone Operations (#398)
1717    // =========================================================================
1718
1719    fn toggle_float(&mut self) -> Result<(), CompositorError> {
1720        let compositor = self
1721            .compositor
1722            .as_mut()
1723            .ok_or(CompositorError::NoActiveLayer)?;
1724
1725        let active = compositor
1726            .active_layer()
1727            .ok_or(CompositorError::NoActiveLayer)?;
1728
1729        let layer = compositor
1730            .layer_compositor_mut(active)
1731            .ok_or(CompositorError::LayerNotFound(active))?;
1732
1733        let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1734
1735        layer.toggle_float(current);
1736        self.changes.window_changed = true;
1737
1738        Ok(())
1739    }
1740
1741    fn raise_float(&mut self) -> Result<(), CompositorError> {
1742        let compositor = self
1743            .compositor
1744            .as_mut()
1745            .ok_or(CompositorError::NoActiveLayer)?;
1746
1747        let active = compositor
1748            .active_layer()
1749            .ok_or(CompositorError::NoActiveLayer)?;
1750
1751        let layer = compositor
1752            .layer_compositor_mut(active)
1753            .ok_or(CompositorError::LayerNotFound(active))?;
1754
1755        let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1756
1757        layer.raise_float(current);
1758
1759        Ok(())
1760    }
1761
1762    fn lower_float(&mut self) -> Result<(), CompositorError> {
1763        let compositor = self
1764            .compositor
1765            .as_mut()
1766            .ok_or(CompositorError::NoActiveLayer)?;
1767
1768        let active = compositor
1769            .active_layer()
1770            .ok_or(CompositorError::NoActiveLayer)?;
1771
1772        let layer = compositor
1773            .layer_compositor_mut(active)
1774            .ok_or(CompositorError::LayerNotFound(active))?;
1775
1776        let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
1777
1778        layer.lower_float(current);
1779
1780        Ok(())
1781    }
1782
1783    // =========================================================================
1784    // Overlay Zone Operations (#399)
1785    // =========================================================================
1786
1787    fn show_overlay(
1788        &mut self,
1789        constraints: OverlayConstraints,
1790    ) -> Result<WindowId, CompositorError> {
1791        let compositor = self
1792            .compositor
1793            .as_mut()
1794            .ok_or(CompositorError::NoActiveLayer)?;
1795
1796        let active = compositor
1797            .active_layer()
1798            .ok_or(CompositorError::NoActiveLayer)?;
1799
1800        let layer = compositor
1801            .layer_compositor_mut(active)
1802            .ok_or(CompositorError::LayerNotFound(active))?;
1803
1804        let id = layer.show_overlay(constraints);
1805        // Note: Overlays do NOT auto-focus
1806        Ok(id)
1807    }
1808
1809    fn hide_overlay(&mut self, window: WindowId) -> Result<(), CompositorError> {
1810        let compositor = self
1811            .compositor
1812            .as_mut()
1813            .ok_or(CompositorError::NoActiveLayer)?;
1814
1815        let active = compositor
1816            .active_layer()
1817            .ok_or(CompositorError::NoActiveLayer)?;
1818
1819        let layer = compositor
1820            .layer_compositor_mut(active)
1821            .ok_or(CompositorError::LayerNotFound(active))?;
1822
1823        layer.hide_overlay(window);
1824        Ok(())
1825    }
1826
1827    fn resize_overlay(
1828        &mut self,
1829        window: WindowId,
1830        width: u16,
1831        height: u16,
1832    ) -> Result<(), CompositorError> {
1833        let compositor = self
1834            .compositor
1835            .as_mut()
1836            .ok_or(CompositorError::NoActiveLayer)?;
1837
1838        let active = compositor
1839            .active_layer()
1840            .ok_or(CompositorError::NoActiveLayer)?;
1841
1842        let layer = compositor
1843            .layer_compositor_mut(active)
1844            .ok_or(CompositorError::LayerNotFound(active))?;
1845
1846        layer.resize_overlay(window, width, height);
1847        Ok(())
1848    }
1849
1850    fn hide_all_overlays(&mut self) -> Result<(), CompositorError> {
1851        let compositor = self
1852            .compositor
1853            .as_mut()
1854            .ok_or(CompositorError::NoActiveLayer)?;
1855
1856        let active = compositor
1857            .active_layer()
1858            .ok_or(CompositorError::NoActiveLayer)?;
1859
1860        let layer = compositor
1861            .layer_compositor_mut(active)
1862            .ok_or(CompositorError::LayerNotFound(active))?;
1863
1864        layer.hide_all_overlays();
1865        Ok(())
1866    }
1867
1868    // =========================================================================
1869    // Opacity Operations (#400)
1870    // =========================================================================
1871
1872    fn set_active_layer_opacity(&mut self, opacity: f32) -> Result<(), CompositorError> {
1873        let compositor = self
1874            .compositor
1875            .as_mut()
1876            .ok_or(CompositorError::NoActiveLayer)?;
1877
1878        let active = compositor
1879            .active_layer()
1880            .ok_or(CompositorError::NoActiveLayer)?;
1881
1882        compositor.set_layer_opacity(active, opacity.clamp(0.0, 1.0));
1883        self.changes.window_changed = true;
1884        Ok(())
1885    }
1886
1887    fn active_layer_opacity(&self) -> Result<f32, CompositorError> {
1888        let compositor = self
1889            .compositor
1890            .as_ref()
1891            .ok_or(CompositorError::NoActiveLayer)?;
1892
1893        let active = compositor
1894            .active_layer()
1895            .ok_or(CompositorError::NoActiveLayer)?;
1896
1897        let opacity = compositor
1898            .layers()
1899            .iter()
1900            .find(|l| l.id == active)
1901            .map_or(1.0, |l| l.opacity);
1902        Ok(opacity)
1903    }
1904
1905    fn adjust_active_layer_opacity(&mut self, delta: f32) -> Result<f32, CompositorError> {
1906        let current = self.active_layer_opacity()?;
1907        let new_opacity = (current + delta).clamp(0.0, 1.0);
1908        self.set_active_layer_opacity(new_opacity)?;
1909        Ok(new_opacity)
1910    }
1911
1912    // =========================================================================
1913    // Tab Page Operations (#401)
1914    //
1915    // Tab page operations — delegate to per-client TabPageSet (#401 Phase 5).
1916    // =========================================================================
1917
1918    fn tab_new(&mut self) -> Result<TabId, CompositorError> {
1919        let id = self.tabs.new_tab();
1920        self.changes.window_changed = true;
1921        Ok(id)
1922    }
1923
1924    fn tab_close(&mut self) -> Result<(), CompositorError> {
1925        if self.tabs.close_tab() {
1926            self.changes.window_changed = true;
1927            Ok(())
1928        } else {
1929            Err(CompositorError::CannotCloseLastTab)
1930        }
1931    }
1932
1933    fn tab_next(&mut self) -> Result<TabId, CompositorError> {
1934        let id = self.tabs.next_tab();
1935        self.changes.window_changed = true;
1936        Ok(id)
1937    }
1938
1939    fn tab_prev(&mut self) -> Result<TabId, CompositorError> {
1940        let id = self.tabs.prev_tab();
1941        self.changes.window_changed = true;
1942        Ok(id)
1943    }
1944
1945    fn tab_goto(&mut self, index: usize) -> Result<TabId, CompositorError> {
1946        self.tabs
1947            .goto_tab(index)
1948            .ok_or_else(|| CompositorError::TabNotFound(TabId::from_raw(index)))
1949            .inspect(|_| {
1950                self.changes.window_changed = true;
1951            })
1952    }
1953
1954    fn tab_count(&self) -> usize {
1955        self.tabs.tab_count()
1956    }
1957
1958    fn active_tab_id(&self) -> Option<TabId> {
1959        Some(self.tabs.active_tab_id())
1960    }
1961}
1962
1963/// Convert a char-column index to a byte offset within a `&str`.
1964fn char_col_to_byte(line: &str, col: usize) -> usize {
1965    line.char_indices().nth(col).map_or(line.len(), |(b, _)| b)
1966}
1967
1968#[cfg(test)]
1969#[path = "runtime_tests.rs"]
1970mod tests;