Skip to main content

reovim_server/session/
client.rs

1//! Per-client role and editing state (#480 Client Architecture Unification).
2//!
3//! Defines the unified `Client` struct that replaces the old enum model.
4//! All clients now have `EditingState` for local viewport, smooth transitions,
5//! and pending keys buffer. The `relation` field controls input routing.
6//!
7//! # Architecture
8//!
9//! ```text
10//! Session (room with shared buffers)
11//! └─ clients: HashMap<ClientId, Client>
12//!     └─ Client { id, relation, state, metadata }
13//!         ├─ relation: None              ← Independent (input → self)
14//!         ├─ relation: Following(X)      ← Spectator (input ignored)
15//!         └─ relation: Sharing(X)        ← Collaboration (input → X)
16//! ```
17//!
18//! # Behavior Matrix
19//!
20//! | Relation     | My Input       | I See          | Use Case            |
21//! |--------------|----------------|----------------|---------------------|
22//! | None         | → my state     | my state       | Solo editing        |
23//! | Following(X) | ignored        | X's state      | Spectator/present   |
24//! | Sharing(X)   | → X's state    | X's state      | Pair programming    |
25//!
26//! # State Transitions
27//!
28//! ```text
29//! Independent ←→ Following(B) ←→ Sharing(B) ←→ Independent
30//! ```
31//!
32//! Transitions are validated by [`Client::try_set_relation()`] which returns
33//! [`TransitionResult`] indicating success or the required precondition.
34//!
35//! # Validation Rules
36//!
37//! - Cannot follow/share with self
38//! - Cannot create cycles (A → B → A)
39//! - Target client must exist
40//! - Following → Sharing upgrade may require cursor sync
41
42use std::{collections::HashMap, time::SystemTime};
43
44use {
45    reovim_driver_layout::RootCompositor,
46    reovim_driver_session::{
47        CursorPosition, ExtensionMap, KeySequence, Selection, SelectionMode, TabPageSet, Viewport,
48        Window, WindowLayout,
49    },
50    reovim_kernel::api::v1::{BufferId, HistoryRing, Jumplist, MarkBank, ModeStack, RegisterBank},
51};
52
53use super::{ClientId, ring_buffer::ClientRingBuffer};
54
55// ============================================================================
56// ClientRelation
57// ============================================================================
58
59/// Relation to another client for input routing.
60///
61/// Used as `Option<ClientRelation>` where `None` means independent.
62///
63/// # Invariants
64///
65/// - `Following { target }`: Input is ignored, sees target's state
66/// - `Sharing { with }`: Input routes to target's state, sees target's state
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum ClientRelation {
69    /// Read-only spectator. Input is ignored, sees target's state.
70    Following {
71        /// Client ID being followed.
72        target: ClientId,
73    },
74    /// Bidirectional co-editing. Input goes to target's state.
75    Sharing {
76        /// Client ID to share input with.
77        with: ClientId,
78    },
79}
80
81impl ClientRelation {
82    /// Get the target client ID.
83    #[must_use]
84    pub const fn target_id(&self) -> ClientId {
85        match *self {
86            Self::Following { target } => target,
87            Self::Sharing { with } => with,
88        }
89    }
90
91    /// Check if this is a Following relation.
92    #[must_use]
93    pub const fn is_following(&self) -> bool {
94        matches!(self, Self::Following { .. })
95    }
96
97    /// Check if this is a Sharing relation.
98    #[must_use]
99    pub const fn is_sharing(&self) -> bool {
100        matches!(self, Self::Sharing { .. })
101    }
102}
103
104// ============================================================================
105// TransitionResult
106// ============================================================================
107
108/// Result of a relation transition attempt.
109///
110/// Returned by [`Client::try_set_relation()`] to indicate success or
111/// the precondition that must be met before the transition can proceed.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum TransitionResult {
114    /// Transition succeeded.
115    Ok,
116    /// Transition requires cursor sync first (for Following → Sharing upgrade).
117    ///
118    /// The caller should sync the cursor to the target position, then retry.
119    RequiresCursorSync {
120        /// Current cursor position.
121        current: CursorPosition,
122        /// Target cursor position to sync to.
123        target: CursorPosition,
124    },
125    /// Target client not found.
126    TargetNotFound(ClientId),
127    /// Cannot create cycle (A → B → A transitively).
128    WouldCreateCycle,
129    /// Cannot follow/share with self.
130    CannotTargetSelf,
131}
132
133impl TransitionResult {
134    /// Check if the transition succeeded.
135    #[must_use]
136    pub const fn is_ok(&self) -> bool {
137        matches!(self, Self::Ok)
138    }
139
140    /// Check if the transition requires cursor sync.
141    #[must_use]
142    pub const fn requires_cursor_sync(&self) -> bool {
143        matches!(self, Self::RequiresCursorSync { .. })
144    }
145}
146
147// ============================================================================
148// Cycle Detection
149// ============================================================================
150
151/// Check if setting `start` to follow/share `target` would create a cycle.
152///
153/// A cycle occurs if traversing from `target` eventually leads back to `start`.
154fn would_create_cycle(
155    start: ClientId,
156    target: ClientId,
157    clients: &HashMap<ClientId, Client>,
158) -> bool {
159    would_create_cycle_impl(start, target, clients, 10)
160}
161
162fn would_create_cycle_impl(
163    start: ClientId,
164    target: ClientId,
165    clients: &HashMap<ClientId, Client>,
166    depth: usize,
167) -> bool {
168    if depth == 0 {
169        return false; // Safety limit
170    }
171    let Some(target_client) = clients.get(&target) else {
172        return false;
173    };
174    match target_client.relation {
175        Some(
176            ClientRelation::Following { target: next } | ClientRelation::Sharing { with: next },
177        ) => {
178            if next == start {
179                return true;
180            }
181            would_create_cycle_impl(start, next, clients, depth - 1)
182        }
183        None => false,
184    }
185}
186
187// ============================================================================
188// ClientMetadata
189// ============================================================================
190
191/// Metadata about a client connection.
192///
193/// Contains identity information for display and debugging.
194#[derive(Debug, Clone)]
195pub struct ClientMetadata {
196    /// Client type identifier ("tui", "android", "web", "cli").
197    pub client_type: String,
198    /// User-friendly display name ("laptop", "phone").
199    pub display_name: String,
200    /// When the client joined (Unix milliseconds).
201    pub joined_at_ms: u64,
202}
203
204impl ClientMetadata {
205    /// Create new metadata with current timestamp.
206    #[must_use]
207    #[allow(clippy::cast_possible_truncation)] // u128 millis to u64 - safe for next 500M years
208    pub fn new(client_type: impl Into<String>, display_name: impl Into<String>) -> Self {
209        Self {
210            client_type: client_type.into(),
211            display_name: display_name.into(),
212            joined_at_ms: SystemTime::now()
213                .duration_since(SystemTime::UNIX_EPOCH)
214                .map_or(0, |d| d.as_millis() as u64),
215        }
216    }
217
218    /// Create metadata with a specific timestamp (for testing).
219    #[must_use]
220    pub fn with_timestamp(
221        client_type: impl Into<String>,
222        display_name: impl Into<String>,
223        joined_at_ms: u64,
224    ) -> Self {
225        Self {
226            client_type: client_type.into(),
227            display_name: display_name.into(),
228            joined_at_ms,
229        }
230    }
231}
232
233impl Default for ClientMetadata {
234    fn default() -> Self {
235        Self::new("unknown", "unknown")
236    }
237}
238
239// ============================================================================
240// Client
241// ============================================================================
242
243/// A client in a session.
244///
245/// All clients have `EditingState` for local viewport, smooth transitions,
246/// and pending keys buffer. The `relation` field controls input routing.
247///
248/// # Property Invariants
249///
250/// 1. `relation = None` implies client is independent (input → self)
251/// 2. `relation = Some(Following{target})` implies input is ignored
252/// 3. `relation = Some(Sharing{with})` implies input routes to `with`
253/// 4. All clients ALWAYS have `state: EditingState` (non-optional)
254/// 5. No cycles allowed: if A → B, then B cannot → A (directly or transitively)
255#[derive(Debug, Clone)]
256pub struct Client {
257    /// Unique client identifier.
258    pub id: ClientId,
259    /// Relation to another client. `None` = independent.
260    pub relation: Option<ClientRelation>,
261    /// Editing state (mode, cursor, windows, etc.). ALWAYS present.
262    pub state: EditingState,
263    /// Client metadata (type, display name, join time).
264    pub metadata: ClientMetadata,
265    /// Per-client debug ring buffer (Phase #478/#481).
266    pub ring_buffer: ClientRingBuffer,
267}
268
269impl Client {
270    /// Create a new independent client with default editing state.
271    #[must_use]
272    pub fn new(id: ClientId, metadata: ClientMetadata) -> Self {
273        Self {
274            id,
275            relation: None,
276            state: EditingState::default(),
277            metadata,
278            ring_buffer: ClientRingBuffer::new(),
279        }
280    }
281
282    /// Create client with specific mode stack.
283    #[must_use]
284    pub fn with_mode_stack(id: ClientId, metadata: ClientMetadata, mode_stack: ModeStack) -> Self {
285        Self {
286            id,
287            relation: None,
288            state: EditingState::with_mode_stack(mode_stack),
289            metadata,
290            ring_buffer: ClientRingBuffer::new(),
291        }
292    }
293
294    /// Create client with mode stack and initial window.
295    #[must_use]
296    pub fn with_mode_stack_and_window(
297        id: ClientId,
298        metadata: ClientMetadata,
299        mode_stack: ModeStack,
300        window: Window,
301    ) -> Self {
302        Self {
303            id,
304            relation: None,
305            state: EditingState::with_mode_stack_and_window(mode_stack, window),
306            metadata,
307            ring_buffer: ClientRingBuffer::new(),
308        }
309    }
310
311    /// Get the ring buffer for this client.
312    #[must_use]
313    pub const fn ring_buffer(&self) -> &ClientRingBuffer {
314        &self.ring_buffer
315    }
316
317    /// Check if client is independent (no relation).
318    #[must_use]
319    pub const fn is_independent(&self) -> bool {
320        self.relation.is_none()
321    }
322
323    /// Check if client is following another.
324    #[must_use]
325    pub const fn is_following(&self) -> bool {
326        matches!(self.relation, Some(ClientRelation::Following { .. }))
327    }
328
329    /// Check if client is sharing with another.
330    #[must_use]
331    pub const fn is_sharing(&self) -> bool {
332        matches!(self.relation, Some(ClientRelation::Sharing { .. }))
333    }
334
335    /// Get the target client ID (for Following/Sharing), or `None` if independent.
336    #[must_use]
337    pub const fn target_id(&self) -> Option<ClientId> {
338        match self.relation {
339            Some(ClientRelation::Following { target }) => Some(target),
340            Some(ClientRelation::Sharing { with }) => Some(with),
341            None => None,
342        }
343    }
344
345    // ========================================================================
346    // State Transitions (Phase 2)
347    // ========================================================================
348
349    /// Validate a relation change without applying it.
350    ///
351    /// This is a static method that checks if a relation change is valid
352    /// without mutating the client. Used by `Session::set_client_relation()`.
353    ///
354    /// # Validation Rules
355    ///
356    /// 1. Cannot target self (returns `CannotTargetSelf`)
357    /// 2. Target must exist (returns `TargetNotFound`)
358    /// 3. Cannot create cycles (returns `WouldCreateCycle`)
359    /// 4. Following → Sharing with same target may require cursor sync
360    #[must_use]
361    pub fn validate_relation_change(
362        client: &Self,
363        new_relation: Option<ClientRelation>,
364        clients: &HashMap<ClientId, Self>,
365    ) -> TransitionResult {
366        // Check self-reference
367        if let Some(
368            ClientRelation::Following { target } | ClientRelation::Sharing { with: target },
369        ) = new_relation
370        {
371            if target == client.id {
372                return TransitionResult::CannotTargetSelf;
373            }
374            // Check target exists
375            let Some(target_client) = clients.get(&target) else {
376                return TransitionResult::TargetNotFound(target);
377            };
378            // Check for cycles
379            if would_create_cycle(client.id, target, clients) {
380                return TransitionResult::WouldCreateCycle;
381            }
382
383            // Edge case: Following → Sharing with same target requires cursor sync
384            if let (
385                Some(ClientRelation::Following { target: old_target }),
386                Some(ClientRelation::Sharing { with: new_target }),
387            ) = (&client.relation, &new_relation)
388            {
389                // Only check cursor sync when upgrading from Follow to Share with same target
390                if old_target == new_target {
391                    let target_cursor = target_client
392                        .state
393                        .windows
394                        .active()
395                        .map_or_else(CursorPosition::default, |w| w.cursor);
396                    let my_cursor = client
397                        .state
398                        .windows
399                        .active()
400                        .map_or_else(CursorPosition::default, |w| w.cursor);
401                    if my_cursor != target_cursor {
402                        return TransitionResult::RequiresCursorSync {
403                            current: my_cursor,
404                            target: target_cursor,
405                        };
406                    }
407                }
408            }
409        }
410
411        TransitionResult::Ok
412    }
413
414    /// Attempt to change relation with validation.
415    ///
416    /// Returns [`TransitionResult`] indicating success or required preconditions.
417    ///
418    /// # Validation Rules
419    ///
420    /// 1. Cannot target self (returns `CannotTargetSelf`)
421    /// 2. Target must exist (returns `TargetNotFound`)
422    /// 3. Cannot create cycles (returns `WouldCreateCycle`)
423    /// 4. Following → Sharing with same target may require cursor sync
424    ///
425    /// # Examples
426    ///
427    /// ```ignore
428    /// // Independent to Following
429    /// let result = client.try_set_relation(
430    ///     Some(ClientRelation::Following { target: other_id }),
431    ///     &clients,
432    /// );
433    /// assert!(result.is_ok());
434    ///
435    /// // Back to independent
436    /// let result = client.try_set_relation(None, &clients);
437    /// assert!(result.is_ok());
438    /// ```
439    pub fn try_set_relation(
440        &mut self,
441        new_relation: Option<ClientRelation>,
442        clients: &HashMap<ClientId, Self>,
443    ) -> TransitionResult {
444        let result = Self::validate_relation_change(self, new_relation, clients);
445        if result.is_ok() {
446            self.relation = new_relation;
447        }
448        result
449    }
450
451    /// Sync cursor to target client's position.
452    ///
453    /// Used for Following → Sharing transitions that require cursor alignment.
454    pub fn sync_cursor_to(&mut self, target: &Self) {
455        if let (Some(target_window), Some(my_window)) =
456            (target.state.windows.active(), self.state.windows.active_mut())
457        {
458            my_window.cursor = target_window.cursor;
459        }
460    }
461
462    /// Force set relation without validation.
463    ///
464    /// **Use sparingly** - prefer `try_set_relation()` for safety.
465    /// This is useful for initialization or internal operations where
466    /// validation has already been performed.
467    pub const fn set_relation_unchecked(&mut self, relation: Option<ClientRelation>) {
468        self.relation = relation;
469    }
470
471    /// Get the effective state for display.
472    ///
473    /// - Independent: own state
474    /// - Following: target's state (with depth limit)
475    /// - Sharing: target's state (with depth limit)
476    ///
477    /// Returns `None` if the target/chain doesn't exist or if there's a cycle.
478    #[must_use]
479    pub fn effective_state<'a>(
480        &'a self,
481        clients: &'a HashMap<ClientId, Self>,
482    ) -> Option<&'a EditingState> {
483        match self.relation {
484            None => Some(&self.state),
485            Some(
486                ClientRelation::Following { target } | ClientRelation::Sharing { with: target },
487            ) => Self::resolve_state(target, clients, 10),
488        }
489    }
490
491    /// Get mutable reference to the effective state for input routing.
492    ///
493    /// - Independent: Returns `None` (caller should use `self.state` directly)
494    /// - Following: Returns `None` (input is ignored)
495    /// - Sharing: Returns target's state (recursively)
496    ///
497    /// Note: For independent clients, the caller must handle `self.state` specially
498    /// due to Rust borrow rules (can't borrow self and return reference to self.state).
499    pub fn effective_state_mut<'a>(
500        &'a self,
501        clients: &'a mut HashMap<ClientId, Self>,
502    ) -> Option<&'a mut EditingState> {
503        match self.relation {
504            None => {
505                // Independent: caller should use self.state directly
506                // Can't return &mut self.state here due to borrow rules
507                None
508            }
509            Some(ClientRelation::Following { .. }) => None, // Input ignored for followers
510            Some(ClientRelation::Sharing { with }) => clients.get_mut(&with).map(|c| &mut c.state),
511        }
512    }
513
514    /// Resolve state through the chain with depth limit.
515    fn resolve_state(
516        target: ClientId,
517        clients: &HashMap<ClientId, Self>,
518        depth: usize,
519    ) -> Option<&EditingState> {
520        if depth == 0 {
521            return None; // Prevent infinite loops
522        }
523
524        let client = clients.get(&target)?;
525        match client.relation {
526            None => Some(&client.state),
527            Some(
528                ClientRelation::Following { target: next } | ClientRelation::Sharing { with: next },
529            ) => Self::resolve_state(next, clients, depth - 1),
530        }
531    }
532}
533
534// ============================================================================
535// EditingState
536// ============================================================================
537
538/// Editing state for a client.
539///
540/// Contains all per-client state needed for editing operations.
541/// All clients have this state - it's not just for "owners" anymore.
542///
543/// # Multi-Client Isolation (#471)
544///
545/// Each client owns their own `WindowLayout` with independent cursors.
546/// This ensures Client A's cursor/mode doesn't affect Client B.
547/// Buffers are still shared (all clients see same text content).
548///
549/// # Client Model Mapping (#480)
550///
551/// This struct maps to `ClientViewState` in the common client model.
552/// Only a subset is transmitted:
553///
554/// | `EditingState` field | `ClientViewState` field | Transform |
555/// |----------------------|-------------------------|-----------|
556/// | `mode_stack` | `mode` | `.current().name()` |
557/// | `windows` | `cursor: Position` | `.focused().cursor` |
558/// | `windows` | `buffer_id` | `.focused().buffer_id` |
559/// | `selection` | `selection` | `.to_driver_selection()` |
560///
561/// Per-client editing state (#471, #477).
562///
563/// Contains all client-specific state including mode, windows, viewport,
564/// selection, and module extensions. Each client has independent state
565/// to prevent cross-client interference (e.g., Client A's pending count
566/// affecting Client B's motions).
567pub struct EditingState {
568    /// Mode stack (current mode on top).
569    pub mode_stack: ModeStack,
570
571    /// Keys accumulated but not yet processed.
572    pub pending_keys: KeySequence,
573
574    /// Per-client window layout with independent cursors (#471).
575    ///
576    /// Each window contains its own cursor position. This replaces
577    /// the old shared `session.windows` that caused multi-client bugs.
578    pub windows: WindowLayout,
579
580    /// Viewport (visible area).
581    pub viewport: Viewport,
582
583    /// Active selection (for visual mode).
584    pub selection: Option<ClientSelection>,
585
586    /// Per-client module extensions (#477).
587    ///
588    /// Type-erased storage for module state like `VimSessionState`,
589    /// `SearchState`, `CmdlineState`. Each client has independent
590    /// extensions to prevent state leakage between clients.
591    ///
592    /// # Why Per-Client
593    ///
594    /// Without isolation, Client A pressing `5` (`pending_count=5`) would
595    /// cause Client B's `j` to move 5 lines instead of 1. This field
596    /// ensures complete module state isolation.
597    pub extensions: ExtensionMap,
598
599    /// Per-client compositor for window layout (#474).
600    ///
601    /// Each client owns their own compositor, cloned from the shared template
602    /// at join time. This ensures window IDs are consistent between the
603    /// compositor (geometry) and per-client windows (cursor/viewport).
604    ///
605    /// Before this field, the shared compositor and per-client windows used
606    /// independent ID namespaces, causing cross-namespace mismatches in
607    /// notifications and state queries.
608    pub compositor: Option<Box<dyn RootCompositor>>,
609
610    /// Per-client tab pages (#401).
611    ///
612    /// Manages tab page lifecycle. Each tab can have its own window layout
613    /// and compositor. Currently starts with a single default tab.
614    /// Future work will integrate with `windows` and `compositor` fields
615    /// so that `active_tab().windows()` becomes the source of truth.
616    pub tabs: TabPageSet,
617
618    /// Per-client register storage (#515).
619    ///
620    /// Each client owns their own registers (unnamed `""`, named `a-z`/`A-Z`).
621    /// System clipboard (`+`, `*`) remains shared via `ClipboardProvider`.
622    /// This prevents Client A's `"ayy` from overwriting Client B's register 'a'.
623    pub registers: RegisterBank,
624
625    /// Per-client clipboard history ring (#515).
626    ///
627    /// Tracks yank/delete history for numbered registers `0-9`.
628    /// Each client has independent history so Client A's deletes don't
629    /// shift Client B's numbered registers.
630    pub clipboard_history: HistoryRing,
631
632    /// Per-client local marks (a-z, per-client special marks) (#515).
633    ///
634    /// Each client owns their own local marks. Global marks (A-Z) remain
635    /// shared in `KernelContext.global_marks`.
636    pub local_marks: MarkBank,
637
638    /// Per-client jump list for Ctrl-O / Ctrl-I navigation (#654).
639    ///
640    /// Each client owns their own jump list. Jump positions are recorded
641    /// on cursor movements across buffer boundaries or large jumps.
642    pub jumplist: Jumplist,
643
644    /// Per-client active buffer (#471).
645    ///
646    /// Each client tracks which buffer they are viewing independently.
647    /// New clients are initialized with the first kernel buffer (scratch).
648    pub active_buffer: Option<BufferId>,
649
650    /// Per-client terminal dimensions (width, height) (#471).
651    ///
652    /// Each client has independent terminal size. Initialized to VT100
653    /// default (80, 24); updated when the client sends a resize RPC.
654    pub terminal_size: (u16, u16),
655}
656
657impl std::fmt::Debug for EditingState {
658    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659        f.debug_struct("EditingState")
660            .field("mode_stack", &self.mode_stack)
661            .field("pending_keys", &self.pending_keys)
662            .field("windows", &self.windows)
663            .field("viewport", &self.viewport)
664            .field("selection", &self.selection)
665            .field("extensions", &self.extensions)
666            .field("compositor", &self.compositor.as_ref().map(|_| "..."))
667            .field("tabs", &self.tabs)
668            .field("registers", &self.registers)
669            .field("clipboard_history", &self.clipboard_history)
670            .field("local_marks", &self.local_marks)
671            .field("jumplist", &self.jumplist)
672            .field("active_buffer", &self.active_buffer)
673            .field("terminal_size", &self.terminal_size)
674            .finish()
675    }
676}
677
678// Manual Clone implementation (#477).
679//
680// ExtensionMap doesn't implement Clone (contains Box<dyn SessionExtensionDyn>).
681// Cloning creates fresh extensions - intentional for relation following where
682// spectators should have their own independent module state.
683impl Clone for EditingState {
684    fn clone(&self) -> Self {
685        Self {
686            mode_stack: self.mode_stack.clone(),
687            pending_keys: self.pending_keys.clone(),
688            windows: self.windows.clone(),
689            viewport: self.viewport, // Copy type
690            selection: self.selection.clone(),
691            extensions: ExtensionMap::new(), // Fresh extensions for cloned state
692            compositor: self.compositor.as_ref().map(|c| c.boxed_clone()), // #474
693            tabs: self.tabs.clone(),         // #401
694            registers: self.registers.clone(), // #515
695            clipboard_history: self.clipboard_history.clone(), // #515
696            local_marks: self.local_marks.clone(), // #515
697            jumplist: self.jumplist.clone(), // #654
698            active_buffer: self.active_buffer, // #471
699            terminal_size: self.terminal_size, // #471
700        }
701    }
702}
703
704impl Default for EditingState {
705    fn default() -> Self {
706        // Use a placeholder mode - the actual home mode is set during session init
707        let placeholder_mode = reovim_kernel::api::v1::ModeId::new(
708            reovim_kernel::api::v1::ModuleId::new("default"),
709            "normal",
710        );
711        Self {
712            mode_stack: ModeStack::new(placeholder_mode),
713            pending_keys: KeySequence::new(),
714            windows: WindowLayout::empty(),
715            viewport: Viewport::default(),
716            selection: None,
717            extensions: ExtensionMap::new(),
718            compositor: None,
719            tabs: TabPageSet::new(),
720            registers: RegisterBank::new(),
721            clipboard_history: HistoryRing::new(),
722            local_marks: MarkBank::new(),
723            jumplist: Jumplist::new(),
724            active_buffer: None,
725            terminal_size: (80, 24),
726        }
727    }
728}
729
730impl EditingState {
731    /// Create editing state with a specific mode stack.
732    #[must_use]
733    pub fn with_mode_stack(mode_stack: ModeStack) -> Self {
734        Self {
735            mode_stack,
736            pending_keys: KeySequence::new(),
737            windows: WindowLayout::empty(),
738            viewport: Viewport::default(),
739            selection: None,
740            extensions: ExtensionMap::new(),
741            compositor: None,
742            tabs: TabPageSet::new(),
743            registers: RegisterBank::new(),
744            clipboard_history: HistoryRing::new(),
745            local_marks: MarkBank::new(),
746            jumplist: Jumplist::new(),
747            active_buffer: None,
748            terminal_size: (80, 24),
749        }
750    }
751
752    /// Create editing state with mode stack and initial window.
753    ///
754    /// Used when a client joins a session that already has buffers.
755    #[must_use]
756    pub fn with_mode_stack_and_window(mode_stack: ModeStack, window: Window) -> Self {
757        let mut windows = WindowLayout::empty();
758        windows.add(window);
759        Self {
760            mode_stack,
761            pending_keys: KeySequence::new(),
762            windows,
763            viewport: Viewport::default(),
764            selection: None,
765            extensions: ExtensionMap::new(),
766            compositor: None,
767            tabs: TabPageSet::new(),
768            registers: RegisterBank::new(),
769            clipboard_history: HistoryRing::new(),
770            local_marks: MarkBank::new(),
771            jumplist: Jumplist::new(),
772            active_buffer: None,
773            terminal_size: (80, 24),
774        }
775    }
776
777    /// Get the current mode ID.
778    #[must_use]
779    pub fn current_mode(&self) -> &reovim_kernel::api::v1::ModeId {
780        self.mode_stack.current()
781    }
782
783    /// Clear pending keys.
784    pub fn clear_pending_keys(&mut self) {
785        self.pending_keys.clear();
786    }
787
788    /// Borrow the 7 per-client mutable fields as a [`ClientContext`].
789    ///
790    /// This bundles the fields that `SessionRuntime` needs, avoiding
791    /// 7-argument parameter lists throughout the session execution chain.
792    pub fn client_context(&mut self) -> reovim_driver_session::ClientContext<'_> {
793        reovim_driver_session::ClientContext {
794            mode_stack: &mut self.mode_stack,
795            windows: &mut self.windows,
796            extensions: &mut self.extensions,
797            compositor: &mut self.compositor,
798            tabs: &mut self.tabs,
799            registers: &mut self.registers,
800            clipboard_history: &mut self.clipboard_history,
801            local_marks: &mut self.local_marks,
802            jumplist: &mut self.jumplist,
803            active_buffer: &mut self.active_buffer,
804            terminal_size: &mut self.terminal_size,
805        }
806    }
807}
808
809// ============================================================================
810// ClientSelection
811// ============================================================================
812
813/// Selection state for a client.
814///
815/// Wraps the driver's Selection with additional client-specific info.
816#[derive(Debug, Clone)]
817pub struct ClientSelection {
818    /// Anchor position (where selection started).
819    pub anchor: CursorPosition,
820
821    /// Current cursor position (selection endpoint).
822    pub cursor: CursorPosition,
823
824    /// Selection mode (char/line/block).
825    pub mode: SelectionMode,
826}
827
828impl ClientSelection {
829    /// Create a new selection.
830    #[must_use]
831    pub const fn new(anchor: CursorPosition, cursor: CursorPosition, mode: SelectionMode) -> Self {
832        Self {
833            anchor,
834            cursor,
835            mode,
836        }
837    }
838
839    /// Convert to the driver's Selection type.
840    #[must_use]
841    pub fn to_driver_selection(&self) -> Selection {
842        Selection {
843            start: self.anchor.into(),
844            end: self.cursor.into(),
845            mode: self.mode,
846        }
847    }
848}
849
850// ============================================================================
851// Tests
852// ============================================================================
853
854#[cfg(test)]
855#[allow(clippy::similar_names)] // client1/client2/clients are clear in test context
856#[path = "client_tests.rs"]
857mod tests;