1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/// Cursor movement and text editing helpers for TextAreaState.
///
/// These are private implementation details extracted to keep
/// the main module under the 1000-line limit.
use super::TextAreaState;
impl TextAreaState {
/// Insert a character at the cursor position.
pub(super) fn insert(&mut self, c: char) {
self.lines[self.cursor_row].insert(self.cursor_col, c);
self.cursor_col += c.len_utf8();
}
/// Insert a newline at the cursor position.
pub(super) fn new_line(&mut self) {
let remainder = self.lines[self.cursor_row].split_off(self.cursor_col);
self.lines.insert(self.cursor_row + 1, remainder);
self.cursor_row += 1;
self.cursor_col = 0;
}
/// Delete the character before the cursor.
pub(super) fn backspace(&mut self) -> bool {
if self.cursor_col > 0 {
// Find previous character boundary
let prev_cursor = self.cursor_col;
self.cursor_col = self.lines[self.cursor_row][..self.cursor_col]
.char_indices()
.last()
.map(|(i, _)| i)
.unwrap_or(0);
self.lines[self.cursor_row].drain(self.cursor_col..prev_cursor);
true
} else if self.cursor_row > 0 {
// Join with previous line
let current_line = self.lines.remove(self.cursor_row);
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
self.lines[self.cursor_row].push_str(¤t_line);
true
} else {
false
}
}
/// Delete the character at the cursor.
pub(super) fn delete(&mut self) -> bool {
let line_len = self.lines[self.cursor_row].len();
if self.cursor_col < line_len {
// Find next character boundary
let next = self.lines[self.cursor_row][self.cursor_col..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor_col + i)
.unwrap_or(line_len);
self.lines[self.cursor_row].drain(self.cursor_col..next);
true
} else if self.cursor_row < self.lines.len() - 1 {
// Join with next line
let next_line = self.lines.remove(self.cursor_row + 1);
self.lines[self.cursor_row].push_str(&next_line);
true
} else {
false
}
}
/// Move cursor left by one character.
pub(super) fn move_left(&mut self) {
if self.cursor_col > 0 {
self.cursor_col = self.lines[self.cursor_row][..self.cursor_col]
.char_indices()
.last()
.map(|(i, _)| i)
.unwrap_or(0);
} else if self.cursor_row > 0 {
// Wrap to end of previous line
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
}
}
/// Move cursor right by one character.
pub(super) fn move_right(&mut self) {
let line_len = self.lines[self.cursor_row].len();
if self.cursor_col < line_len {
self.cursor_col = self.lines[self.cursor_row][self.cursor_col..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor_col + i)
.unwrap_or(line_len);
} else if self.cursor_row < self.lines.len() - 1 {
// Wrap to start of next line
self.cursor_row += 1;
self.cursor_col = 0;
}
}
/// Move cursor up by one line.
pub(super) fn move_up(&mut self) {
if self.cursor_row > 0 {
// Remember char position, not byte position
let char_pos = self.lines[self.cursor_row][..self.cursor_col]
.chars()
.count();
self.cursor_row -= 1;
// Restore to same char position (clamped to line length)
let line = &self.lines[self.cursor_row];
let char_count = line.chars().count();
let target_pos = char_pos.min(char_count);
self.cursor_col = line
.char_indices()
.nth(target_pos)
.map(|(i, _)| i)
.unwrap_or(line.len());
}
}
/// Move cursor down by one line.
pub(super) fn move_down(&mut self) {
if self.cursor_row < self.lines.len() - 1 {
// Remember char position, not byte position
let char_pos = self.lines[self.cursor_row][..self.cursor_col]
.chars()
.count();
self.cursor_row += 1;
// Restore to same char position (clamped to line length)
let line = &self.lines[self.cursor_row];
let char_count = line.chars().count();
let target_pos = char_pos.min(char_count);
self.cursor_col = line
.char_indices()
.nth(target_pos)
.map(|(i, _)| i)
.unwrap_or(line.len());
}
}
/// Move cursor to the start of the previous word.
pub(super) fn move_word_left(&mut self) {
if self.cursor_col == 0 {
// If at line start, move to previous line end
if self.cursor_row > 0 {
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
}
return;
}
let before = &self.lines[self.cursor_row][..self.cursor_col];
let chars: Vec<(usize, char)> = before.char_indices().collect();
// Skip trailing whitespace
let mut idx = chars.len() - 1;
while idx > 0 && chars[idx].1.is_whitespace() {
idx -= 1;
}
// Skip word characters
while idx > 0 && !chars[idx - 1].1.is_whitespace() {
idx -= 1;
}
self.cursor_col = chars.get(idx).map(|(i, _)| *i).unwrap_or(0);
}
/// Move cursor to the end of the next word.
pub(super) fn move_word_right(&mut self) {
let line_len = self.lines[self.cursor_row].len();
if self.cursor_col >= line_len {
// If at line end, move to next line start
if self.cursor_row < self.lines.len() - 1 {
self.cursor_row += 1;
self.cursor_col = 0;
}
return;
}
let after = &self.lines[self.cursor_row][self.cursor_col..];
let chars: Vec<(usize, char)> = after.char_indices().collect();
// Skip leading non-whitespace
let mut idx = 0;
while idx < chars.len() && !chars[idx].1.is_whitespace() {
idx += 1;
}
// Skip whitespace
while idx < chars.len() && chars[idx].1.is_whitespace() {
idx += 1;
}
self.cursor_col = chars
.get(idx)
.map(|(i, _)| self.cursor_col + *i)
.unwrap_or(line_len);
}
/// Delete the entire current line.
pub(super) fn delete_line(&mut self) -> bool {
if self.lines.len() > 1 {
self.lines.remove(self.cursor_row);
if self.cursor_row >= self.lines.len() {
self.cursor_row = self.lines.len() - 1;
}
// Clamp cursor column to a valid char boundary.
// Simply clamping to line_len is insufficient because that
// could leave cursor_col in the middle of a multi-byte character.
self.clamp_cursor_col();
true
} else {
// Single line: just clear it
if !self.lines[0].is_empty() {
self.lines[0].clear();
self.cursor_col = 0;
true
} else {
false
}
}
}
/// Delete from cursor to end of line.
pub(super) fn delete_to_end(&mut self) -> bool {
let line_len = self.lines[self.cursor_row].len();
if self.cursor_col < line_len {
self.lines[self.cursor_row].truncate(self.cursor_col);
true
} else {
false
}
}
/// Delete from cursor to beginning of line.
pub(super) fn delete_to_start(&mut self) -> bool {
if self.cursor_col > 0 {
self.lines[self.cursor_row].drain(..self.cursor_col);
self.cursor_col = 0;
true
} else {
false
}
}
/// Clamp cursor_col to a valid char boundary on the current line.
///
/// After operations that change the current line (e.g., delete_line),
/// cursor_col may no longer be on a char boundary. This method finds
/// the nearest valid boundary at or before cursor_col.
fn clamp_cursor_col(&mut self) {
let line = &self.lines[self.cursor_row];
let line_len = line.len();
if self.cursor_col >= line_len {
self.cursor_col = line_len;
return;
}
// If already on a boundary, nothing to do
if line.is_char_boundary(self.cursor_col) {
return;
}
// Walk backwards to find the nearest char boundary
let mut col = self.cursor_col;
while col > 0 && !line.is_char_boundary(col) {
col -= 1;
}
self.cursor_col = col;
}
}