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;