Skip to main content

reovim_module_vim/operators/
change.rs

1//! Change operator.
2//!
3//! Reference: lib/core/src/command/builtin/operator.rs (concept-extraction, not migration)
4
5use {
6    reovim_driver_undo::{UndoKey, UndoProviderRegistry},
7    reovim_kernel::api::v1::{Edit, RegisterContent},
8};
9
10use super::{Operator, OperatorContext, OperatorError, Range, char_col_to_byte, registers};
11
12/// Change operator - cuts text and signals insert mode.
13///
14/// Behavior:
15/// - Deletes text in the given range
16/// - Stores deleted text in the unnamed register (or specified register)
17/// - Signals that insert mode should be entered (via return value or event)
18///
19/// Note: The actual mode change is handled by the caller (server/display driver).
20/// The operator just deletes the text.
21///
22/// # Example
23///
24/// ```ignore
25/// let change = ChangeOperator;
26/// change.execute(&mut ctx, range)?;
27/// // Caller should now enter insert mode
28/// ```
29#[derive(Debug, Clone, Copy)]
30pub struct ChangeOperator;
31
32impl Operator for ChangeOperator {
33    fn id(&self) -> &'static str {
34        "change"
35    }
36
37    #[allow(clippy::option_if_let_else)]
38    #[cfg_attr(coverage_nightly, coverage(off))]
39    fn execute(&self, ctx: &mut OperatorContext<'_>, range: Range) -> Result<(), OperatorError> {
40        // Get the buffer via kernel's buffer manager
41        let buffer_arc = ctx
42            .kernel
43            .buffers
44            .get(ctx.buffer_id)
45            .ok_or(OperatorError::BufferNotFound(ctx.buffer_id))?;
46
47        let mut buffer = buffer_arc.write();
48
49        // Use cursor position from context (passed from caller who has window access)
50        let cursor_before = ctx.cursor_position;
51
52        // Get text before deleting
53        let start = range.start;
54        let end = range.end;
55
56        // Build deleted text from lines
57        let mut deleted_text = String::new();
58        let line_count = buffer.line_count();
59
60        // Track what was actually deleted for undo
61        let delete_pos;
62
63        if range.is_linewise {
64            // Linewise change: delete content of lines from start.line to end.line (inclusive)
65            // Unlike delete, change keeps ONE line for insertion (Vim behavior)
66            // Clamp end.line to last valid line to handle counts exceeding buffer
67            let clamped_end = end.line.min(line_count.saturating_sub(1));
68
69            for line_idx in start.line..=clamped_end {
70                if let Some(line) = buffer.line(line_idx) {
71                    deleted_text.push_str(line);
72                    deleted_text.push('\n');
73                }
74            }
75
76            // For linewise change (cc):
77            // - Delete content of all affected lines
78            // - Keep ONE empty line at start.line for insertion
79            // This means: delete from (start.line, 0) to (clamped_end, end_of_content),
80            // then if multiple lines, delete the extra newlines to leave just one line
81            let delete_start = reovim_kernel::api::v1::Position::new(start.line, 0);
82            let delete_end = if clamped_end + 1 < line_count {
83                // Not the last line - delete content but preserve start.line's newline
84                // Delete all lines but keep start.line as empty (with its newline)
85                let end_line_char_len = buffer.line(clamped_end).map_or(0, |l| l.chars().count());
86                reovim_kernel::api::v1::Position::new(clamped_end, end_line_char_len)
87            } else {
88                // End line is last line - delete to end of content (keep line structure)
89                let last_line_char_len = buffer.line(clamped_end).map_or(0, |l| l.chars().count());
90                reovim_kernel::api::v1::Position::new(clamped_end, last_line_char_len)
91            };
92
93            delete_pos = delete_start;
94            buffer.delete_range(delete_start, delete_end);
95        } else if start.line == end.line {
96            // Single line characterwise change
97            if let Some(line) = buffer.line(start.line) {
98                let char_len = line.chars().count();
99                let start_col = start.column.min(char_len);
100                let end_col = end.column.min(char_len);
101                if start_col < end_col {
102                    let start_byte = char_col_to_byte(line, start_col);
103                    let end_byte = char_col_to_byte(line, end_col);
104                    deleted_text.push_str(&line[start_byte..end_byte]);
105                }
106            }
107            delete_pos = start;
108            buffer.delete_range(start, end);
109        } else {
110            // Multi-line characterwise change
111            for line_idx in start.line..=end.line {
112                if let Some(line) = buffer.line(line_idx) {
113                    if line_idx == start.line {
114                        let char_len = line.chars().count();
115                        let start_col = start.column.min(char_len);
116                        let start_byte = char_col_to_byte(line, start_col);
117                        deleted_text.push_str(&line[start_byte..]);
118                        deleted_text.push('\n');
119                    } else if line_idx == end.line {
120                        let char_len = line.chars().count();
121                        let end_col = end.column.min(char_len);
122                        let end_byte = char_col_to_byte(line, end_col);
123                        deleted_text.push_str(&line[..end_byte]);
124                    } else {
125                        deleted_text.push_str(line);
126                        deleted_text.push('\n');
127                    }
128                }
129            }
130            delete_pos = start;
131            buffer.delete_range(start, end);
132        }
133
134        // Cursor after change is at delete_pos (where insertion will happen) (#552)
135        let cursor_after = delete_pos;
136        ctx.cursor_after = Some(cursor_after);
137
138        drop(buffer);
139
140        // Record edit for undo
141        if !deleted_text.is_empty()
142            && let Some(undo_registry) = ctx.kernel.services.get::<UndoProviderRegistry>()
143            && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
144        {
145            let edit = Edit::Delete {
146                position: delete_pos,
147                text: deleted_text.clone(),
148            };
149            undo_provider.record(ctx.buffer_id, vec![edit], cursor_before, cursor_after);
150        }
151
152        // Store in register - linewise if the range was linewise (handles +/* via ClipboardProvider)
153        let content = if range.is_linewise {
154            RegisterContent::linewise(deleted_text)
155        } else {
156            RegisterContent::characterwise(deleted_text)
157        };
158
159        registers::store_and_sync(ctx.kernel, ctx.registers, ctx.register, &content);
160        registers::push_to_history(ctx.clipboard_history, &content);
161
162        // Note: Insert mode transition is handled by the caller
163        // The server/display driver should check operator id and enter insert mode
164
165        Ok(())
166    }
167
168    fn is_linewise(&self) -> bool {
169        false // Default; actual linewise-ness is determined by motion
170    }
171
172    fn is_text_modifying(&self) -> bool {
173        true
174    }
175}