Skip to main content

reovim_module_vim/operators/
case.rs

1//! Case transformation operators.
2//!
3//! Implements three case operators that work with motions and text objects:
4//! - `LowercaseOperator` (gu) - converts text to lowercase
5//! - `UppercaseOperator` (gU) - converts text to uppercase
6//! - `ToggleCaseOperator` (g~) - toggles case of each character
7//!
8//! Unlike delete/yank/change, case operators do not modify registers.
9
10use {
11    reovim_driver_undo::{UndoKey, UndoProviderRegistry},
12    reovim_kernel::api::v1::{Edit, Position},
13};
14
15use super::{Operator, OperatorContext, OperatorError, Range, char_col_to_byte};
16
17// =============================================================================
18// Case Transformation Helper
19// =============================================================================
20
21/// Apply a case transformation to text in a range.
22///
23/// Reads text from the buffer, applies the transformation function,
24/// and replaces the original text if it changed.
25#[allow(clippy::too_many_lines)]
26#[cfg_attr(coverage_nightly, coverage(off))]
27fn apply_case_transform(
28    ctx: &mut OperatorContext<'_>,
29    range: Range,
30    transform: fn(&str) -> String,
31) -> Result<(), OperatorError> {
32    let buffer_arc = ctx
33        .kernel
34        .buffers
35        .get(ctx.buffer_id)
36        .ok_or(OperatorError::BufferNotFound(ctx.buffer_id))?;
37
38    let mut buffer = buffer_arc.write();
39
40    let start = range.start;
41    let end = range.end;
42
43    // Build original text from the range
44    let mut original = String::new();
45
46    if range.is_linewise {
47        let line_count = buffer.line_count();
48        let clamped_end = end.line.min(line_count.saturating_sub(1));
49
50        for line_idx in start.line..=clamped_end {
51            if line_idx > start.line {
52                original.push('\n');
53            }
54            if let Some(line) = buffer.line(line_idx) {
55                original.push_str(line);
56            }
57        }
58    } else if start.line == end.line {
59        // Single line
60        if let Some(line) = buffer.line(start.line) {
61            let char_len = line.chars().count();
62            let start_col = start.column.min(char_len);
63            let end_col = end.column.min(char_len);
64            if start_col < end_col {
65                let start_byte = char_col_to_byte(line, start_col);
66                let end_byte = char_col_to_byte(line, end_col);
67                original.push_str(&line[start_byte..end_byte]);
68            }
69        }
70    } else {
71        // Multi-line characterwise
72        for line_idx in start.line..=end.line {
73            if let Some(line) = buffer.line(line_idx) {
74                if line_idx == start.line {
75                    let char_len = line.chars().count();
76                    let start_col = start.column.min(char_len);
77                    let start_byte = char_col_to_byte(line, start_col);
78                    original.push_str(&line[start_byte..]);
79                    original.push('\n');
80                } else if line_idx == end.line {
81                    let char_len = line.chars().count();
82                    let end_col = end.column.min(char_len);
83                    let end_byte = char_col_to_byte(line, end_col);
84                    original.push_str(&line[..end_byte]);
85                } else {
86                    original.push_str(line);
87                    original.push('\n');
88                }
89            }
90        }
91    }
92
93    let transformed = transform(&original);
94
95    // Only modify buffer if something actually changed
96    if transformed == original {
97        // Even if nothing changed, set cursor_after for consistency
98        ctx.cursor_after = Some(start);
99        return Ok(());
100    }
101
102    let cursor_before = ctx.cursor_position;
103
104    if range.is_linewise {
105        let line_count = buffer.line_count();
106        let clamped_end = end.line.min(line_count.saturating_sub(1));
107
108        // Delete from start of first line to end of last line
109        let delete_start = Position::new(start.line, 0);
110        let last_line_char_len = buffer.line(clamped_end).map_or(0, |l| l.chars().count());
111        let delete_end = Position::new(clamped_end, last_line_char_len);
112
113        buffer.delete_range(delete_start, delete_end);
114        buffer.insert_at(delete_start, &transformed);
115
116        ctx.cursor_after = Some(Position::new(start.line, 0));
117    } else {
118        buffer.delete_range(start, end);
119        buffer.insert_at(start, &transformed);
120
121        ctx.cursor_after = Some(start);
122    }
123
124    // Record undo
125    if let Some(undo_registry) = ctx.kernel.services.get::<UndoProviderRegistry>()
126        && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
127    {
128        // Record as delete-then-insert pair
129        let delete_edit = Edit::Delete {
130            position: if range.is_linewise {
131                Position::new(start.line, 0)
132            } else {
133                start
134            },
135            text: original,
136        };
137        let insert_edit = Edit::Insert {
138            position: if range.is_linewise {
139                Position::new(start.line, 0)
140            } else {
141                start
142            },
143            text: transformed,
144        };
145        let cursor_after = ctx.cursor_after.unwrap_or(start);
146        undo_provider.record(
147            ctx.buffer_id,
148            vec![delete_edit, insert_edit],
149            cursor_before,
150            cursor_after,
151        );
152    }
153
154    drop(buffer);
155    Ok(())
156}
157
158// =============================================================================
159// Lowercase Operator (gu)
160// =============================================================================
161
162/// Lowercase operator - converts text to lowercase.
163#[derive(Debug, Clone, Copy)]
164pub struct LowercaseOperator;
165
166impl Operator for LowercaseOperator {
167    fn id(&self) -> &'static str {
168        "lowercase"
169    }
170
171    fn execute(&self, ctx: &mut OperatorContext<'_>, range: Range) -> Result<(), OperatorError> {
172        apply_case_transform(ctx, range, str::to_lowercase)
173    }
174
175    fn is_text_modifying(&self) -> bool {
176        true
177    }
178}
179
180// =============================================================================
181// Uppercase Operator (gU)
182// =============================================================================
183
184/// Uppercase operator - converts text to uppercase.
185#[derive(Debug, Clone, Copy)]
186pub struct UppercaseOperator;
187
188impl Operator for UppercaseOperator {
189    fn id(&self) -> &'static str {
190        "uppercase"
191    }
192
193    fn execute(&self, ctx: &mut OperatorContext<'_>, range: Range) -> Result<(), OperatorError> {
194        apply_case_transform(ctx, range, str::to_uppercase)
195    }
196
197    fn is_text_modifying(&self) -> bool {
198        true
199    }
200}
201
202// =============================================================================
203// Toggle Case Operator (g~)
204// =============================================================================
205
206/// Toggle case operator - swaps uppercase/lowercase.
207#[derive(Debug, Clone, Copy)]
208pub struct ToggleCaseOperator;
209
210impl Operator for ToggleCaseOperator {
211    fn id(&self) -> &'static str {
212        "toggle-case"
213    }
214
215    fn execute(&self, ctx: &mut OperatorContext<'_>, range: Range) -> Result<(), OperatorError> {
216        apply_case_transform(ctx, range, toggle_case)
217    }
218
219    fn is_text_modifying(&self) -> bool {
220        true
221    }
222}
223
224/// Toggle the case of each character in a string.
225fn toggle_case(s: &str) -> String {
226    s.chars()
227        .map(|c| {
228            if c.is_uppercase() {
229                c.to_lowercase().next().unwrap_or(c)
230            } else if c.is_lowercase() {
231                c.to_uppercase().next().unwrap_or(c)
232            } else {
233                c
234            }
235        })
236        .collect()
237}