Skip to main content

reovim_server/session/
state.rs

1//! Session state containing application state and registries.
2//!
3//! `SessionState` bundles the runtime application state with the registries
4//! needed for key processing. Each session has its own isolated state.
5//!
6//! # SSOT Architecture
7//!
8//! `driver_session` is the Single Source of Truth (SSOT) for per-session state:
9//! - `mode_stack` - current editing mode
10//! - `pending_keys` - accumulated key sequence
11//! - `extensions` - module-provided policy state
12//! - `active_buffer` - currently active buffer ID
13//! - `terminal_size` - session-level terminal dimensions
14//!
15//! The `AppState` within this struct provides server-specific state (kernel,
16//! windows, cmdline) that doesn't belong in the driver layer.
17
18use std::{collections::HashMap, sync::Arc};
19
20use {
21    parking_lot::RwLock,
22    reovim_driver_command::{CommandContext, CommandResult},
23    reovim_driver_input::{FallbackContext, PendingBindings, ResolverRegistry},
24    reovim_driver_layout::RootCompositor,
25    reovim_driver_session::{ClientId, Session as DriverSession},
26    reovim_driver_vfs::VfsDriver,
27    reovim_kernel::api::v1::{
28        Buffer, BufferId, CommandId, Jumplist, KernelContext, ModeId, ModeStack, RegisterContent,
29    },
30};
31
32use crate::{
33    app::AppState,
34    registry::{CommandRegistry, KeyLookupResult, KeymapRegistry, ModeRegistry},
35};
36
37/// Session state combining application state with registries.
38///
39/// This is the complete state for a single editing session. Each session
40/// (like tmux sessions) has its own `SessionState` with independent:
41/// - Driver-layer session (SSOT for `mode_stack`, `pending_keys`, `extensions`, etc.)
42/// - Kernel context (buffers, events, options)
43/// - Mode/command/keymap registries
44///
45/// # SSOT Architecture
46///
47/// `driver_session` is the Single Source of Truth for per-session state.
48/// `AppState` provides server-specific state that doesn't belong in the driver.
49///
50/// # Thread Safety
51///
52/// `SessionState` is NOT `Sync` by itself. The `Session` wrapper provides
53/// thread-safe access via `RwLock<SessionState>`.
54pub struct SessionState {
55    /// Driver-layer session state (SSOT for buffers and shared resources).
56    ///
57    /// # Multi-Client Warning (#471)
58    ///
59    /// This contains SHARED state used by ALL clients. In multi-client scenarios:
60    ///
61    /// | Field | Status | Use Instead |
62    /// |-------|--------|-------------|
63    /// | `mode_stack` | **DEPRECATED** | `Client::Owner.state.mode_stack` |
64    /// | `pending_keys` | **DEPRECATED** | `Client::Owner.state.pending_keys` |
65    /// | `windows` | Shared (layout) | Per-client cursor in `EditingState.cursor` |
66    /// | `extensions` | Shared | Module state is inherently shared |
67    /// | `active_buffer` | Shared | All clients see same buffers |
68    ///
69    /// **DO NOT** access `driver_session.mode_stack` directly for key resolution.
70    /// Use `Session::resolve_key_for_client()` which routes through per-client state.
71    pub driver_session: DriverSession,
72
73    /// Application state (kernel + server-specific state).
74    ///
75    /// Contains: kernel context, running flag, windows, cmdline.
76    /// NOTE: `mode_stack`, `pending_keys`, `extensions`, `active_buffer`, and
77    /// `terminal_size` in `AppState` are DEPRECATED - use `driver_session` instead.
78    pub app: AppState,
79
80    /// Virtual filesystem driver for file operations.
81    ///
82    /// Commands access files through this VFS abstraction rather than
83    /// using `std::fs` directly.
84    pub vfs: Arc<dyn VfsDriver>,
85
86    /// Registry of mode metadata and behavior.
87    pub mode_registry: ModeRegistry,
88
89    /// Registry of command handlers.
90    pub command_registry: CommandRegistry,
91
92    /// Registry of keybindings.
93    pub keymap_registry: KeymapRegistry,
94
95    /// Registry of mode key resolvers.
96    ///
97    /// Resolvers implement mode-specific key handling policy:
98    /// - Operator interception (keys that enter operator-pending mode)
99    /// - Motion handling (keys that compute cursor ranges)
100    /// - Line-operator detection (repeated operator keys)
101    pub resolver_registry: ResolverRegistry,
102
103    /// Session-scoped shared registers (A-Z) (#515 Phase 5).
104    ///
105    /// All clients in the session read/write from this shared storage.
106    /// Accessed via `Register::Session('A')` through `Register::Session('Z')`.
107    /// Provides cross-client register sharing within a single session.
108    pub session_registers: HashMap<char, RegisterContent>,
109}
110
111impl SessionState {
112    /// Create a new session state.
113    ///
114    /// # Arguments
115    ///
116    /// * `kernel` - The kernel context for this session
117    /// * `initial_mode` - The home mode for new clients joining this session
118    /// * `vfs` - The virtual filesystem driver for file operations
119    #[must_use]
120    pub fn new(kernel: KernelContext, initial_mode: ModeId, vfs: Arc<dyn VfsDriver>) -> Self {
121        // Create driver session with home_mode in SessionShared (#491)
122        // ClientId(0) is a placeholder - real clients get IDs from server layer
123        let driver_session = DriverSession::new(ClientId::new(0), initial_mode);
124
125        Self {
126            driver_session,
127            app: AppState::new(kernel),
128            vfs,
129            mode_registry: ModeRegistry::new(),
130            command_registry: CommandRegistry::new(),
131            keymap_registry: KeymapRegistry::new(),
132            resolver_registry: ResolverRegistry::new(),
133            session_registers: HashMap::new(),
134        }
135    }
136
137    /// Create session state with existing registries.
138    ///
139    /// Used when modules need to populate registries before creating
140    /// the session state.
141    #[must_use]
142    #[allow(clippy::too_many_arguments)]
143    pub fn with_registries(
144        kernel: KernelContext,
145        initial_mode: ModeId,
146        vfs: Arc<dyn VfsDriver>,
147        mode_registry: ModeRegistry,
148        command_registry: CommandRegistry,
149        keymap_registry: KeymapRegistry,
150        resolver_registry: ResolverRegistry,
151        compositor: Option<Box<dyn RootCompositor>>,
152    ) -> Self {
153        // Create driver session with home_mode in SessionShared (#491)
154        let mut driver_session = DriverSession::new(ClientId::new(0), initial_mode);
155
156        // Set compositor if provided by a module
157        if let Some(c) = compositor {
158            driver_session.set_compositor(c);
159        }
160
161        // Create initial window in compositor if kernel has any buffers.
162        // Per-client active_buffer is set in EditingState when clients connect.
163        let buffer_ids = kernel.buffers.list();
164        if !buffer_ids.is_empty()
165            && let Some(compositor) = driver_session.compositor_mut()
166            && let Some(active_layer) = compositor.active_layer()
167            && let Some(layer) = compositor.layer_compositor_mut(active_layer)
168            && layer
169                .windows_in_zone(reovim_driver_layout::Zone::Tiled)
170                .is_empty()
171        {
172            let _window_id = layer.add_tiled();
173            tracing::debug!("Created initial window in compositor");
174        }
175
176        Self {
177            driver_session,
178            app: AppState::new(kernel),
179            vfs,
180            mode_registry,
181            command_registry,
182            keymap_registry,
183            resolver_registry,
184            session_registers: HashMap::new(),
185        }
186    }
187
188    /// Ensure the compositor has at least one tiled window.
189    ///
190    /// Called after scratch buffer creation to handle the case where
191    /// `with_registries()` ran before any buffers existed.
192    pub fn ensure_initial_compositor_window(&mut self) {
193        if self.app.kernel.buffers.list().is_empty() {
194            return;
195        }
196        let Some(compositor) = self.driver_session.compositor_mut() else {
197            return;
198        };
199        let Some(active_layer) = compositor.active_layer() else {
200            return;
201        };
202        let Some(layer) = compositor.layer_compositor_mut(active_layer) else {
203            return;
204        };
205        if layer
206            .windows_in_zone(reovim_driver_layout::Zone::Tiled)
207            .is_empty()
208        {
209            layer.add_tiled();
210        }
211    }
212
213    /// Get a reference to the driver session (SSOT for session state).
214    #[must_use]
215    pub const fn driver_session(&self) -> &DriverSession {
216        &self.driver_session
217    }
218
219    /// Get a mutable reference to the driver session.
220    #[allow(clippy::missing_const_for_fn)]
221    pub fn driver_session_mut(&mut self) -> &mut DriverSession {
222        &mut self.driver_session
223    }
224
225    // ========================================================================
226    // Delegation Methods (SSOT in driver_session.shared)
227    // ========================================================================
228    //
229    // NOTE (#491/#471): mode_stack, extensions, active_buffer, terminal_size
230    // are all per-client state now. Access via Session::client_state().
231    // Only compositor and home_mode remain in SessionShared.
232
233    /// Get the home mode for initializing new clients (#491).
234    ///
235    /// When a new client connects, their mode stack is initialized with
236    /// this mode at the bottom. This is stored in `SessionShared`.
237    #[must_use]
238    pub const fn home_mode(&self) -> &ModeId {
239        self.driver_session.shared.home_mode()
240    }
241
242    // ========================================================================
243    // Registry Accessors
244    // ========================================================================
245
246    // NOTE (#491): current_mode() removed. Per-client mode lives in EditingState.
247    // Use Session::client_current_mode(client_id) or home_mode() instead.
248
249    /// Look up a key sequence in the current mode's keymap.
250    #[must_use]
251    pub fn lookup_keys(
252        &self,
253        mode: &ModeId,
254        keys: &reovim_driver_input::KeySequence,
255    ) -> KeyLookupResult {
256        self.keymap_registry.lookup(mode, keys)
257    }
258
259    /// Execute a command with per-client state (#471, #477).
260    ///
261    /// This enables multi-client isolation by operating on per-client
262    /// mode, cursor, and extension state instead of shared session state.
263    ///
264    /// # Arguments
265    ///
266    /// * `client_mode_stack` - Per-client mode stack (source of truth for mode)
267    /// * `client_windows` - Per-client window layout (source of truth for cursor)
268    /// * `client_extensions` - Per-client module extensions (#477)
269    /// * `id` - The command ID to execute
270    /// * `args` - Command arguments (count, register, etc.)
271    #[must_use]
272    pub fn execute_command_for_client(
273        &mut self,
274        client_id: usize,
275        client: reovim_driver_session::ClientContext<'_>,
276        id: &CommandId,
277        args: &CommandContext,
278    ) -> Option<(
279        CommandResult,
280        reovim_driver_session::api::StateChanges,
281        Vec<reovim_driver_command_types::RuntimeSignal>,
282    )> {
283        // Flush pending edits before command execution
284        self.app.flush_pending_edits();
285        // Use per-client state (#471, #477, #515)
286        // Pass client_id for per-client undo support
287        // Pass shared extensions for session-wide state (#543)
288        let kernel = &self.app.kernel;
289        let shared_ext = &mut self.app.extensions;
290        self.command_registry.execute_for_client(
291            client_id,
292            id,
293            &mut self.driver_session,
294            client,
295            kernel,
296            &self.vfs,
297            args,
298            Some(shared_ext),
299        )
300    }
301
302    /// Check if the home mode accepts character input.
303    ///
304    /// NOTE (#491): This uses `home_mode()` since per-client mode lives in `EditingState`.
305    /// For per-client mode checking, use the per-client state directly.
306    #[must_use]
307    pub fn mode_accepts_char_input(&self) -> bool {
308        self.mode_registry.accepts_char_input(self.home_mode())
309    }
310
311    /// Check if the session should continue running.
312    #[must_use]
313    pub const fn is_running(&self) -> bool {
314        self.app.is_running()
315    }
316
317    /// Request the session to quit.
318    pub fn request_quit(&mut self) {
319        self.app.request_quit();
320    }
321
322    /// Request clients to detach (server continues running).
323    pub fn request_detach(&mut self) {
324        self.app.request_detach();
325    }
326
327    /// Get the resolver registry.
328    #[must_use]
329    pub const fn resolver_registry(&self) -> &ResolverRegistry {
330        &self.resolver_registry
331    }
332
333    // ========================================================================
334    // Buffer Methods (delegated to kernel)
335    // ========================================================================
336
337    /// Get a buffer by ID.
338    #[must_use]
339    pub fn buffer(&self, id: BufferId) -> Option<Arc<RwLock<Buffer>>> {
340        self.app.kernel.buffers.get(id)
341    }
342
343    /// Create a new buffer with the given content.
344    ///
345    /// Returns the buffer ID. Per-client `active_buffer` is managed by
346    /// `EditingState` — callers must set it there if needed.
347    ///
348    /// Uses `Buffer::from_string` so buffers start with `modified = false`.
349    pub fn create_buffer(&mut self, content: &str) -> BufferId {
350        let buffer = Buffer::from_string(content);
351        self.app.kernel.buffers.register(buffer)
352    }
353
354    /// Resolve a key event using the resolver registry.
355    ///
356    /// This is the primary key resolution method that handles:
357    /// - Operator interception (entering operator-pending mode)
358    /// - Mode-specific key handling (via registered resolvers)
359    /// - Extension access for module state
360    ///
361    /// # Returns
362    ///
363    /// - `Some(ResolveResult)` - if a resolver handled the key
364    /// - `None` - if no resolver is registered for the current mode
365    pub fn resolve_key(
366        &mut self,
367        key: &reovim_driver_input::KeyEvent,
368    ) -> Option<(reovim_driver_input::ResolveResult, reovim_driver_session::api::StateChanges)>
369    {
370        use {
371            reovim_driver_input::ModeState,
372            reovim_driver_session::{
373                SessionRuntime,
374                api::{CommandExecutor, CommandHandle},
375            },
376        };
377
378        // Stub command executor - commands are executed separately
379        struct StubExecutor;
380        impl CommandExecutor for StubExecutor {
381            fn get_handle(&self, _id: &CommandId) -> Option<std::sync::Arc<dyn CommandHandle>> {
382                None
383            }
384        }
385
386        // Phase #491: Use home_mode since current_mode() removed.
387        // This method is DEPRECATED - use resolve_key_for_client() with per-client state.
388        let home_mode = self.driver_session.shared.home_mode().clone();
389        let mode = home_mode.clone();
390        let mut mode_state = ModeState::new(mode.clone());
391
392        // Create SessionRuntime for resolver access to session state
393        // #471 Phase 0: Create temporary per-client state for backward compatibility.
394        // This is DEPRECATED - use resolve_key_for_client() with proper per-client state.
395        let stub_executor = StubExecutor;
396        let mut temp_mode_stack = ModeStack::new(home_mode);
397        let mut temp_windows = reovim_driver_session::WindowLayout::empty();
398        let mut runtime_ext = reovim_driver_session::ExtensionMap::new();
399        let mut temp_client_extensions = reovim_driver_session::ExtensionMap::new();
400        let mut temp_compositor = None;
401        let mut temp_tabs = reovim_driver_session::TabPageSet::new();
402        let mut temp_registers = reovim_kernel::api::v1::RegisterBank::new();
403        let mut temp_clipboard_history = reovim_kernel::api::v1::HistoryRing::new();
404        let mut temp_local_marks = reovim_kernel::api::v1::MarkBank::new();
405        let mut temp_jumplist = Jumplist::new();
406        let mut active_buffer = None;
407        let mut terminal_size = (80u16, 24u16);
408
409        let mut runtime = SessionRuntime::new(
410            &mut self.driver_session,
411            reovim_driver_session::ClientContext {
412                mode_stack: &mut temp_mode_stack,
413                windows: &mut temp_windows,
414                extensions: &mut runtime_ext,
415                compositor: &mut temp_compositor,
416                tabs: &mut temp_tabs,
417                registers: &mut temp_registers,
418                clipboard_history: &mut temp_clipboard_history,
419                local_marks: &mut temp_local_marks,
420                jumplist: &mut temp_jumplist,
421                active_buffer: &mut active_buffer,
422                terminal_size: &mut terminal_size,
423            },
424            &self.app.kernel,
425            &stub_executor,
426        );
427
428        // Call resolver
429        let result = self.resolver_registry.resolve_with_session(
430            &mode,
431            key,
432            &mut mode_state,
433            &self.keymap_registry,
434            &mut runtime,
435            &mut self.app.extensions,
436            &mut temp_client_extensions,
437        );
438
439        // Take accumulated changes
440        let changes = reovim_driver_session::api::ChangeTracker::take_changes(&mut runtime);
441
442        result.map(|r| (r, changes))
443    }
444
445    /// Resolve a key event with per-client mode stack (#471).
446    ///
447    /// Like `resolve_key()`, but uses a provided per-client mode stack instead
448    /// of the shared session mode stack. This enables multi-client mode isolation
449    /// where each client has independent mode state.
450    ///
451    /// # Arguments
452    ///
453    /// * `client_id` - The client ID for undo origin tracking (#471 Phase 5)
454    /// * `client_mode_stack` - Per-client mode stack (from server-level `EditingState`)
455    /// * `client_windows` - Per-client window layout
456    /// * `client_extensions` - Per-client extensions
457    /// * `key` - The key event to resolve
458    ///
459    /// # Returns
460    ///
461    /// - `Some((ResolveResult, StateChanges))` - if a resolver handled the key
462    /// - `None` - if no resolver is registered for the current mode
463    ///
464    /// # Example
465    ///
466    /// ```ignore
467    /// // Get per-client EditingState
468    /// let editing_state = session.client_state_mut(client_id)?;
469    ///
470    /// // Resolve key with per-client state
471    /// let client = editing_state.client_context();
472    /// let result = session_state.resolve_key_for_client(client_id, client, &key);
473    /// ```
474    #[allow(clippy::too_many_lines)] // PendingBindings population requires match arms
475    pub fn resolve_key_for_client(
476        &mut self,
477        client_id: usize,
478        client: reovim_driver_session::ClientContext<'_>,
479        key: &reovim_driver_input::KeyEvent,
480    ) -> Option<(reovim_driver_input::ResolveResult, reovim_driver_session::api::StateChanges)>
481    {
482        use {
483            reovim_driver_input::ModeState,
484            reovim_driver_session::{
485                ClientId as DriverClientId, SessionRuntime,
486                api::{CommandExecutor, CommandHandle},
487            },
488        };
489
490        // Stub command executor - commands are executed separately
491        struct StubExecutor;
492        impl CommandExecutor for StubExecutor {
493            fn get_handle(&self, _id: &CommandId) -> Option<std::sync::Arc<dyn CommandHandle>> {
494                None
495            }
496        }
497
498        let reovim_driver_session::ClientContext {
499            mode_stack: client_mode_stack,
500            windows: client_windows,
501            extensions: client_extensions,
502            compositor: client_compositor,
503            tabs: client_tabs,
504            registers: client_registers,
505            clipboard_history: client_clipboard_history,
506            local_marks: client_local_marks,
507            jumplist: client_jumplist,
508            active_buffer: client_active_buffer,
509            terminal_size: client_terminal_size,
510        } = client;
511
512        // Phase #471, #477: Use per-client state for resolution
513        let mode = client_mode_stack.current().clone();
514        let mut mode_state = ModeState::new(mode.clone());
515
516        // Create SessionRuntime with per-client state and owner (#471 Phase 5)
517        // The owner enables undo_mine()/redo_mine() for per-client undo
518        //
519        // Use placeholder extensions in runtime - resolvers access session only
520        // via SessionApiDyn (excludes ExtensionApi), so placeholder is safe.
521        let stub_executor = StubExecutor;
522        let driver_client_id = DriverClientId::new(client_id);
523        let mut runtime_ext = reovim_driver_session::ExtensionMap::new();
524        let mut runtime = SessionRuntime::with_owner(
525            driver_client_id,
526            &mut self.driver_session,
527            reovim_driver_session::ClientContext {
528                mode_stack: client_mode_stack,
529                windows: client_windows,
530                extensions: &mut runtime_ext,
531                compositor: client_compositor,
532                tabs: client_tabs,
533                registers: client_registers,
534                clipboard_history: client_clipboard_history,
535                local_marks: client_local_marks,
536                jumplist: client_jumplist,
537                active_buffer: client_active_buffer,
538                terminal_size: client_terminal_size,
539            },
540            &self.app.kernel,
541            &stub_executor,
542        );
543
544        // Call resolver - mode operations will use client_mode_stack
545        let result = self.resolver_registry.resolve_with_session(
546            &mode,
547            key,
548            &mut mode_state,
549            &self.keymap_registry,
550            &mut runtime,
551            &mut self.app.extensions,
552            client_extensions,
553        );
554
555        // Generic PendingBindings population for bridge consumers (#468).
556        // After resolution, populate PendingBindings so bridges (e.g., WhichKeyBridge)
557        // can produce UI hints without knowing about specific resolvers.
558        match &result {
559            Some(reovim_driver_input::ResolveResult::Pending) => {
560                let pending = self.resolver_registry.pending_keys_for(&mode);
561                if !pending.is_empty() {
562                    let mut continuations =
563                        self.keymap_registry.bindings_with_prefix(&mode, &pending);
564                    // Include parent mode bindings (consistent with Push arm)
565                    if let Some(resolver) = self.resolver_registry.get(&mode)
566                        && let Some(parent) = resolver.inherits_from()
567                    {
568                        let parent_bindings =
569                            self.keymap_registry.bindings_with_prefix(parent, &pending);
570                        continuations.extend(parent_bindings);
571                    }
572                    let pb = client_extensions.get_or_insert::<PendingBindings>();
573                    // Preserve mode_prefix from Push (e.g., trigger key for operator mode)
574                    pb.pending_keys = pending;
575                    pb.mode = mode;
576                    pb.continuations = continuations;
577                }
578            }
579            Some(reovim_driver_input::ResolveResult::ModeTransition(
580                reovim_driver_input::ModeTransition::Push {
581                    mode: target_mode, ..
582                },
583            )) => {
584                // On mode push (e.g., entering an operator-pending mode),
585                // populate PendingBindings with the new mode's available bindings
586                // so which-key can show hints immediately on mode entry.
587                let trigger_key = reovim_driver_input::KeySequence::from_keys(&[*key]);
588                let empty = reovim_driver_input::KeySequence::new();
589                let mut continuations = self
590                    .keymap_registry
591                    .bindings_with_prefix(target_mode, &empty);
592                // Include parent mode bindings (operator modes inherit motions)
593                if let Some(resolver) = self.resolver_registry.get(target_mode)
594                    && let Some(parent) = resolver.inherits_from()
595                {
596                    let parent_bindings = self.keymap_registry.bindings_with_prefix(parent, &empty);
597                    continuations.extend(parent_bindings);
598                }
599                if !continuations.is_empty() {
600                    let pb = client_extensions.get_or_insert::<PendingBindings>();
601                    pb.mode_prefix = trigger_key;
602                    pb.pending_keys = reovim_driver_input::KeySequence::new();
603                    pb.mode = target_mode.clone();
604                    pb.continuations = continuations;
605                }
606            }
607            Some(_) => {
608                // Non-pending, non-push: clear any previous pending bindings
609                if let Some(pb) = client_extensions.get_mut::<PendingBindings>() {
610                    pb.clear();
611                }
612            }
613            None => {}
614        }
615
616        // Take accumulated changes
617        let changes = reovim_driver_session::api::ChangeTracker::take_changes(&mut runtime);
618
619        result.map(|r| (r, changes))
620    }
621
622    /// Try to call `on_command_complete` on the current mode's resolver.
623    ///
624    /// Called after executing a command from `ResolveResult::Execute`.
625    /// For operator-pending modes, this is where the resolver reads
626    /// the post-motion cursor position and builds the final command.
627    ///
628    /// # Flow
629    ///
630    /// 1. Key press → resolver returns `Execute(motion-command)`
631    /// 2. Runner executes the motion → cursor moves
632    /// 3. **This method** → resolver reads end position, returns
633    ///    `ModeTransition::Pop { ExecuteCommand { operator, range } }`
634    /// 4. Runner pops the operator mode and executes the operator command
635    pub fn try_on_command_complete(&mut self) -> Option<reovim_driver_input::ModeTransition> {
636        use reovim_driver_session::{
637            SessionRuntime,
638            api::{CommandExecutor, CommandHandle},
639        };
640
641        struct StubExecutor;
642        impl CommandExecutor for StubExecutor {
643            fn get_handle(&self, _id: &CommandId) -> Option<std::sync::Arc<dyn CommandHandle>> {
644                None
645            }
646        }
647
648        // Phase #491: Use home_mode since current_mode() removed.
649        // This method is DEPRECATED - use try_on_command_complete_for_client() with per-client state.
650        let home_mode = self.driver_session.shared.home_mode().clone();
651        let mode = home_mode.clone();
652        let resolver = self.resolver_registry.get(&mode)?;
653
654        // #471 Phase 0: Create temporary per-client state for backward compatibility.
655        let stub_executor = StubExecutor;
656        let mut temp_mode_stack = ModeStack::new(home_mode);
657        let mut temp_windows = reovim_driver_session::WindowLayout::empty();
658        let mut runtime_ext = reovim_driver_session::ExtensionMap::new();
659        let mut temp_client_extensions = reovim_driver_session::ExtensionMap::new();
660        let mut temp_compositor = None;
661        let mut temp_tabs = reovim_driver_session::TabPageSet::new();
662        let mut temp_registers = reovim_kernel::api::v1::RegisterBank::new();
663        let mut temp_clipboard_history = reovim_kernel::api::v1::HistoryRing::new();
664        let mut temp_local_marks = reovim_kernel::api::v1::MarkBank::new();
665        let mut temp_jumplist = Jumplist::new();
666        let mut active_buffer = None;
667        let mut terminal_size = (80u16, 24u16);
668
669        let mut runtime = SessionRuntime::new(
670            &mut self.driver_session,
671            reovim_driver_session::ClientContext {
672                mode_stack: &mut temp_mode_stack,
673                windows: &mut temp_windows,
674                extensions: &mut runtime_ext,
675                compositor: &mut temp_compositor,
676                tabs: &mut temp_tabs,
677                registers: &mut temp_registers,
678                clipboard_history: &mut temp_clipboard_history,
679                local_marks: &mut temp_local_marks,
680                jumplist: &mut temp_jumplist,
681                active_buffer: &mut active_buffer,
682                terminal_size: &mut terminal_size,
683            },
684            &self.app.kernel,
685            &stub_executor,
686        );
687
688        resolver.on_command_complete(
689            &mut runtime,
690            &mut self.app.extensions,
691            &mut temp_client_extensions,
692        )
693    }
694
695    /// Try to call `on_command_complete` with per-client state (#471, #477).
696    ///
697    /// Like `try_on_command_complete()`, but uses per-client mode stack, windows,
698    /// and extensions instead of the shared session state.
699    ///
700    /// # Arguments
701    ///
702    /// * `client` - Per-client state bundle (from server-level `EditingState`)
703    ///
704    /// # Returns
705    ///
706    /// A `ModeTransition` if the resolver wants to change modes.
707    pub fn try_on_command_complete_for_client(
708        &mut self,
709        client_id: usize,
710        client: reovim_driver_session::ClientContext<'_>,
711    ) -> Option<reovim_driver_input::ModeTransition> {
712        use reovim_driver_session::{
713            ClientId as DriverClientId, SessionRuntime,
714            api::{CommandExecutor, CommandHandle},
715        };
716
717        struct StubExecutor;
718        impl CommandExecutor for StubExecutor {
719            fn get_handle(&self, _id: &CommandId) -> Option<std::sync::Arc<dyn CommandHandle>> {
720                None
721            }
722        }
723
724        let reovim_driver_session::ClientContext {
725            mode_stack: client_mode_stack,
726            windows: client_windows,
727            extensions: client_extensions,
728            compositor: client_compositor,
729            tabs: client_tabs,
730            registers: client_registers,
731            clipboard_history: client_clipboard_history,
732            local_marks: client_local_marks,
733            jumplist: client_jumplist,
734            active_buffer: client_active_buffer,
735            terminal_size: client_terminal_size,
736        } = client;
737
738        // Phase #471, #477, #515: Use per-client state with owner for per-client undo
739        //
740        // Use placeholder extensions in runtime - resolvers access session only
741        // via SessionApiDyn (excludes ExtensionApi), so placeholder is safe.
742        let mode = client_mode_stack.current().clone();
743        let resolver = self.resolver_registry.get(&mode)?;
744        let stub_executor = StubExecutor;
745        let driver_client_id = DriverClientId::new(client_id);
746        let mut runtime_ext = reovim_driver_session::ExtensionMap::new();
747        let mut runtime = SessionRuntime::with_owner(
748            driver_client_id,
749            &mut self.driver_session,
750            reovim_driver_session::ClientContext {
751                mode_stack: client_mode_stack,
752                windows: client_windows,
753                extensions: &mut runtime_ext,
754                compositor: client_compositor,
755                tabs: client_tabs,
756                registers: client_registers,
757                clipboard_history: client_clipboard_history,
758                local_marks: client_local_marks,
759                jumplist: client_jumplist,
760                active_buffer: client_active_buffer,
761                terminal_size: client_terminal_size,
762            },
763            &self.app.kernel,
764            &stub_executor,
765        );
766
767        resolver.on_command_complete(&mut runtime, &mut self.app.extensions, client_extensions)
768    }
769}
770
771impl Default for SessionState {
772    fn default() -> Self {
773        // Create with default mode (will be overwritten by modules)
774        let mode = ModeId::new(reovim_kernel::api::v1::ModuleId::new("default"), "normal");
775        let vfs: Arc<dyn VfsDriver> = Arc::new(reovim_driver_vfs::MockVfs::new());
776        Self::new(KernelContext::default(), mode, vfs)
777    }
778}
779
780impl SessionState {
781    /// Create a session state with a custom kernel context.
782    ///
783    /// Useful for testing with non-default buffer managers.
784    #[must_use]
785    pub fn with_kernel(kernel: KernelContext) -> Self {
786        let mode = ModeId::new(reovim_kernel::api::v1::ModuleId::new("default"), "normal");
787        let vfs: Arc<dyn VfsDriver> = Arc::new(reovim_driver_vfs::MockVfs::new());
788        Self::new(kernel, mode, vfs)
789    }
790}
791
792#[cfg(test)]
793#[path = "state_tests.rs"]
794mod tests;