Skip to main content

reovim_module_vim/commands/
change.rs

1//! Change commands.
2//!
3//! Provides change commands that delete text and enter insert mode:
4//! - `ChangeLine` (cc)
5//! - `ChangeToEndOfLine` (C)
6//!
7//! # Epic #372 - Mode Ownership
8//!
9//! These commands use `VimMode::INSERT_ID` to transition to insert mode after
10//! deleting, which is why they belong in the vim module.
11
12use {
13    reovim_driver_command::{
14        ArgKind, ArgSpec, Command, CommandContext, CommandHandler, CommandResult,
15    },
16    reovim_driver_session::{BufferApi, SessionRuntime, TransitionContext, api::ModeApi},
17    reovim_kernel::api::v1::{CommandId, Position, RegisterContent},
18};
19
20/// Helper to get cursor position from the active window.
21fn get_cursor_position(runtime: &SessionRuntime<'_>) -> Option<Position> {
22    let window = runtime.windows().active()?;
23    Some(Position::new(window.cursor.line, window.cursor.column))
24}
25
26/// Helper to set cursor position on the active window.
27#[cfg_attr(coverage_nightly, coverage(off))]
28fn set_cursor_position(runtime: &mut SessionRuntime<'_>, pos: Position) {
29    if let Some(window) = runtime.windows_mut().active_mut() {
30        window.cursor = pos.into();
31    }
32}
33
34use crate::{ids, modes::VimMode};
35
36/// Change current line (cc).
37///
38/// Clears the content of the current line(s) and enters insert mode.
39/// Unlike `dd`, this keeps the line(s) but empties their content.
40/// The deleted text is stored in the register as linewise.
41#[derive(Debug, Clone, Copy, Default)]
42pub struct ChangeLine;
43
44impl Command for ChangeLine {
45    fn id(&self) -> CommandId {
46        ids::CHANGE_LINE
47    }
48
49    fn description(&self) -> &'static str {
50        "Change current line"
51    }
52
53    fn args(&self) -> Vec<ArgSpec> {
54        vec![
55            ArgSpec::optional("count", ArgKind::Count, "Number of lines to change"),
56            ArgSpec::optional("register", ArgKind::Register, "Target register"),
57        ]
58    }
59}
60
61impl CommandHandler for ChangeLine {
62    #[cfg_attr(coverage_nightly, coverage(off))]
63    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
64        let Some(buffer_id) = args.buffer_id() else {
65            return CommandResult::error("No active buffer");
66        };
67
68        let count = args.count().unwrap_or(1);
69        let start_line = get_cursor_position(runtime).map_or(0, |p| p.line);
70        let line_count = runtime.buffer_line_count(buffer_id).unwrap_or(0);
71
72        if line_count == 0 {
73            // Empty buffer - just enter insert mode
74            runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
75            return CommandResult::Success;
76        }
77
78        // Calculate lines to change
79        let lines_to_change = count.min(line_count.saturating_sub(start_line));
80        if lines_to_change == 0 {
81            runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
82            return CommandResult::Success;
83        }
84
85        // Collect text to delete for register (include newlines between lines)
86        let mut deleted_text = String::new();
87        for i in 0..lines_to_change {
88            let line_idx = start_line + i;
89            if let Some(line) = runtime.buffer_line(buffer_id, line_idx) {
90                deleted_text.push_str(&line);
91            }
92            if i < lines_to_change - 1 {
93                deleted_text.push('\n');
94            }
95        }
96        deleted_text.push('\n'); // Linewise content ends with newline
97
98        // Store in register with clipboard sync (#515)
99        let content = RegisterContent::linewise(deleted_text);
100        let register = args.register();
101        runtime.store_register_with_sync(register, content);
102
103        // For cc: if changing multiple lines, delete all but first, then clear first
104        // Single line: just clear the content
105
106        if lines_to_change == 1 {
107            // Clear the single line content
108            let line_len = runtime.buffer_line_len(buffer_id, start_line).unwrap_or(0);
109            if line_len > 0 {
110                let delete_start = Position::new(start_line, 0);
111                let delete_end = Position::new(start_line, line_len);
112                runtime.delete_range(buffer_id, delete_start, delete_end);
113                set_cursor_position(runtime, Position::new(start_line, 0));
114            }
115            runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
116            return CommandResult::Success;
117        }
118
119        // Multiple lines: delete all content from first line to end of last changed line
120        // Then keep one empty line at start_line
121        let last_changed_line = start_line + lines_to_change - 1;
122        let last_line_len = runtime
123            .buffer_line_len(buffer_id, last_changed_line)
124            .unwrap_or(0);
125        let end_line = start_line + lines_to_change;
126
127        let (delete_start, delete_end) = if end_line >= line_count {
128            // Changing to end of buffer - delete from start of first line to end of last line
129            (Position::new(start_line, 0), Position::new(last_changed_line, last_line_len))
130        } else {
131            // Normal case: delete from start of first line to start of line after changed range
132            (Position::new(start_line, 0), Position::new(end_line, 0))
133        };
134
135        runtime.delete_range(buffer_id, delete_start, delete_end);
136        set_cursor_position(runtime, Position::new(start_line, 0));
137        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
138
139        CommandResult::Success
140    }
141}
142
143/// Change to end of line (C).
144///
145/// Deletes from cursor to end of line and enters insert mode.
146/// The deleted text is stored in the register as characterwise.
147#[derive(Debug, Clone, Copy, Default)]
148pub struct ChangeToEndOfLine;
149
150impl Command for ChangeToEndOfLine {
151    fn id(&self) -> CommandId {
152        ids::CHANGE_TO_EOL
153    }
154
155    fn description(&self) -> &'static str {
156        "Change to end of line"
157    }
158
159    fn args(&self) -> Vec<ArgSpec> {
160        vec![ArgSpec::optional(
161            "register",
162            ArgKind::Register,
163            "Target register",
164        )]
165    }
166}
167
168impl CommandHandler for ChangeToEndOfLine {
169    #[cfg_attr(coverage_nightly, coverage(off))]
170    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
171        let Some(buffer_id) = args.buffer_id() else {
172            return CommandResult::error("No active buffer");
173        };
174
175        let pos = get_cursor_position(runtime).unwrap_or_else(|| Position::new(0, 0));
176        let line_len = runtime.buffer_line_len(buffer_id, pos.line).unwrap_or(0);
177
178        // Nothing to delete if at or past end of line - just enter insert mode
179        if pos.column >= line_len {
180            runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
181            return CommandResult::Success;
182        }
183
184        // Get text to delete for register
185        let deleted_text = runtime
186            .buffer_line(buffer_id, pos.line)
187            .map(|line| line[pos.column..].to_string())
188            .unwrap_or_default();
189
190        // Store in register with clipboard sync (#515)
191        let content = RegisterContent::characterwise(deleted_text);
192        let register = args.register();
193        runtime.store_register_with_sync(register, content);
194
195        // Delete from cursor to end of line (not including newline)
196        let delete_end = Position::new(pos.line, line_len);
197        runtime.delete_range(buffer_id, pos, delete_end);
198
199        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
200
201        CommandResult::Success
202    }
203}