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
//! Multi-cursor operations for adding cursors at various positions
use crate::model::cursor::Cursor;
use crate::primitives::word_navigation::{find_word_end, find_word_start};
use crate::state::EditorState;
/// Result of attempting to add a cursor
pub enum AddCursorResult {
/// Cursor was added successfully
Success {
cursor: Cursor,
total_cursors: usize,
},
/// Word was selected (no new cursor added, but primary cursor selection changed)
/// This happens when Ctrl+D is pressed with no selection - it selects the current word
WordSelected { word_start: usize, word_end: usize },
/// Operation failed with a message
Failed { message: String },
}
/// Information about a cursor's position within its line
struct CursorLineInfo {
/// Byte offset of the line start
line_start: usize,
/// Column offset from line start
col_offset: usize,
}
/// Get line info for a cursor position
fn get_cursor_line_info(state: &mut EditorState, position: usize) -> Option<CursorLineInfo> {
let mut iter = state.buffer.line_iterator(position, 80);
let (line_start, _) = iter.next_line()?;
Some(CursorLineInfo {
line_start,
col_offset: position.saturating_sub(line_start),
})
}
/// Calculate cursor position on a line, clamping to line length (excluding newline)
fn cursor_position_on_line(line_start: usize, line_content: &str, target_col: usize) -> usize {
let line_len = line_content.trim_end_matches('\n').len();
line_start + target_col.min(line_len)
}
/// Create a successful AddCursorResult
fn success_result(cursor: Cursor, state: &EditorState) -> AddCursorResult {
AddCursorResult::Success {
cursor,
total_cursors: state.cursors.iter().count() + 1,
}
}
/// Adjust cursor position if it's on a newline character
/// Returns position + 1 if cursor is at a newline, otherwise returns position unchanged
fn adjust_position_for_newline(state: &mut EditorState, position: usize) -> usize {
if position < state.buffer.len() {
if let Ok(byte_at_cursor) = state.buffer.get_text_range_mut(position, 1) {
if byte_at_cursor.first() == Some(&b'\n') {
return position + 1;
}
}
}
position
}
/// Add a cursor at the next occurrence of the selected text
/// If no selection, selects the entire word at cursor position first
pub fn add_cursor_at_next_match(state: &mut EditorState) -> AddCursorResult {
// Get the selected text from the primary cursor
let primary = state.cursors.primary();
let selection_range = match primary.selection_range() {
Some(range) => range,
None => {
// No selection - select the entire word at cursor position
let cursor_pos = primary.position;
let word_start = find_word_start(&state.buffer, cursor_pos);
// Determine word_end: if we're just past a word (at a non-word char but
// word_start < cursor_pos), use cursor_pos as the end. This handles the
// case where cursor is at the space right after a word.
let word_end = if word_start < cursor_pos {
// Check if we're at a word character
let at_word_char = if cursor_pos < state.buffer.len() {
if let Ok(bytes) = state.buffer.get_text_range_mut(cursor_pos, 1) {
bytes
.first()
.map(|&b| crate::primitives::word_navigation::is_word_char(b))
.unwrap_or(false)
} else {
false
}
} else {
false
};
if at_word_char {
// We're in the middle of a word, find the actual end
find_word_end(&state.buffer, cursor_pos)
} else {
// We're just past a word, use cursor position as end
cursor_pos
}
} else {
// word_start == cursor_pos, find the end normally
find_word_end(&state.buffer, cursor_pos)
};
// If cursor is on whitespace or punctuation (word_start == word_end), fail
if word_start == word_end {
return AddCursorResult::Failed {
message: "No word at cursor position".to_string(),
};
}
// Return WordSelected so caller can update the cursor's selection
return AddCursorResult::WordSelected {
word_start,
word_end,
};
}
};
// Determine if the original selection is "backward" (cursor at start of selection)
let cursor_at_start = primary.position == selection_range.start;
// Extract the selected text
let pattern = state.get_text_range(selection_range.start, selection_range.end);
let pattern_len = pattern.len();
// Start searching from the end of the current selection
let mut search_start = selection_range.end;
let _ign = search_start; // To prevent infinite loops (unused now)
// Loop until we find a match that isn't already occupied by a cursor
loop {
let match_pos = match state.buffer.find_next(&pattern, search_start) {
Some(pos) => pos,
None => {
// If finding next failed even with wrap-around (implied by buffer.find_next usually),
// then truly no matches exist.
return AddCursorResult::Failed {
message: "No more matches".to_string(),
};
}
};
// Calculate the range of the found match
let match_range = match_pos..(match_pos + pattern_len);
// Check if any existing cursor overlaps with this match
let is_occupied = state.cursors.iter().any(|(_, c)| {
if let Some(r) = c.selection_range() {
r == match_range
} else {
false
}
});
if !is_occupied {
// Found a free match!
let match_start = match_pos;
let match_end = match_pos + pattern_len;
let new_cursor = if cursor_at_start {
let mut cursor = Cursor::new(match_start);
cursor.set_anchor(match_end);
cursor
} else {
Cursor::with_selection(match_start, match_end)
};
return success_result(new_cursor, state);
}
// If we wrapped around and came back to where we started searching (or past it), stop to avoid infinite loop
// We need to handle the case where find_next wraps around.
// Assuming buffer.find_next does wrap around:
// If match_pos <= search_start and we haven't wrapped explicitly, it means we wrapped.
// Let's refine the search start. We want to search *after* this occupied match.
// If match_pos is behind us, we wrapped.
let next_start = match_pos + pattern_len;
// Simple cycle detection: if we are stuck on the same spot or have cycled through the whole buffer
// Ideally we check if we've visited this match_pos before, but checking if we passed initial_start again is a decent proxy
// provided we handle the wrap-around logic correctly.
// If find_next scans the whole buffer, it might return the same spot if it's the only match.
// If it's occupied, we are done.
// To be safe against infinite loops if all matches are occupied:
if match_pos == selection_range.start {
// We wrapped all the way back to the primary cursor without finding a free spot
return AddCursorResult::Failed {
message: "All matches are already selected".to_string(),
};
}
search_start = next_start;
}
}
/// Add a cursor above the primary cursor at the same column
pub fn add_cursor_above(state: &mut EditorState) -> AddCursorResult {
let position = state.cursors.primary().position;
// Adjust position if cursor is at a newline character
// This handles cases where add_cursor_above/below places cursor at same column
let adjusted_position = adjust_position_for_newline(state, position);
// Get current line info
let Some(info) = get_cursor_line_info(state, adjusted_position) else {
return AddCursorResult::Failed {
message: "Unable to find current line".to_string(),
};
};
// Check if we're on the first line
if info.line_start == 0 {
return AddCursorResult::Failed {
message: "Already at first line".to_string(),
};
}
// Navigate to previous line using iterator
let mut iter = state.buffer.line_iterator(adjusted_position, 80);
iter.next_line(); // Consume current line
iter.prev(); // Move back to current line
// Get the previous line
if let Some((prev_line_start, prev_line_content)) = iter.prev() {
let new_pos = cursor_position_on_line(prev_line_start, &prev_line_content, info.col_offset);
success_result(Cursor::new(new_pos), state)
} else {
AddCursorResult::Failed {
message: "Already at first line".to_string(),
}
}
}
/// Add a cursor below the primary cursor at the same column
pub fn add_cursor_below(state: &mut EditorState) -> AddCursorResult {
let position = state.cursors.primary().position;
// Get current line info
let Some(info) = get_cursor_line_info(state, position) else {
return AddCursorResult::Failed {
message: "Unable to find current line".to_string(),
};
};
// Navigate to next line using iterator
let mut iter = state.buffer.line_iterator(position, 80);
iter.next_line(); // Consume current line
// Get next line
if let Some((next_line_start, next_line_content)) = iter.next_line() {
let new_pos = cursor_position_on_line(next_line_start, &next_line_content, info.col_offset);
success_result(Cursor::new(new_pos), state)
} else {
AddCursorResult::Failed {
message: "Already at last line".to_string(),
}
}
}