Skip to main content

reovim_driver_session/api/
changes.rs

1//! Change tracking for session operations.
2//!
3//! This module provides [`StateChanges`], which tracks what changed during
4//! session operations. The runner uses this to know what notifications to
5//! send to clients.
6//!
7//! # Design
8//!
9//! Following the mechanism vs policy principle:
10//! - **Mechanism**: `StateChanges` tracks WHAT changed
11//! - **Policy**: Runner decides HOW to notify clients
12//!
13//! # Example
14//!
15//! ```ignore
16//! use reovim_driver_session::api::StateChanges;
17//!
18//! let mut changes = StateChanges::new();
19//! changes.record_mode_change();
20//! changes.record_cursor_move(buffer_id);
21//!
22//! if changes.has_changes() {
23//!     // Runner broadcasts notifications
24//! }
25//! ```
26
27use reovim_kernel::api::v1::{BufferId, OptionValue, WindowId};
28
29/// Represents a single option change.
30#[derive(Debug, Clone)]
31pub struct OptionChange {
32    /// Name of the option that changed.
33    pub name: String,
34    /// New value.
35    pub value: OptionValue,
36    /// Window ID if window-scoped, None if global.
37    pub window_id: Option<WindowId>,
38}
39
40impl OptionChange {
41    /// Create a global option change.
42    #[must_use]
43    pub fn global(name: impl Into<String>, value: OptionValue) -> Self {
44        Self {
45            name: name.into(),
46            value,
47            window_id: None,
48        }
49    }
50
51    /// Create a window-scoped option change.
52    #[must_use]
53    pub fn window(name: impl Into<String>, value: OptionValue, window_id: WindowId) -> Self {
54        Self {
55            name: name.into(),
56            value,
57            window_id: Some(window_id),
58        }
59    }
60}
61
62/// Tracks what changed during an operation.
63///
64/// Runner uses this to know what notifications to send to clients.
65/// All changes are accumulated internally and taken at the end of
66/// an operation via [`ChangeTracker::take_changes`].
67///
68/// The multiple boolean flags are intentional - each tracks a distinct
69/// notification type that clients may need to receive.
70#[derive(Debug, Default, Clone)]
71#[allow(clippy::struct_excessive_bools)]
72pub struct StateChanges {
73    // === Mode Changes ===
74    /// Whether the mode changed.
75    pub mode_changed: bool,
76
77    // === Cursor/Selection Changes ===
78    /// Whether the cursor moved.
79    pub cursor_moved: bool,
80    /// Whether the selection changed.
81    pub selection_changed: bool,
82
83    // === Buffer Content Changes ===
84    /// Whether any buffer content was modified.
85    pub buffer_modified: bool,
86    /// Buffers whose content was modified.
87    pub modified_buffers: Vec<BufferId>,
88    /// Edit details for modified buffers (for incremental syntax parsing, #655).
89    ///
90    /// Parallel to `modified_buffers`. Contains `(BufferId, Modification)` pairs
91    /// for edits that have structured edit info. Empty when edits come from paths
92    /// that don't provide `Modification` (e.g., undo/redo).
93    pub modified_buffer_edits:
94        Vec<(BufferId, reovim_kernel::api::v1::events::kernel::Modification)>,
95    /// All buffers affected (for cursor, selection, etc.).
96    pub affected_buffers: Vec<BufferId>,
97
98    // === Buffer Lifecycle Changes ===
99    /// Buffers that were created.
100    pub buffers_created: Vec<BufferId>,
101    /// Buffers that were deleted.
102    pub buffers_deleted: Vec<BufferId>,
103    /// Buffers that were renamed: (id, `new_name`).
104    pub buffers_renamed: Vec<(BufferId, String)>,
105
106    // === Window Changes ===
107    /// Whether window layout changed.
108    pub window_changed: bool,
109    /// Windows that were created.
110    pub windows_created: Vec<WindowId>,
111    /// Windows that were closed.
112    pub windows_closed: Vec<WindowId>,
113    /// Whether window focus changed.
114    pub focus_changed: bool,
115
116    // === Option Changes (#445) ===
117    /// Whether any option changed.
118    pub option_changed: bool,
119    /// Options that changed.
120    /// Each entry contains name, value, and optional `window_id`.
121    pub options_changed: Vec<OptionChange>,
122
123    // === Scroll Changes ===
124    /// Whether scroll position changed in any window.
125    pub scroll_changed: bool,
126    /// Windows whose scroll position changed.
127    pub scrolled_windows: Vec<WindowId>,
128
129    // === Presence Changes (Phase 14) ===
130    // Note: Presence RPCs emit notifications directly (like CaptureRequest).
131    // These fields are for future cursor sync scenarios where cursor movement
132    // might trigger presence updates.
133    /// Whether presence state changed (for cursor sync scenarios).
134    pub presence_changed: bool,
135    /// Client IDs whose presence changed.
136    pub presence_updates: Vec<usize>,
137
138    // === Extension Changes (#514) ===
139    /// Whether any extension state changed (activation/deactivation).
140    pub extension_changed: bool,
141    /// Extension kinds that changed (e.g., `["cmdline"]`).
142    pub extensions_updated: Vec<String>,
143
144    // === Lifecycle Signals (#547) ===
145    /// Whether a quit was requested during this operation.
146    ///
147    /// Set when a command pushes `RuntimeSignal::Quit`. Not included in
148    /// `has_changes()` — quit is a lifecycle signal, not a state change
149    /// that triggers notifications.
150    pub should_quit: bool,
151}
152
153impl StateChanges {
154    /// Create empty changes.
155    #[must_use]
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Check if any changes occurred.
161    #[must_use]
162    pub const fn has_changes(&self) -> bool {
163        self.mode_changed
164            || self.cursor_moved
165            || self.selection_changed
166            || self.buffer_modified
167            || !self.buffers_created.is_empty()
168            || !self.buffers_deleted.is_empty()
169            || !self.buffers_renamed.is_empty()
170            || self.window_changed
171            || !self.windows_created.is_empty()
172            || !self.windows_closed.is_empty()
173            || self.focus_changed
174            || self.option_changed
175            || self.scroll_changed
176            || self.presence_changed
177            || self.extension_changed
178    }
179
180    /// Merge another `StateChanges` into this one.
181    pub fn merge(&mut self, other: Self) {
182        self.mode_changed |= other.mode_changed;
183        self.cursor_moved |= other.cursor_moved;
184        self.selection_changed |= other.selection_changed;
185        self.buffer_modified |= other.buffer_modified;
186        self.modified_buffers.extend(other.modified_buffers);
187        self.modified_buffer_edits
188            .extend(other.modified_buffer_edits);
189        self.affected_buffers.extend(other.affected_buffers);
190        self.buffers_created.extend(other.buffers_created);
191        self.buffers_deleted.extend(other.buffers_deleted);
192        self.buffers_renamed.extend(other.buffers_renamed);
193        self.window_changed |= other.window_changed;
194        self.windows_created.extend(other.windows_created);
195        self.windows_closed.extend(other.windows_closed);
196        self.focus_changed |= other.focus_changed;
197        self.option_changed |= other.option_changed;
198        self.options_changed.extend(other.options_changed);
199        self.scroll_changed |= other.scroll_changed;
200        self.scrolled_windows.extend(other.scrolled_windows);
201        // Phase 14: Presence changes
202        self.presence_changed |= other.presence_changed;
203        for client_id in other.presence_updates {
204            if !self.presence_updates.contains(&client_id) {
205                self.presence_updates.push(client_id);
206            }
207        }
208        // #514: Extension changes
209        self.extension_changed |= other.extension_changed;
210        for kind in other.extensions_updated {
211            if !self.extensions_updated.contains(&kind) {
212                self.extensions_updated.push(kind);
213            }
214        }
215        // #547: Lifecycle signals
216        self.should_quit |= other.should_quit;
217    }
218
219    // === Recording helpers ===
220
221    /// Record that the mode changed.
222    pub const fn record_mode_change(&mut self) {
223        self.mode_changed = true;
224    }
225
226    /// Record that the cursor moved in a buffer.
227    pub fn record_cursor_move(&mut self, buffer: BufferId) {
228        self.cursor_moved = true;
229        if !self.affected_buffers.contains(&buffer) {
230            self.affected_buffers.push(buffer);
231        }
232    }
233
234    /// Record that buffer content was modified.
235    pub fn record_buffer_modified(&mut self, buffer: BufferId) {
236        self.buffer_modified = true;
237        if !self.modified_buffers.contains(&buffer) {
238            self.modified_buffers.push(buffer);
239        }
240        if !self.affected_buffers.contains(&buffer) {
241            self.affected_buffers.push(buffer);
242        }
243    }
244
245    /// Record buffer modification with structured edit info (#655).
246    ///
247    /// Like `record_buffer_modified` but also stores the `Modification` data
248    /// for incremental syntax parsing. The server layer uses this to call
249    /// `driver.update()` instead of `driver.parse()`.
250    pub fn record_buffer_modified_with_edit(
251        &mut self,
252        buffer: BufferId,
253        modification: reovim_kernel::api::v1::events::kernel::Modification,
254    ) {
255        self.record_buffer_modified(buffer);
256        self.modified_buffer_edits.push((buffer, modification));
257    }
258
259    /// Record that a buffer was created.
260    pub fn record_buffer_created(&mut self, buffer: BufferId) {
261        self.buffers_created.push(buffer);
262    }
263
264    /// Record that a buffer was deleted.
265    pub fn record_buffer_deleted(&mut self, buffer: BufferId) {
266        self.buffers_deleted.push(buffer);
267    }
268
269    /// Record that a buffer was renamed.
270    pub fn record_buffer_renamed(&mut self, buffer: BufferId, new_name: String) {
271        self.buffers_renamed.push((buffer, new_name));
272    }
273
274    /// Record that a window was created.
275    pub fn record_window_created(&mut self, window: WindowId) {
276        self.window_changed = true;
277        self.windows_created.push(window);
278    }
279
280    /// Record that a window was closed.
281    pub fn record_window_closed(&mut self, window: WindowId) {
282        self.window_changed = true;
283        self.windows_closed.push(window);
284    }
285
286    /// Record that window focus changed.
287    pub const fn record_focus_change(&mut self) {
288        self.focus_changed = true;
289    }
290
291    /// Record that selection changed in a buffer.
292    pub fn record_selection_change(&mut self, buffer: BufferId) {
293        self.selection_changed = true;
294        if !self.affected_buffers.contains(&buffer) {
295            self.affected_buffers.push(buffer);
296        }
297    }
298
299    /// Record that an option changed.
300    pub fn record_option_change(&mut self, change: OptionChange) {
301        self.option_changed = true;
302        self.options_changed.push(change);
303    }
304
305    /// Record a global option change.
306    pub fn record_global_option_change(&mut self, name: impl Into<String>, value: OptionValue) {
307        self.record_option_change(OptionChange::global(name, value));
308    }
309
310    /// Record a window-scoped option change.
311    pub fn record_window_option_change(
312        &mut self,
313        name: impl Into<String>,
314        value: OptionValue,
315        window_id: WindowId,
316    ) {
317        self.record_option_change(OptionChange::window(name, value, window_id));
318    }
319
320    /// Record that scroll position changed in a window.
321    pub fn record_scroll_change(&mut self, window: WindowId) {
322        self.scroll_changed = true;
323        if !self.scrolled_windows.contains(&window) {
324            self.scrolled_windows.push(window);
325        }
326    }
327
328    /// Record that presence state changed for a client (Phase 14).
329    ///
330    /// Used for future cursor sync scenarios where cursor movement
331    /// might trigger presence updates.
332    pub fn record_presence_change(&mut self, client_id: usize) {
333        self.presence_changed = true;
334        if !self.presence_updates.contains(&client_id) {
335            self.presence_updates.push(client_id);
336        }
337    }
338
339    /// Record that an extension's state changed (#514).
340    ///
341    /// Called when a bridge's `is_active()` changes (activation/deactivation).
342    pub fn record_extension_change(&mut self, kind: String) {
343        self.extension_changed = true;
344        if !self.extensions_updated.contains(&kind) {
345            self.extensions_updated.push(kind);
346        }
347    }
348
349    /// Record that a quit was requested (#547).
350    ///
351    /// Called when a command pushes `RuntimeSignal::Quit`. The runner
352    /// uses this to signal the client to disconnect.
353    pub const fn record_quit_requested(&mut self) {
354        self.should_quit = true;
355    }
356}
357
358/// Trait for collecting accumulated changes.
359///
360/// Session runtime implements this to allow the runner to take
361/// all accumulated changes at the end of an operation.
362///
363/// # Recording Changes
364///
365/// Commands can record changes via this trait. The runner collects
366/// all changes at the end of an operation to send notifications.
367pub trait ChangeTracker: Send {
368    /// Take all accumulated changes, resetting internal state.
369    fn take_changes(&mut self) -> StateChanges;
370
371    /// Record that the cursor moved in a buffer.
372    ///
373    /// Commands should call this after moving the cursor via
374    /// `BufferApi::set_cursor_position()`.
375    fn record_cursor_move(&mut self, buffer: BufferId);
376
377    /// Record that the selection changed in a buffer (#474).
378    ///
379    /// Commands should call this after creating, modifying, or clearing
380    /// a visual selection. The notification pipeline uses this to
381    /// broadcast selection state to other clients.
382    fn record_selection_change(&mut self, buffer: BufferId);
383}
384#[cfg(test)]
385#[path = "tests/changes.rs"]
386mod tests;