reovim-driver-session 0.14.4

Session driver for reovim - provides traits for session management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
//! Test utilities for session-based command testing.
//!
//! This module provides [`TestSessionRuntime`], which simplifies testing commands
//! that use the new `SessionRuntime` signature.
//!
//! # Example
//!
//! ```ignore
//! use reovim_driver_session::testing::TestSessionRuntime;
//! use reovim_driver_command::{CommandHandler, CommandContext, CommandResult};
//!
//! #[test]
//! fn test_enter_insert_mode() {
//!     let mut test = TestSessionRuntime::new();
//!     let cmd = EnterInsertMode;
//!     let args = CommandContext::new();
//!
//!     // Use with_runtime to automatically capture changes
//!     let result = test.with_runtime(|runtime| {
//!         cmd.execute(runtime, &args)
//!     });
//!
//!     assert_eq!(result, CommandResult::Success);
//!     test.assert_mode_name("insert");
//!     assert!(test.changes().mode_changed);
//! }
//! ```

use {
    crate::{
        ClientId, Session, WindowLayout,
        api::{CommandExecutor, CommandHandle, StateChanges},
        extension::ExtensionMap,
        runtime::SessionRuntime,
    },
    reovim_kernel::api::v1::{
        Buffer, BufferId, CommandId, HistoryRing, Jumplist, KernelContext, MarkBank, ModeId,
        ModeStack, ModuleId, Position, RegisterBank,
    },
    std::sync::Arc,
};

/// Test helper for commands using `SessionRuntime`.
///
/// Owns all the components needed to create a `SessionRuntime` and provides
/// convenient assertion methods for testing.
///
/// # Architecture (#471 Phase 0)
///
/// Per-client state (`mode_stack`, `windows`, `extensions`) is held as **separate
/// fields** instead of inside `session`. This mirrors the production architecture
/// where `EditingState` (per-client) is separate from `Session` (shared).
///
/// This separation is REQUIRED by the borrow checker: `SessionRuntime::new()` takes
/// `&mut Session` AND `&mut ModeStack` etc. If `mode_stack` were inside `session`,
/// we'd have a double mutable borrow conflict.
///
/// ```text
/// TestSessionRuntime
/// ├── session: Session              // Shared infra (terminal_size, compositor)
/// ├── mode_stack: ModeStack         // Per-client (SEPARATE field)
/// ├── windows: WindowLayout         // Per-client (SEPARATE field)
/// ├── extensions: ExtensionMap      // Per-client (SEPARATE field)
/// ├── kernel: KernelContext
/// └── changes: StateChanges
/// ```
pub struct TestSessionRuntime {
    /// Shared session infrastructure (compositor, `terminal_size`).
    ///
    /// Does NOT contain per-client state - that's in separate fields below.
    session: Session,
    /// Per-client mode stack (SEPARATE from session for borrow-checker).
    ///
    /// Commands use this via `runtime.current_mode()`, `runtime.push_mode()`, etc.
    /// Public for direct test access (e.g., `test.mode_stack.current()`).
    pub mode_stack: ModeStack,
    /// Per-client window layout with cursors (SEPARATE from session).
    ///
    /// Commands use this via `runtime.windows()`.
    /// Public for direct test access (e.g., `test.windows.active_mut()`).
    pub windows: WindowLayout,
    /// Per-client extensions (SEPARATE from session).
    ///
    /// Commands use this via `runtime.ext::<T>()`, `runtime.ext_mut::<T>()`.
    pub extensions: ExtensionMap,
    /// Per-client compositor (#474).
    ///
    /// `None` for tests that don't need compositor. Matches `EditingState.compositor`.
    compositor: Option<Box<dyn reovim_driver_layout::RootCompositor>>,
    /// Per-client tab pages (#401).
    tabs: crate::TabPageSet,
    /// Per-client register storage (#515).
    registers: RegisterBank,
    /// Per-client clipboard history ring (#515).
    clipboard_history: HistoryRing,
    /// Per-client local marks (#515).
    local_marks: MarkBank,
    jumplist: Jumplist,
    /// Per-client active buffer (#471).
    active_buffer: Option<BufferId>,
    /// Per-client terminal dimensions (#471).
    terminal_size: (u16, u16),
    kernel: KernelContext,
    executor: StubExecutor,
    /// Accumulated changes from operations.
    changes: StateChanges,
}

impl Default for TestSessionRuntime {
    fn default() -> Self {
        Self::new()
    }
}

// Test infrastructure — not production code.
#[cfg_attr(coverage_nightly, coverage(off))]
impl TestSessionRuntime {
    /// Create a `KernelContext` that uses a real buffer manager for testing.
    fn make_test_kernel() -> KernelContext {
        reovim_kernel::testing::create_test_context()
    }

    /// Create a new test runtime with default normal mode.
    #[must_use]
    pub fn new() -> Self {
        let home_mode = ModeId::new(ModuleId::new("test"), "normal");
        Self {
            session: Session::new(ClientId::new(1), home_mode.clone()), // #491: home_mode in shared
            mode_stack: ModeStack::new(home_mode),                      // Per-client state
            windows: WindowLayout::empty(),                             // Per-client state
            extensions: ExtensionMap::new(),                            // Per-client state
            compositor: None,                                           // Per-client (#474)
            tabs: crate::TabPageSet::new(),                             // Per-client (#401)
            registers: RegisterBank::new(),                             // Per-client (#515)
            clipboard_history: HistoryRing::new(),                      // Per-client (#515)
            local_marks: MarkBank::new(),                               // Per-client (#515)
            jumplist: Jumplist::new(),
            active_buffer: None,     // Per-client (#471)
            terminal_size: (80, 24), // Per-client (#471)
            kernel: Self::make_test_kernel(),
            executor: StubExecutor,
            changes: StateChanges::new(),
        }
    }

    /// Create a test runtime with a specific home mode.
    #[must_use]
    pub fn with_home_mode(mode: ModeId) -> Self {
        Self {
            session: Session::new(ClientId::new(1), mode.clone()), // #491: home_mode in shared
            mode_stack: ModeStack::new(mode),                      // Per-client state
            windows: WindowLayout::empty(),                        // Per-client state
            extensions: ExtensionMap::new(),                       // Per-client state
            compositor: None,                                      // Per-client (#474)
            tabs: crate::TabPageSet::new(),                        // Per-client (#401)
            registers: RegisterBank::new(),                        // Per-client (#515)
            clipboard_history: HistoryRing::new(),                 // Per-client (#515)
            local_marks: MarkBank::new(),                          // Per-client (#515)
            jumplist: Jumplist::new(),
            active_buffer: None,     // Per-client (#471)
            terminal_size: (80, 24), // Per-client (#471)
            kernel: Self::make_test_kernel(),
            executor: StubExecutor,
            changes: StateChanges::new(),
        }
    }

    /// Create a test runtime with a buffer containing the given content.
    #[must_use]
    pub fn with_buffer(content: &str) -> Self {
        let mut test = Self::new();

        // Create buffer with content
        let buffer = Buffer::from_string(content);
        let buffer_id = test.kernel.buffers.register(buffer);

        // Create a window displaying this buffer
        let mut window = crate::Window::new();
        window.buffer_id = Some(buffer_id);
        test.windows.add(window); // Use self.windows, NOT self.session.windows

        // Set the active buffer (per-client state)
        test.active_buffer = Some(buffer_id);

        test
    }

    /// Create a test runtime with buffer content and a specific home mode.
    ///
    /// Use for mode-sensitive tests (e.g., textobjects that behave differently
    /// in visual vs normal mode).
    #[must_use]
    pub fn with_buffer_and_mode(content: &str, mode: ModeId) -> Self {
        let mut test = Self::with_home_mode(mode);
        let buffer = Buffer::from_string(content);
        let buffer_id = test.kernel.buffers.register(buffer);
        let mut window = crate::Window::new();
        window.buffer_id = Some(buffer_id);
        test.windows.add(window);
        test.active_buffer = Some(buffer_id);
        test
    }

    /// Create a test runtime with a pre-configured window and mode.
    ///
    /// Use when tests need specific cursor position or window configuration
    /// before executing a command.
    #[must_use]
    pub fn with_window(window: crate::Window, mode: ModeId) -> Self {
        let mut test = Self::with_home_mode(mode);
        test.active_buffer = window.buffer_id;
        test.windows.add(window);
        test
    }

    /// Execute operations with a runtime, capturing changes automatically.
    ///
    /// This is the preferred way to use the test runtime. Changes are
    /// automatically captured when the callback returns.
    ///
    /// # Per-Client State (#471 Phase 0)
    ///
    /// Uses `SessionRuntime::new()` with per-client state held as
    /// **separate fields** in `TestSessionRuntime`. This matches production behavior
    /// where `EditingState` (per-client) is separate from `Session` (shared).
    ///
    /// # Example
    ///
    /// ```ignore
    /// test.with_runtime(|runtime| {
    ///     runtime.push_mode(insert_mode, TransitionContext::new());
    /// });
    /// assert!(test.changes().mode_changed);
    /// ```
    pub fn with_runtime<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut SessionRuntime<'_>) -> R,
    {
        use crate::api::ChangeTracker;

        // Phase #471 Phase 0: Use new() with per-client state from SEPARATE fields
        // (not from session, which would cause double mutable borrow)
        let client = crate::ClientContext {
            mode_stack: &mut self.mode_stack,
            windows: &mut self.windows,
            extensions: &mut self.extensions,
            compositor: &mut self.compositor,
            tabs: &mut self.tabs,
            registers: &mut self.registers,
            clipboard_history: &mut self.clipboard_history,
            local_marks: &mut self.local_marks,
            jumplist: &mut self.jumplist,
            active_buffer: &mut self.active_buffer,
            terminal_size: &mut self.terminal_size,
        };
        let mut runtime =
            SessionRuntime::new(&mut self.session, client, &self.kernel, &self.executor);
        let result = f(&mut runtime);
        let changes = ChangeTracker::take_changes(&mut runtime);
        self.changes.merge(changes);
        result
    }

    /// Get a mutable reference to the `SessionRuntime`.
    ///
    /// **Note**: Changes are NOT automatically captured when using this method.
    /// Prefer `with_runtime` for tests that need to verify changes.
    /// Use this for simple operations where change tracking isn't needed.
    ///
    /// # Per-Client State (#471 Phase 0)
    ///
    /// Uses `SessionRuntime::new()` with per-client state from separate fields.
    pub fn runtime(&mut self) -> SessionRuntime<'_> {
        let client = crate::ClientContext {
            mode_stack: &mut self.mode_stack,
            windows: &mut self.windows,
            extensions: &mut self.extensions,
            compositor: &mut self.compositor,
            tabs: &mut self.tabs,
            registers: &mut self.registers,
            clipboard_history: &mut self.clipboard_history,
            local_marks: &mut self.local_marks,
            jumplist: &mut self.jumplist,
            active_buffer: &mut self.active_buffer,
            terminal_size: &mut self.terminal_size,
        };
        SessionRuntime::new(&mut self.session, client, &self.kernel, &self.executor)
    }

    /// Take accumulated changes and reset the tracker.
    ///
    /// Returns all changes that have been recorded since the last call.
    pub fn take_changes(&mut self) -> StateChanges {
        std::mem::take(&mut self.changes)
    }

    /// Get reference to all accumulated changes (doesn't reset).
    #[must_use]
    pub const fn changes(&self) -> &StateChanges {
        &self.changes
    }

    // === Assertions ===

    /// Assert the current mode matches the expected mode ID.
    ///
    /// # Panics
    ///
    /// Panics if the current mode doesn't match.
    pub fn assert_mode(&self, expected: &ModeId) {
        let current = self.mode_stack.current(); // Use separate field, NOT session.mode_stack
        assert_eq!(current, expected, "Expected mode {expected:?}, got {current:?}");
    }

    /// Assert the current mode name matches (ignores module).
    ///
    /// # Panics
    ///
    /// Panics if the mode name doesn't match.
    #[cfg_attr(coverage_nightly, coverage(off))]
    pub fn assert_mode_name(&self, expected_name: &str) {
        let current = self.mode_stack.current(); // Use separate field
        assert_eq!(
            current.name(),
            expected_name,
            "Expected mode name '{}', got '{}'",
            expected_name,
            current.name()
        );
    }

    /// Assert the mode stack depth.
    ///
    /// # Panics
    ///
    /// Panics if the depth doesn't match.
    pub fn assert_mode_depth(&self, expected: usize) {
        let depth = self.mode_stack.depth(); // Use separate field
        assert_eq!(depth, expected, "Expected mode depth {expected}, got {depth}");
    }

    /// Assert the cursor position for the active buffer.
    ///
    /// # Panics
    ///
    /// Panics if no active window or cursor position doesn't match.
    pub fn assert_cursor(&self, line: usize, column: usize) {
        let window = self
            .windows // Use separate field, NOT session.windows
            .active()
            .expect("No active window for cursor assertion");
        assert_eq!(
            (window.cursor.line, window.cursor.column),
            (line, column),
            "Expected cursor at ({}, {}), got ({}, {})",
            line,
            column,
            window.cursor.line,
            window.cursor.column
        );
    }

    /// Assert the buffer content for the active buffer.
    ///
    /// # Panics
    ///
    /// Panics if no active buffer or content doesn't match.
    pub fn assert_buffer_content(&self, expected: &str) {
        let buffer_id = self
            .active_buffer
            .expect("No active buffer for content assertion");
        let buffer = self
            .kernel
            .buffers
            .get(buffer_id)
            .expect("Buffer not found");
        let content = buffer.read().content();
        assert_eq!(
            content, expected,
            "Buffer content mismatch.\nExpected:\n{expected}\nGot:\n{content}"
        );
    }

    /// Assert the buffer line count for the active buffer.
    ///
    /// # Panics
    ///
    /// Panics if no active buffer or line count doesn't match.
    pub fn assert_line_count(&self, expected: usize) {
        let buffer_id = self
            .active_buffer
            .expect("No active buffer for line count assertion");
        let buffer = self
            .kernel
            .buffers
            .get(buffer_id)
            .expect("Buffer not found");
        let count = buffer.read().line_count();
        assert_eq!(count, expected, "Expected {expected} lines, got {count}");
    }

    /// Assert window count.
    ///
    /// # Panics
    ///
    /// Panics if window count doesn't match.
    pub fn assert_window_count(&self, expected: usize) {
        let count = self.windows.len(); // Use separate field
        assert_eq!(count, expected, "Expected {expected} windows, got {count}");
    }

    // === Getters for advanced assertions ===

    /// Get the current mode ID.
    #[must_use]
    pub fn current_mode(&self) -> &ModeId {
        self.mode_stack.current() // Use separate field
    }

    /// Get the active buffer ID, if any.
    #[must_use]
    pub const fn active_buffer(&self) -> Option<BufferId> {
        self.active_buffer
    }

    /// Get the cursor position for the active buffer.
    #[must_use]
    pub fn cursor_position(&self) -> Option<Position> {
        self.windows // Use separate field
            .active()
            .map(|w| Position::new(w.cursor.line, w.cursor.column))
    }

    /// Get buffer content for the active buffer.
    #[must_use]
    pub fn buffer_content(&self) -> Option<String> {
        self.active_buffer
            .and_then(|id| self.kernel.buffers.get(id).map(|b| b.read().content()))
    }

    /// Get direct access to the kernel context.
    #[must_use]
    pub const fn kernel(&self) -> &KernelContext {
        &self.kernel
    }

    /// Get direct access to the session.
    #[must_use]
    pub const fn session(&self) -> &Session {
        &self.session
    }

    /// Set the compositor for layout testing.
    ///
    /// Required for testing commands that need window navigation, splitting,
    /// or other compositor operations (e.g., window-ops commands).
    pub fn set_compositor(&mut self, compositor: Box<dyn reovim_driver_layout::RootCompositor>) {
        self.compositor = Some(compositor);
    }
}

/// Stub command executor for testing.
///
/// Returns `None` (command not found) for all lookups.
pub struct StubExecutor;

#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandExecutor for StubExecutor {
    fn get_handle(&self, _id: &CommandId) -> Option<Arc<dyn CommandHandle>> {
        None
    }
}
#[cfg(test)]
#[path = "testing_tests.rs"]
mod tests;