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;