fresh/input/
multi_cursor.rs1use crate::model::cursor::Cursor;
4use crate::state::EditorState;
5
6pub enum AddCursorResult {
8 Success {
10 cursor: Cursor,
11 total_cursors: usize,
12 },
13 Failed { message: String },
15}
16
17struct CursorLineInfo {
19 line_start: usize,
21 col_offset: usize,
23}
24
25fn get_cursor_line_info(state: &mut EditorState, position: usize) -> Option<CursorLineInfo> {
27 let mut iter = state.buffer.line_iterator(position, 80);
28 let (line_start, _) = iter.next_line()?;
29 Some(CursorLineInfo {
30 line_start,
31 col_offset: position.saturating_sub(line_start),
32 })
33}
34
35fn cursor_position_on_line(line_start: usize, line_content: &str, target_col: usize) -> usize {
37 let line_len = line_content.trim_end_matches('\n').len();
38 line_start + target_col.min(line_len)
39}
40
41fn success_result(cursor: Cursor, state: &EditorState) -> AddCursorResult {
43 AddCursorResult::Success {
44 cursor,
45 total_cursors: state.cursors.iter().count() + 1,
46 }
47}
48
49fn adjust_position_for_newline(state: &mut EditorState, position: usize) -> usize {
52 if position < state.buffer.len() {
53 if let Ok(byte_at_cursor) = state.buffer.get_text_range_mut(position, 1) {
54 if byte_at_cursor.first() == Some(&b'\n') {
55 return position + 1;
56 }
57 }
58 }
59 position
60}
61
62pub fn add_cursor_at_next_match(state: &mut EditorState) -> AddCursorResult {
65 let primary = state.cursors.primary();
67 let selection_range = match primary.selection_range() {
68 Some(range) => range,
69 None => {
70 return AddCursorResult::Failed {
71 message: "No selection to match".to_string(),
72 }
73 }
74 };
75
76 let cursor_at_start = primary.position == selection_range.start;
78
79 let pattern = state.get_text_range(selection_range.start, selection_range.end);
81 let pattern_len = pattern.len();
82
83 let mut search_start = selection_range.end;
85 let _ign = search_start; loop {
89 let match_pos = match state.buffer.find_next(&pattern, search_start) {
90 Some(pos) => pos,
91 None => {
92 return AddCursorResult::Failed {
95 message: "No more matches".to_string(),
96 };
97 }
98 };
99
100 let match_range = match_pos..(match_pos + pattern_len);
102
103 let is_occupied = state.cursors.iter().any(|(_, c)| {
105 if let Some(r) = c.selection_range() {
106 r == match_range
107 } else {
108 false
109 }
110 });
111
112 if !is_occupied {
113 let match_start = match_pos;
115 let match_end = match_pos + pattern_len;
116 let new_cursor = if cursor_at_start {
117 let mut cursor = Cursor::new(match_start);
118 cursor.set_anchor(match_end);
119 cursor
120 } else {
121 Cursor::with_selection(match_start, match_end)
122 };
123 return success_result(new_cursor, state);
124 }
125
126 let next_start = match_pos + pattern_len;
135
136 if match_pos == selection_range.start {
145 return AddCursorResult::Failed {
147 message: "All matches are already selected".to_string(),
148 };
149 }
150
151 search_start = next_start;
152 }
153}
154
155pub fn add_cursor_above(state: &mut EditorState) -> AddCursorResult {
157 let position = state.cursors.primary().position;
158
159 let adjusted_position = adjust_position_for_newline(state, position);
162
163 let Some(info) = get_cursor_line_info(state, adjusted_position) else {
165 return AddCursorResult::Failed {
166 message: "Unable to find current line".to_string(),
167 };
168 };
169
170 if info.line_start == 0 {
172 return AddCursorResult::Failed {
173 message: "Already at first line".to_string(),
174 };
175 }
176
177 let mut iter = state.buffer.line_iterator(adjusted_position, 80);
179 iter.next_line(); iter.prev(); if let Some((prev_line_start, prev_line_content)) = iter.prev() {
184 let new_pos = cursor_position_on_line(prev_line_start, &prev_line_content, info.col_offset);
185 success_result(Cursor::new(new_pos), state)
186 } else {
187 AddCursorResult::Failed {
188 message: "Already at first line".to_string(),
189 }
190 }
191}
192
193pub fn add_cursor_below(state: &mut EditorState) -> AddCursorResult {
195 let position = state.cursors.primary().position;
196
197 let Some(info) = get_cursor_line_info(state, position) else {
199 return AddCursorResult::Failed {
200 message: "Unable to find current line".to_string(),
201 };
202 };
203
204 let mut iter = state.buffer.line_iterator(position, 80);
206 iter.next_line(); if let Some((next_line_start, next_line_content)) = iter.next_line() {
210 let new_pos = cursor_position_on_line(next_line_start, &next_line_content, info.col_offset);
211 success_result(Cursor::new(new_pos), state)
212 } else {
213 AddCursorResult::Failed {
214 message: "Already at last line".to_string(),
215 }
216 }
217}