reovim_module_vim/operators/
case.rs1use {
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#[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 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 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 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 if transformed == original {
97 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 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 if let Some(undo_registry) = ctx.kernel.services.get::<UndoProviderRegistry>()
126 && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
127 {
128 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#[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#[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#[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
224fn 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}