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;