Skip to main content

reovim_module_vim/operators/
delete.rs

1//! Delete 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/// Delete operator - cuts text to register.
13///
14/// Behavior:
15/// - Deletes text in the given range
16/// - Stores deleted text in the unnamed register (or specified register)
17/// - Linewise if the motion was linewise
18///
19/// # Example
20///
21/// ```ignore
22/// let delete = DeleteOperator;
23/// delete.execute(&mut ctx, range)?;
24/// ```
25#[derive(Debug, Clone, Copy)]
26pub struct DeleteOperator;
27
28impl Operator for DeleteOperator {
29    fn id(&self) -> &'static str {
30        "delete"
31    }
32
33    #[allow(clippy::too_many_lines, clippy::option_if_let_else)]
34    #[cfg_attr(coverage_nightly, coverage(off))]
35    fn execute(&self, ctx: &mut OperatorContext<'_>, range: Range) -> Result<(), OperatorError> {
36        // Get the buffer via kernel's buffer manager
37        let buffer_arc = ctx
38            .kernel
39            .buffers
40            .get(ctx.buffer_id)
41            .ok_or(OperatorError::BufferNotFound(ctx.buffer_id))?;
42
43        let mut buffer = buffer_arc.write();
44
45        // Get text before deleting
46        let start = range.start;
47        let end = range.end;
48
49        // Build deleted text from lines
50        // - register_text: for register storage (linewise = "line\n")
51        // - deleted_text: for undo (exact bytes deleted)
52        let mut register_text = String::new();
53        let mut deleted_text = String::new();
54
55        if range.is_linewise {
56            // Linewise deletion: delete entire lines from start.line to end.line (inclusive)
57            // Ignore column values - always delete full lines
58            let line_count = buffer.line_count();
59
60            // Clamp end.line to last valid line to handle counts exceeding buffer
61            let clamped_end = end.line.min(line_count.saturating_sub(1));
62
63            // Build register_text as "line\n" for each line (for paste to work correctly)
64            for line_idx in start.line..=clamped_end {
65                if let Some(line) = buffer.line(line_idx) {
66                    register_text.push_str(line);
67                    register_text.push('\n');
68                }
69            }
70
71            // For linewise, adjust the actual deletion range to cover full lines.
72            // Three cases based on Vim semantics:
73            //
74            // Case 1: Deleting non-last lines (e.g., dd on line 0 of 3-line buffer)
75            //   - Delete from start of first line through the newline
76            //   - Range: (start.line, 0) to (clamped_end + 1, 0)
77            //   - Removes: "line\n" leaving subsequent lines
78            //   - deleted_text: "line\n"
79            //
80            // Case 2: Deleting last line(s) but not all (e.g., dd on line 1 of 2-line buffer)
81            //   - Include the PRECEDING newline from previous line
82            //   - Range: (start.line - 1, prev_len) to (clamped_end, last_len)
83            //   - Removes: "\nline" leaving previous lines intact
84            //   - deleted_text: "\nline" (for correct undo)
85            //
86            // Case 3: Deleting the only line (single-line buffer)
87            //   - Delete entire content
88            //   - Range: (0, 0) to (0, content_len)
89            //   - Results in empty buffer
90            //   - deleted_text: "line"
91            let delete_start;
92            let delete_end;
93
94            if clamped_end + 1 < line_count {
95                // Case 1: Deleting non-last lines - delete through newline to next line
96                delete_start = reovim_kernel::api::v1::Position::new(start.line, 0);
97                delete_end = reovim_kernel::api::v1::Position::new(clamped_end + 1, 0);
98                // deleted_text matches what we're deleting: "line\n"
99                deleted_text.clone_from(&register_text);
100            } else if start.line > 0 {
101                // Case 2: Deleting last line(s) but not all
102                // Include the preceding newline (from end of previous line)
103                let prev_line_len = buffer.line(start.line - 1).map_or(0, |l| l.chars().count());
104                delete_start = reovim_kernel::api::v1::Position::new(start.line - 1, prev_line_len);
105                let last_line_char_len = buffer.line(clamped_end).map_or(0, |l| l.chars().count());
106                delete_end = reovim_kernel::api::v1::Position::new(clamped_end, last_line_char_len);
107                // Build deleted_text as "\nline" (preceding newline + content, no trailing newline)
108                // This matches what we're actually deleting for correct undo
109                for line_idx in start.line..=clamped_end {
110                    deleted_text.push('\n');
111                    if let Some(line) = buffer.line(line_idx) {
112                        deleted_text.push_str(line);
113                    }
114                }
115            } else {
116                // Case 3: Deleting all lines (start.line == 0 and clamped_end is last line)
117                delete_start = reovim_kernel::api::v1::Position::new(0, 0);
118                let last_line_char_len = buffer.line(clamped_end).map_or(0, |l| l.chars().count());
119                delete_end = reovim_kernel::api::v1::Position::new(clamped_end, last_line_char_len);
120                // deleted_text is just the content (no newlines - single line)
121                if let Some(line) = buffer.line(clamped_end) {
122                    deleted_text.push_str(line);
123                }
124            }
125
126            // Track which case for cursor positioning
127            let is_deleting_last_line = (clamped_end + 1 >= line_count) && start.line > 0;
128
129            // Store in register as linewise (handles +/* via ClipboardProvider)
130            let content = RegisterContent::linewise(register_text);
131            registers::store_and_sync(ctx.kernel, ctx.registers, ctx.register, &content);
132            registers::push_to_history(ctx.clipboard_history, &content);
133
134            // Use cursor position from context (passed from caller who has window access)
135            let cursor_before = ctx.cursor_position;
136
137            // Delete entire lines
138            buffer.delete_range(delete_start, delete_end);
139
140            // Cursor positioning after linewise delete follows Vim behavior:
141            //
142            // Case 1 (delete non-last lines): Cursor at column 0 of the line that
143            //   takes the place of the deleted lines (i.e., the first remaining line).
144            //
145            // Case 2 (delete last lines but not all): Cursor at the last valid column
146            //   of the new last line, since there's no line below to move to.
147            //
148            // Case 3 (delete all lines): Buffer is empty, cursor at (0, 0).
149            let line_count = buffer.line_count();
150            let final_line = start.line.min(line_count.saturating_sub(1));
151            let final_col = if is_deleting_last_line {
152                // Case 2: Cursor at last valid column of the new last line
153                let line_len = buffer.line_len(final_line).unwrap_or(0);
154                if line_len == 0 {
155                    0
156                } else {
157                    line_len.saturating_sub(1)
158                }
159            } else {
160                // Case 1: Cursor at column 0
161                0
162            };
163            // Cursor position after delete — used for both undo tracking and
164            // communicating desired cursor back to execute_operator (#552)
165            let cursor_after = reovim_kernel::api::v1::Position::new(final_line, final_col);
166            ctx.cursor_after = Some(cursor_after);
167
168            // Record edit for undo
169            if let Some(undo_registry) = ctx.kernel.services.get::<UndoProviderRegistry>()
170                && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
171            {
172                let edit = Edit::Delete {
173                    position: delete_start,
174                    text: deleted_text.clone(),
175                };
176                undo_provider.record(ctx.buffer_id, vec![edit], cursor_before, cursor_after);
177            }
178        } else {
179            // Characterwise deletion
180            if start.line == end.line {
181                // Single line deletion
182                if let Some(line) = buffer.line(start.line) {
183                    let char_len = line.chars().count();
184                    let start_col = start.column.min(char_len);
185                    let end_col = end.column.min(char_len);
186                    if start_col < end_col {
187                        let start_byte = char_col_to_byte(line, start_col);
188                        let end_byte = char_col_to_byte(line, end_col);
189                        deleted_text.push_str(&line[start_byte..end_byte]);
190                    }
191                }
192            } else {
193                // Multi-line deletion
194                for line_idx in start.line..=end.line {
195                    if let Some(line) = buffer.line(line_idx) {
196                        if line_idx == start.line {
197                            let char_len = line.chars().count();
198                            let start_col = start.column.min(char_len);
199                            let start_byte = char_col_to_byte(line, start_col);
200                            deleted_text.push_str(&line[start_byte..]);
201                            deleted_text.push('\n');
202                        } else if line_idx == end.line {
203                            let char_len = line.chars().count();
204                            let end_col = end.column.min(char_len);
205                            let end_byte = char_col_to_byte(line, end_col);
206                            deleted_text.push_str(&line[..end_byte]);
207                        } else {
208                            deleted_text.push_str(line);
209                            deleted_text.push('\n');
210                        }
211                    }
212                }
213            }
214
215            // Store in register as characterwise (handles +/* via ClipboardProvider)
216            let content = RegisterContent::characterwise(deleted_text.clone());
217            registers::store_and_sync(ctx.kernel, ctx.registers, ctx.register, &content);
218            registers::push_to_history(ctx.clipboard_history, &content);
219
220            // Use cursor position from context (passed from caller who has window access)
221            let cursor_before = ctx.cursor_position;
222
223            // Delete the text from buffer
224            buffer.delete_range(start, end);
225
226            // Cursor after characterwise delete is at the start position (#552)
227            let cursor_after = start;
228            ctx.cursor_after = Some(cursor_after);
229
230            // Record edit for undo
231            if let Some(undo_registry) = ctx.kernel.services.get::<UndoProviderRegistry>()
232                && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
233            {
234                let edit = Edit::Delete {
235                    position: start,
236                    text: deleted_text,
237                };
238                undo_provider.record(ctx.buffer_id, vec![edit], cursor_before, cursor_after);
239            }
240        }
241
242        drop(buffer);
243        Ok(())
244    }
245
246    fn is_linewise(&self) -> bool {
247        false // Default; actual linewise-ness is determined by motion
248    }
249
250    fn is_text_modifying(&self) -> bool {
251        true
252    }
253}