Skip to main content

reovim_driver_session/
types.rs

1//! Session types for driver-layer state.
2//!
3//! This module provides types for managing session state at the driver layer.
4//!
5//! # Design (#471)
6//!
7//! - **`SessionShared`**: Shared session infrastructure (compositor, `terminal_size`)
8//! - **`Session`**: DEPRECATED - legacy type being migrated to `SessionShared`
9//! - **`ClientId`**: Unique client connection identifier
10//! - **Viewport**: Client viewport dimensions and scroll
11//! - **Window**: Single window with buffer reference
12//! - **`WindowLayout`**: Window arrangement for a session
13//! - **`TextObjRange`**: Range computed by text object commands
14//!
15//! # Architecture (#471)
16//!
17//! Per-client state (mode, cursor, selection) lives in `server::EditingState`.
18//! The driver layer provides ONLY shared infrastructure via `SessionShared`.
19//! `DriverRuntime` (formerly `SessionRuntime`) borrows both to operate on
20//! the correct client's state.
21
22use {
23    reovim_driver_layout::RootCompositor,
24    reovim_kernel::api::v1::{
25        BufferId, HistoryRing, Jumplist, MarkBank, ModeId, ModeStack, Position, RegisterBank,
26        WindowId,
27    },
28};
29
30use crate::{api::Selection as ApiSelection, extension::ExtensionMap};
31
32// ============================================================================
33// SessionShared - Shared Session Infrastructure (#471)
34// ============================================================================
35
36/// Shared session infrastructure.
37///
38/// Contains state that is shared across all clients in a session:
39/// - Compositor for window layout management
40/// - Default terminal size
41/// - Active buffer ID (session-level)
42/// - Home mode for initializing new clients
43///
44/// # Architecture (#471, #491)
45///
46/// Per-client state lives in `server::EditingState`, NOT here.
47/// This type provides only the truly shared infrastructure.
48///
49/// ```text
50/// ┌─────────────────────────────────────────────────────────────────┐
51/// │ SERVER LAYER                                                    │
52/// │   Session                                                       │
53/// │   └── clients: HashMap<ClientId, Client>                        │
54/// │       └── Client                                                │
55/// │           └── state: EditingState  ◄─── OWNS per-client state   │
56/// │               ├── mode_stack                                    │
57/// │               ├── windows (with cursors!)                       │
58/// │               └── extensions                                    │
59/// └─────────────────────────────────────────────────────────────────┘
60/// ┌─────────────────────────────────────────────────────────────────┐
61/// │ DRIVER LAYER                                                    │
62/// │   SessionShared  ◄─── Truly shared infrastructure               │
63/// │   ├── compositor  (template for layout)                         │
64/// │   └── home_mode   (bootstrap template for new clients, #491)   │
65/// └─────────────────────────────────────────────────────────────────┘
66/// ```
67///
68/// # Migration from `Session`
69///
70/// `Session` is being replaced by `SessionShared`. The following fields:
71/// - `mode_stack`, `pending_keys`, `extensions` → now in `EditingState`
72/// - `windows` → now in `EditingState` (per-client cursors)
73/// - `id: ClientId` → now in `server::Client`
74pub struct SessionShared {
75    /// Window compositor for layout management.
76    ///
77    /// The compositor manages window geometry, splits, and navigation.
78    /// This is set by the layout module during session initialization.
79    pub compositor: Option<Box<dyn RootCompositor>>,
80
81    /// Home mode for initializing new clients (#491).
82    ///
83    /// Bootstrap template for initializing new clients -- each client's
84    /// mode evolves independently after init.
85    home_mode: ModeId,
86}
87
88#[cfg_attr(coverage_nightly, coverage(off))]
89impl std::fmt::Debug for SessionShared {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("SessionShared")
92            .field("compositor", &self.compositor.as_ref().map(|_| "..."))
93            .field("home_mode", &self.home_mode)
94            .finish()
95    }
96}
97
98// NOTE: Default impl removed in #491 - SessionShared now requires home_mode parameter
99
100impl SessionShared {
101    /// Create a new shared session infrastructure with the specified home mode.
102    ///
103    /// # Parameters
104    ///
105    /// - `home_mode`: The mode used to initialize new clients' mode stacks
106    ///
107    /// Compositor is initialized as `None` and should be set by the layout module.
108    #[must_use]
109    pub fn new(home_mode: ModeId) -> Self {
110        Self {
111            compositor: None,
112            home_mode,
113        }
114    }
115
116    /// Set the compositor for this session.
117    ///
118    /// Called by the layout module during session initialization.
119    pub fn set_compositor(&mut self, compositor: Box<dyn RootCompositor>) {
120        self.compositor = Some(compositor);
121    }
122
123    /// Get a reference to the compositor.
124    #[must_use]
125    pub fn compositor(&self) -> Option<&dyn RootCompositor> {
126        self.compositor.as_deref()
127    }
128
129    /// Get a mutable reference to the compositor.
130    pub fn compositor_mut(&mut self) -> Option<&mut (dyn RootCompositor + 'static)> {
131        self.compositor.as_deref_mut()
132    }
133
134    /// Get the home mode for initializing new clients (#491).
135    ///
136    /// When a new client connects, their mode stack is initialized
137    /// with this mode at the bottom.
138    #[must_use]
139    pub const fn home_mode(&self) -> &ModeId {
140        &self.home_mode
141    }
142}
143
144// ============================================================================
145// BootstrapState - Initial Per-Client State for Session Initialization (#488)
146// ============================================================================
147
148/// Bootstrap state for creating a new session with initial per-client data.
149///
150/// This struct provides the initial per-client state needed when initializing
151/// a session or adding the first client. It contains fields that will become
152/// part of `EditingState` in the server layer.
153///
154/// # Usage
155///
156/// ```rust,ignore
157/// // Create session with bootstrap state for tests
158/// let (session, bootstrap) = Session::bootstrap(ClientId::new(1), home_mode);
159///
160/// // Use bootstrap fields for per-client state
161/// let mode_stack = bootstrap.mode_stack;
162/// let windows = bootstrap.windows;
163/// ```
164///
165/// # Architecture (#488)
166///
167/// This type exists to provide a clean separation between:
168/// - **Session**: Shared infrastructure only (`SessionShared`)
169/// - **`BootstrapState`**: Initial per-client state (mode, windows, extensions)
170///
171/// At runtime, per-client state lives in `EditingState` in the server layer.
172#[derive(Debug)]
173pub struct BootstrapState {
174    /// Initial mode stack with home mode at the bottom.
175    pub mode_stack: ModeStack,
176    /// Initial window layout (empty by default).
177    pub windows: WindowLayout,
178    /// Initial pending keys (empty by default).
179    pub pending_keys: KeySequence,
180    /// Initial extensions map (empty by default).
181    pub extensions: ExtensionMap,
182}
183
184impl BootstrapState {
185    /// Create bootstrap state with the given home mode.
186    ///
187    /// The home mode becomes the bottom of the mode stack.
188    /// Windows, pending keys, and extensions start empty.
189    #[must_use]
190    pub fn new(home_mode: ModeId) -> Self {
191        Self {
192            mode_stack: ModeStack::new(home_mode),
193            windows: WindowLayout::empty(),
194            pending_keys: KeySequence::new(),
195            extensions: ExtensionMap::new(),
196        }
197    }
198
199    /// Create bootstrap state with a window for the given buffer.
200    ///
201    /// This is useful when the session already has an active buffer
202    /// and the new client should have a window showing that buffer.
203    #[must_use]
204    pub fn with_buffer(home_mode: ModeId, buffer_id: BufferId) -> Self {
205        let mut state = Self::new(home_mode);
206        state.windows.add(Window::with_buffer(buffer_id));
207        state
208    }
209}
210
211// ============================================================================
212// ClientContext - Borrowed Per-Client State Bundle (#515)
213// ============================================================================
214
215/// Borrowed per-client state bundle for session operations.
216///
217/// Groups the 7 mutable references to per-client state that [`SessionRuntime`]
218/// and command execution require. This replaces passing 7 individual `&mut`
219/// parameters through function signatures.
220///
221/// # Ownership
222///
223/// `ClientContext` does NOT own the data -- it borrows from `server::EditingState`
224/// (or from test fixtures). The owned counterpart is `server::EditingState`.
225///
226/// # Convention
227///
228/// Follows the `*Context<'a>` naming convention used throughout the codebase
229/// (`OperatorContext`, `HandlerContext`, `CommandContext`, etc.).
230///
231/// [`SessionRuntime`]: crate::SessionRuntime
232pub struct ClientContext<'a> {
233    /// Per-client mode stack (current mode on top).
234    pub mode_stack: &'a mut ModeStack,
235    /// Per-client window layout with independent cursors.
236    pub windows: &'a mut WindowLayout,
237    /// Per-client module extensions (type-erased state).
238    pub extensions: &'a mut ExtensionMap,
239    /// Per-client compositor for window layout geometry.
240    pub compositor: &'a mut Option<Box<dyn RootCompositor>>,
241    /// Per-client tab pages (#401).
242    pub tabs: &'a mut crate::TabPageSet,
243    /// Per-client register storage (unnamed, named a-z/A-Z).
244    pub registers: &'a mut RegisterBank,
245    /// Per-client clipboard history ring (numbered registers 0-9).
246    pub clipboard_history: &'a mut HistoryRing,
247    /// Per-client local marks (a-z).
248    pub local_marks: &'a mut MarkBank,
249    /// Per-client jump list for Ctrl-O / Ctrl-I navigation (#654).
250    pub jumplist: &'a mut Jumplist,
251    /// Per-client active buffer (#471).
252    ///
253    /// Each client tracks which buffer they are viewing independently.
254    /// Previously session-level in `SessionShared`, migrated to per-client
255    /// so that multi-client sessions don't share active buffer state.
256    pub active_buffer: &'a mut Option<BufferId>,
257    /// Per-client terminal dimensions (width, height) (#471).
258    ///
259    /// Each client has independent terminal size for compositor calculations.
260    /// Previously hardcoded (80, 24) in `SessionShared`, migrated to per-client
261    /// so that clients with different screen sizes get correct layouts.
262    pub terminal_size: &'a mut (u16, u16),
263}
264
265// ============================================================================
266// TextObjRange
267// ============================================================================
268
269/// Range computed by a text object command.
270///
271/// Text objects like `iw`, `aw`, `i"` compute a range directly instead of
272/// moving the cursor. When executed during operator-pending mode, they store
273/// the range for the operator resolver to consume.
274///
275/// # Coordinate System
276///
277/// - `start`: First position in the range (inclusive)
278/// - `end`: First position AFTER the range (exclusive)
279///
280/// This matches Rust's standard `Range<Position>` semantics.
281///
282/// # Example Flow
283///
284/// 1. User presses `d` → enters operator-pending mode
285/// 2. User presses `iw` → `InnerWord` command executes
286/// 3. Command calculates word boundaries and creates `TextObjRange`
287/// 4. Command stores range in session extension state
288/// 5. Operator resolver's `on_command_complete` consumes the range
289/// 6. Delete operation uses the range
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub struct TextObjRange {
292    /// Start position (inclusive).
293    pub start: Position,
294    /// End position (exclusive - points to first char NOT in range).
295    pub end: Position,
296    /// Whether this is a linewise text object (e.g., `ip`, `ap`).
297    ///
298    /// Linewise text objects affect entire lines, similar to linewise
299    /// motions like `j`, `k`, `gg`, `G`.
300    pub is_linewise: bool,
301}
302
303impl TextObjRange {
304    /// Create a new characterwise text object range.
305    ///
306    /// Characterwise ranges operate on a specific range of characters,
307    /// like `iw` (inner word) or `i"` (inside quotes).
308    #[must_use]
309    pub const fn characterwise(start: Position, end: Position) -> Self {
310        Self {
311            start,
312            end,
313            is_linewise: false,
314        }
315    }
316
317    /// Create a new linewise text object range.
318    ///
319    /// Linewise ranges affect entire lines, like `ip` (inner paragraph)
320    /// or `ap` (a paragraph).
321    #[must_use]
322    pub const fn linewise(start: Position, end: Position) -> Self {
323        Self {
324            start,
325            end,
326            is_linewise: true,
327        }
328    }
329
330    /// Check if this range is empty.
331    #[must_use]
332    pub const fn is_empty(&self) -> bool {
333        self.start.line == self.end.line && self.start.column == self.end.column
334    }
335}
336
337/// Unique client connection identifier.
338///
339/// Each terminal/TUI that connects to the server gets a unique `ClientId`.
340/// IDs are monotonically increasing and not reused after disconnect.
341///
342/// # Semantics
343///
344/// - **Client**: Individual connection to the server (like tmux clients)
345/// - **Session**: Named editing context (defined in runner layer)
346/// - Multiple clients can attach to the same session
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
348pub struct ClientId(pub usize);
349
350impl ClientId {
351    /// Create a new client ID.
352    #[must_use]
353    pub const fn new(id: usize) -> Self {
354        Self(id)
355    }
356
357    /// Get the raw ID value.
358    #[must_use]
359    pub const fn as_usize(&self) -> usize {
360        self.0
361    }
362}
363
364impl std::fmt::Display for ClientId {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        write!(f, "client-{}", self.0)
367    }
368}
369
370/// Viewport for a client session.
371///
372/// Represents the visible area of a buffer in a window.
373/// Tracks both vertical and horizontal scroll positions.
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375pub struct Viewport {
376    /// Width in columns.
377    pub width: u16,
378    /// Height in rows.
379    pub height: u16,
380    /// Vertical scroll offset (first visible line, 0-indexed).
381    pub scroll_top: usize,
382    /// Horizontal scroll offset (first visible column, 0-indexed).
383    pub scroll_left: usize,
384}
385
386impl Viewport {
387    /// Create a new viewport.
388    #[must_use]
389    pub const fn new(width: u16, height: u16) -> Self {
390        Self {
391            width,
392            height,
393            scroll_top: 0,
394            scroll_left: 0,
395        }
396    }
397
398    /// Create a default viewport (80x24).
399    #[must_use]
400    pub const fn default_size() -> Self {
401        Self::new(80, 24)
402    }
403
404    /// Get the last visible line index.
405    #[must_use]
406    pub const fn last_visible_line(&self) -> usize {
407        self.scroll_top + self.height as usize - 1
408    }
409
410    /// Get the last visible column index.
411    #[must_use]
412    pub const fn last_visible_column(&self) -> usize {
413        self.scroll_left + self.width as usize - 1
414    }
415
416    /// Check if a line is visible.
417    #[must_use]
418    pub const fn is_line_visible(&self, line: usize) -> bool {
419        line >= self.scroll_top && line <= self.last_visible_line()
420    }
421
422    /// Check if a column is visible.
423    #[must_use]
424    pub const fn is_column_visible(&self, column: usize) -> bool {
425        column >= self.scroll_left && column <= self.last_visible_column()
426    }
427
428    /// Check if a position (line, column) is visible.
429    #[must_use]
430    pub const fn is_position_visible(&self, line: usize, column: usize) -> bool {
431        self.is_line_visible(line) && self.is_column_visible(column)
432    }
433
434    /// Adjust `scroll_top` so the cursor line is visible.
435    ///
436    /// Returns `true` if `scroll_top` was changed.
437    pub const fn ensure_cursor_visible(&mut self, cursor_line: usize) -> bool {
438        if self.height == 0 {
439            return false;
440        }
441        let old = self.scroll_top;
442        let h = self.height as usize;
443        if cursor_line < self.scroll_top {
444            self.scroll_top = cursor_line;
445        } else if cursor_line >= self.scroll_top + h {
446            self.scroll_top = cursor_line - h + 1;
447        }
448        self.scroll_top != old
449    }
450}
451
452impl Default for Viewport {
453    fn default() -> Self {
454        Self::default_size()
455    }
456}
457
458/// Cursor position within a window.
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
460pub struct CursorPosition {
461    /// Line number (0-indexed).
462    pub line: usize,
463    /// Column number (0-indexed).
464    pub column: usize,
465}
466
467impl CursorPosition {
468    /// Create a new cursor position.
469    #[must_use]
470    pub const fn new(line: usize, column: usize) -> Self {
471        Self { line, column }
472    }
473
474    /// Create cursor at origin (0, 0).
475    #[must_use]
476    pub const fn origin() -> Self {
477        Self::new(0, 0)
478    }
479}
480
481impl From<Position> for CursorPosition {
482    fn from(pos: Position) -> Self {
483        Self::new(pos.line, pos.column)
484    }
485}
486
487impl From<CursorPosition> for Position {
488    fn from(cursor: CursorPosition) -> Self {
489        Self::new(cursor.line, cursor.column)
490    }
491}
492
493/// Window within a session.
494///
495/// A window displays a portion of a buffer. Sessions can have multiple
496/// windows (splits), each with its own viewport and cursor.
497///
498/// # Per-Window Selection (Phase 8 #465)
499///
500/// Selection is stored per-window, not per-buffer. This enables:
501/// - Multi-window same-buffer independence (different selections in each window)
502/// - Multi-client selection isolation (each client's visual mode is independent)
503///
504/// Uses API's `Selection` type with explicit start/end, NOT kernel's Selection.
505#[derive(Debug, Clone)]
506pub struct Window {
507    /// Unique window identifier.
508    pub id: WindowId,
509    /// Buffer displayed in this window (if any).
510    pub buffer_id: Option<BufferId>,
511    /// Cursor position within the buffer.
512    pub cursor: CursorPosition,
513    /// Visible viewport.
514    pub viewport: Viewport,
515    /// Selection state for visual mode (Phase 8 #465).
516    ///
517    /// Uses API's Selection type with explicit start/end positions.
518    /// `None` means no active selection.
519    pub selection: Option<ApiSelection>,
520}
521
522impl Window {
523    /// Create a new empty window.
524    #[must_use]
525    pub fn new() -> Self {
526        Self {
527            id: WindowId::new(),
528            buffer_id: None,
529            cursor: CursorPosition::origin(),
530            viewport: Viewport::default(),
531            selection: None,
532        }
533    }
534
535    /// Create a window displaying a buffer.
536    #[must_use]
537    pub fn with_buffer(buffer_id: BufferId) -> Self {
538        Self {
539            id: WindowId::new(),
540            buffer_id: Some(buffer_id),
541            cursor: CursorPosition::origin(),
542            viewport: Viewport::default(),
543            selection: None,
544        }
545    }
546
547    /// Create a window with a specific ID and buffer (#474).
548    ///
549    /// Used when creating per-client windows that must match compositor window IDs.
550    /// Unlike `with_buffer()`, this does NOT generate a new `WindowId`.
551    #[must_use]
552    pub fn with_id_and_buffer(id: WindowId, buffer_id: BufferId) -> Self {
553        Self {
554            id,
555            buffer_id: Some(buffer_id),
556            cursor: CursorPosition::origin(),
557            viewport: Viewport::default(),
558            selection: None,
559        }
560    }
561
562    /// Create a window that inherits state from a source window (#692).
563    ///
564    /// Used when splitting: the new pane gets the same buffer, cursor position,
565    /// and viewport scroll offset as the source. Selection is NOT copied —
566    /// splitting does not clone visual mode state.
567    #[must_use]
568    pub const fn split_from(id: WindowId, source: &Self) -> Self {
569        Self {
570            id,
571            buffer_id: source.buffer_id,
572            cursor: source.cursor,
573            viewport: source.viewport,
574            selection: None,
575        }
576    }
577}
578
579impl Default for Window {
580    fn default() -> Self {
581        Self::new()
582    }
583}
584
585/// Window layout for a session.
586///
587/// Manages the windows in a session, including which window is active.
588#[derive(Debug, Default, Clone)]
589pub struct WindowLayout {
590    /// All windows in this session.
591    pub windows: Vec<Window>,
592    /// Currently active window index.
593    active_index: Option<usize>,
594}
595
596impl WindowLayout {
597    /// Create an empty layout.
598    #[must_use]
599    pub const fn empty() -> Self {
600        Self {
601            windows: Vec::new(),
602            active_index: None,
603        }
604    }
605
606    /// Create a layout with one window.
607    #[must_use]
608    pub fn single(window: Window) -> Self {
609        Self {
610            windows: vec![window],
611            active_index: Some(0),
612        }
613    }
614
615    /// Add a window to the layout.
616    ///
617    /// If this is the first window, it becomes active.
618    pub fn add(&mut self, window: Window) {
619        self.windows.push(window);
620        if self.active_index.is_none() {
621            self.active_index = Some(0);
622        }
623    }
624
625    /// Get the active window.
626    ///
627    /// If no window is explicitly active but windows exist, returns the first window.
628    /// This ensures a valid window is always available when the layout is non-empty.
629    #[must_use]
630    pub fn active(&self) -> Option<&Window> {
631        // Try explicit active index first, fallback to first window
632        if let Some(idx) = self.active_index
633            && let Some(window) = self.windows.get(idx)
634        {
635            return Some(window);
636        }
637        // Fallback: return first window if layout is non-empty
638        self.windows.first()
639    }
640
641    /// Get the active window mutably.
642    ///
643    /// If no window is explicitly active but windows exist, returns the first window.
644    /// This ensures a valid window is always available when the layout is non-empty.
645    #[cfg_attr(coverage_nightly, coverage(off))]
646    pub fn active_mut(&mut self) -> Option<&mut Window> {
647        // Try explicit active index first, fallback to first window
648        if let Some(idx) = self.active_index
649            && idx < self.windows.len()
650        {
651            return self.windows.get_mut(idx);
652        }
653        // Fallback: return first window if layout is non-empty
654        self.windows.first_mut()
655    }
656
657    /// Get the active window ID.
658    #[must_use]
659    pub fn active_id(&self) -> Option<WindowId> {
660        self.active().map(|w| w.id)
661    }
662
663    /// Set the active window by ID.
664    ///
665    /// Returns `true` if the window was found and made active.
666    pub fn set_active(&mut self, id: WindowId) -> bool {
667        if let Some(idx) = self.windows.iter().position(|w| w.id == id) {
668            self.active_index = Some(idx);
669            true
670        } else {
671            false
672        }
673    }
674
675    /// Check if the layout is empty.
676    #[must_use]
677    pub const fn is_empty(&self) -> bool {
678        self.windows.is_empty()
679    }
680
681    /// Get the number of windows.
682    #[must_use]
683    pub const fn len(&self) -> usize {
684        self.windows.len()
685    }
686
687    /// Clear all windows from the layout.
688    pub fn clear(&mut self) {
689        self.windows.clear();
690        self.active_index = None;
691    }
692
693    /// Get window by ID.
694    #[must_use]
695    pub fn get(&self, id: WindowId) -> Option<&Window> {
696        self.windows.iter().find(|w| w.id == id)
697    }
698
699    /// Get window by ID mutably.
700    pub fn get_mut(&mut self, id: WindowId) -> Option<&mut Window> {
701        self.windows.iter_mut().find(|w| w.id == id)
702    }
703
704    /// Remove a window by ID (#474).
705    ///
706    /// Returns `true` if the window was found and removed. If the removed
707    /// window was active, the active index is adjusted.
708    pub fn remove(&mut self, id: WindowId) -> bool {
709        if let Some(idx) = self.windows.iter().position(|w| w.id == id) {
710            self.windows.remove(idx);
711            // Adjust active index after removal
712            match self.active_index {
713                Some(active) if active == idx => {
714                    // Active window was removed - reset to first window (if any)
715                    self.active_index = if self.windows.is_empty() {
716                        None
717                    } else {
718                        Some(0)
719                    };
720                }
721                Some(active) if active > idx => {
722                    // Active was after removed - shift down
723                    self.active_index = Some(active - 1);
724                }
725                _ => {}
726            }
727            true
728        } else {
729            false
730        }
731    }
732}
733
734/// Pending key sequence.
735///
736/// Accumulates keys that haven't been resolved yet (e.g., `d` waiting for motion).
737#[derive(Debug, Clone, Default)]
738pub struct KeySequence {
739    /// Accumulated key representations.
740    keys: Vec<String>,
741}
742
743impl KeySequence {
744    /// Create an empty sequence.
745    #[must_use]
746    pub const fn new() -> Self {
747        Self { keys: Vec::new() }
748    }
749
750    /// Add a key to the sequence.
751    pub fn push(&mut self, key: String) {
752        self.keys.push(key);
753    }
754
755    /// Clear the sequence.
756    pub fn clear(&mut self) {
757        self.keys.clear();
758    }
759
760    /// Check if empty.
761    #[must_use]
762    pub const fn is_empty(&self) -> bool {
763        self.keys.is_empty()
764    }
765
766    /// Get the keys.
767    #[must_use]
768    pub fn keys(&self) -> &[String] {
769        &self.keys
770    }
771
772    /// Get the sequence as a single string.
773    #[must_use]
774    pub fn as_string(&self) -> String {
775        self.keys.join("")
776    }
777}
778
779/// Driver-layer session containing shared infrastructure.
780///
781/// # Architecture (#471, #491)
782///
783/// After the per-client state migration, `Session` contains only:
784/// - `id`: Client identifier (placeholder for session-level operations)
785/// - `shared`: Truly shared infrastructure (`SessionShared`)
786///
787/// Per-client state (mode, cursor, selection) now lives in `EditingState`
788/// at the server layer. Commands use `SessionRuntime::new()` which borrows
789/// both shared infrastructure and per-client state.
790///
791/// ```text
792/// ┌─────────────────────────────────────────────────────────────────┐
793/// │ SERVER LAYER                                                    │
794/// │   Session (server/lib/server/)                                  │
795/// │   └── clients: HashMap<ClientId, Client>                        │
796/// │       └── Client                                                │
797/// │           └── state: EditingState  ◄─── OWNS per-client state   │
798/// │               ├── mode_stack                                    │
799/// │               ├── windows (with cursors!)                       │
800/// │               ├── pending_keys                                  │
801/// │               └── extensions                                    │
802/// └─────────────────────────────────────────────────────────────────┘
803/// ┌─────────────────────────────────────────────────────────────────┐
804/// │ DRIVER LAYER                                                    │
805/// │   Session (this struct)                                         │
806/// │   ├── id: ClientId (deprecated, not used at runtime #471)       │
807/// │   └── shared: SessionShared  ◄─── Truly shared infrastructure   │
808/// │       ├── compositor  (template for layout)                     │
809/// │       └── home_mode   (bootstrap template for new clients)      │
810/// └─────────────────────────────────────────────────────────────────┘
811/// ```
812pub struct Session {
813    /// Client identifier (deprecated -- not used at runtime #471).
814    pub id: ClientId,
815    /// Shared session infrastructure (compositor, home mode).
816    ///
817    /// This field contains truly shared state that is accessed by all clients.
818    /// `SessionRuntime` accesses shared state via `session.shared`.
819    pub shared: SessionShared,
820}
821
822#[cfg_attr(coverage_nightly, coverage(off))]
823impl std::fmt::Debug for Session {
824    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
825        f.debug_struct("Session")
826            .field("id", &self.id)
827            .field("shared", &self.shared)
828            .finish()
829    }
830}
831
832impl Session {
833    /// Create a new session with shared infrastructure.
834    ///
835    /// # Parameters
836    ///
837    /// - `id`: Unique client identifier
838    /// - `home_mode`: The mode used to initialize new clients' mode stacks
839    ///
840    /// # Architecture (#488, #491)
841    ///
842    /// This creates a session with shared state (`SessionShared`) containing
843    /// the `home_mode` for initializing new clients.
844    ///
845    /// The deprecated fields (`mode_stack`, `windows`, `extensions`) are
846    /// initialized with placeholder values and should NOT be used at runtime.
847    ///
848    /// For per-client state, use [`BootstrapState`] or create `EditingState` directly.
849    ///
850    /// # Example
851    ///
852    /// ```rust,ignore
853    /// // Create session with home mode
854    /// let session = Session::new(ClientId::new(1), home_mode.clone());
855    ///
856    /// // Get home_mode for initializing new clients
857    /// let mode_for_new_client = session.shared.home_mode().clone();
858    ///
859    /// // Create per-client state separately
860    /// let mode_stack = ModeStack::new(mode_for_new_client);
861    /// let windows = WindowLayout::empty();
862    /// let extensions = ExtensionMap::new();
863    /// ```
864    #[must_use]
865    pub fn new(id: ClientId, home_mode: ModeId) -> Self {
866        Self {
867            id,
868            shared: SessionShared::new(home_mode),
869        }
870    }
871
872    /// Create session with bootstrap state for a single client.
873    ///
874    /// This is a convenience method for tests and initialization that need
875    /// both shared session state and initial per-client state.
876    ///
877    /// # Returns
878    ///
879    /// Tuple of `(Session, BootstrapState)` where:
880    /// - `Session` contains shared infrastructure (`SessionShared`) with `home_mode`
881    /// - `BootstrapState` contains initial per-client data that should be
882    ///   stored in `EditingState` when adding a client
883    ///
884    /// # Example
885    ///
886    /// ```rust,ignore
887    /// let (mut session, bootstrap) = Session::bootstrap(ClientId::new(1), home_mode);
888    ///
889    /// // Use bootstrap for per-client state
890    /// let mut mode_stack = bootstrap.mode_stack;
891    /// let mut windows = bootstrap.windows;
892    /// let mut extensions = bootstrap.extensions;
893    ///
894    /// // Create runtime with per-client state
895    /// let runtime = SessionRuntime::new(
896    ///     &mut session, &mut mode_stack, &mut windows, &mut extensions,
897    ///     &kernel, &executor,
898    /// );
899    /// ```
900    #[must_use]
901    pub fn bootstrap(id: ClientId, home_mode: ModeId) -> (Self, BootstrapState) {
902        (Self::new(id, home_mode.clone()), BootstrapState::new(home_mode))
903    }
904
905    /// Set the compositor for this session.
906    ///
907    /// Called by the layout module during session initialization.
908    /// Delegates to `self.shared.set_compositor()`.
909    pub fn set_compositor(&mut self, compositor: Box<dyn RootCompositor>) {
910        self.shared.set_compositor(compositor);
911    }
912
913    /// Get a reference to the compositor.
914    ///
915    /// Delegates to `self.shared.compositor()`.
916    #[must_use]
917    pub fn compositor(&self) -> Option<&dyn RootCompositor> {
918        self.shared.compositor()
919    }
920
921    /// Get a mutable reference to the compositor.
922    ///
923    /// Delegates to `self.shared.compositor_mut()`.
924    pub fn compositor_mut(&mut self) -> Option<&mut (dyn RootCompositor + 'static)> {
925        self.shared.compositor_mut()
926    }
927}
928
929/// Per-client cursor position snapshot for bridge tick consumption.
930///
931/// Updated by the runner after each key event. Bridges read this in
932/// `tick()` to detect cursor movement without direct access to the
933/// window layout.
934///
935/// This is a mechanism-level type: any bridge can read it, the runner
936/// writes it. No module-specific coupling.
937#[derive(Debug, Clone, Copy, PartialEq, Eq)]
938pub struct CursorSnapshot {
939    /// Cursor line (0-indexed).
940    pub line: u32,
941    /// Cursor column (0-indexed).
942    pub col: u32,
943    /// Raw buffer ID for the active buffer.
944    pub buffer_id: u64,
945}
946
947impl CursorSnapshot {
948    /// Create a new cursor snapshot.
949    #[must_use]
950    pub const fn new(line: u32, col: u32, buffer_id: u64) -> Self {
951        Self {
952            line,
953            col,
954            buffer_id,
955        }
956    }
957}
958
959impl crate::SessionExtension for CursorSnapshot {
960    fn create() -> Self {
961        Self {
962            line: u32::MAX,
963            col: u32::MAX,
964            buffer_id: 0,
965        }
966    }
967}
968
969#[cfg(test)]
970#[path = "types_tests.rs"]
971mod tests;