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
#[cfg(test)]
mod tests {
use crate::model::filesystem::StdFileSystem;
use std::sync::Arc;
fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
Arc::new(StdFileSystem)
}
use crate::input::actions::get_auto_close_char;
use crate::input::multi_cursor::{add_cursor_at_next_match, AddCursorResult};
use crate::model::buffer::Buffer;
use crate::primitives::word_navigation::{find_word_start_left, find_word_start_right};
use crate::state::EditorState;
// --- Auto-Pairs Logic Tests ---
#[test]
fn test_auto_close_quotes_rust() {
// In Rust, quotes should auto-close
assert_eq!(get_auto_close_char('"', true, "rust"), Some('"'));
assert_eq!(get_auto_close_char('\'', true, "rust"), Some('\''));
}
#[test]
fn test_auto_close_quotes_text() {
// In Text, quotes should NOT auto-close
assert_eq!(get_auto_close_char('"', true, "text"), None);
assert_eq!(get_auto_close_char('\'', true, "text"), None);
// But brackets SHOULD still auto-close
assert_eq!(get_auto_close_char('(', true, "text"), Some(')'));
assert_eq!(get_auto_close_char('[', true, "text"), Some(']'));
assert_eq!(get_auto_close_char('{', true, "text"), Some('}'));
}
// --- Word Movement Tests ---
#[test]
fn test_word_movement_punctuation() {
let buffer = Buffer::from_str("foo.bar_baz", 0, test_fs());
// "foo|.bar_baz" -> Right -> "foo.|bar_baz"
// Starting at 3 (after 'foo')
// It should stop at start of '.', then start of 'bar'
// Current impl of find_word_start_right:
// skip current class, then skip whitespace.
// Position 0 ('f'): Word
// next boundary is 3 ('o' -> '.')
assert_eq!(find_word_start_right(&buffer, 0), 3);
// Position 3 ('.'): Punctuation
// next boundary is 4 ('.' -> 'b')
assert_eq!(find_word_start_right(&buffer, 3), 4);
// Position 4 ('b'): Word
// 'bar_baz' is all word chars? '_' is word char.
// so it should go to end (11)
assert_eq!(find_word_start_right(&buffer, 4), 11);
}
#[test]
fn test_word_movement_whitespace_punctuation() {
// "a . b"
let buffer = Buffer::from_str("a . b", 0, test_fs());
// 0 ('a') -> Word. Ends at 1. Skip whitespace -> 2 ('.')
assert_eq!(find_word_start_right(&buffer, 0), 2);
// 2 ('.') -> Punctuation. Ends at 3. Skip whitespace -> 4 ('b')
assert_eq!(find_word_start_right(&buffer, 2), 4);
}
#[test]
fn test_word_movement_left() {
// "foo.bar"
let buffer = Buffer::from_str("foo.bar", 0, test_fs());
// 7 (end) -> Left -> 4 ('b')
// 'bar' is word.
assert_eq!(find_word_start_left(&buffer, 7), 4);
// 4 ('b') -> Left -> 3 ('.')
// '.' is punctuation
assert_eq!(find_word_start_left(&buffer, 4), 3);
// 3 ('.') -> Left -> 0 ('f')
// 'foo' is word
assert_eq!(find_word_start_left(&buffer, 3), 0);
}
// --- Multi-Cursor Tests ---
use crate::model::event::{CursorId, Event};
// Helper to apply the result of add_cursor_at_next_match to the state
fn perform_add_cursor_at_next_match(state: &mut EditorState) -> AddCursorResult {
let result = add_cursor_at_next_match(state);
if let AddCursorResult::Success { cursor, .. } = &result {
// Manually apply the change to the state since add_cursor_at_next_match is pure
// We use a high ID to avoid conflicts in simple tests
let next_id = CursorId(state.cursors.iter().count());
state.apply(&Event::AddCursor {
cursor_id: next_id,
position: cursor.position,
anchor: cursor.anchor,
});
}
result
}
// Helper to create a basic editor state
fn create_state(content: &str) -> EditorState {
let mut state = EditorState::new(0, 0, 1024 * 1024, test_fs()); // sizes don't matter for these tests
// Manually replace buffer
let buffer = Buffer::from_str(content, 0, test_fs());
// We need to swap the buffer. EditorState fields are public?
state.buffer = buffer;
state
}
#[test]
fn test_ctrl_d_basic() {
let mut state = create_state("foo foo foo");
// Select first "foo"
state.cursors.primary_mut().position = 3;
state.cursors.primary_mut().set_anchor(0);
// Add next match
match perform_add_cursor_at_next_match(&mut state) {
AddCursorResult::Success { total_cursors, .. } => {
assert_eq!(total_cursors, 2);
let cursors: Vec<_> = state.cursors.iter().map(|(_, c)| c.position).collect();
// Should have cursor at 3 and 7 (end of second foo)
assert!(cursors.contains(&3));
assert!(cursors.contains(&7));
}
_ => panic!("Failed to add cursor"),
}
}
#[test]
fn test_ctrl_d_skip_overlap() {
let mut state = create_state("foo foo foo");
// Cursor 1 on first "foo"
state.cursors.primary_mut().position = 3;
state.cursors.primary_mut().set_anchor(0);
// Manually add cursor on SECOND "foo" (4..7)
// We use a hack or just ensure we simulate it properly.
// Let's add it via add_cursor_at_next_match FIRST
perform_add_cursor_at_next_match(&mut state); // Now we have 2 cursors
// Now try to add THIRD match. It should skip the second one (already valid) and find the third.
match perform_add_cursor_at_next_match(&mut state) {
AddCursorResult::Success { total_cursors, .. } => {
assert_eq!(total_cursors, 3);
// Should have cursors at 3, 7, 11
let cursors: Vec<_> = state.cursors.iter().map(|(_, c)| c.position).collect();
assert!(cursors.contains(&11));
}
_ => panic!("Failed to add 3rd cursor"),
}
}
#[test]
fn test_ctrl_d_wrap_around() {
let mut state = create_state("foo bar foo");
// Cursor 1 on SECOND "foo" (8..11)
state.cursors.primary_mut().position = 11;
state.cursors.primary_mut().set_anchor(8);
// Now add next match. It should wrap around to the FIRST "foo" (0..3)
match perform_add_cursor_at_next_match(&mut state) {
AddCursorResult::Success { total_cursors, .. } => {
assert_eq!(total_cursors, 2);
let cursors: Vec<_> = state.cursors.iter().map(|(_, c)| c.position).collect();
assert!(cursors.contains(&11));
assert!(cursors.contains(&3)); // Wrap around found start match
}
_ => panic!("Failed to wrap around"),
}
}
#[test]
fn test_ctrl_d_wrap_skip_existing() {
let mut state = create_state("foo foo foo");
// Cursor on 3rd foo
state.cursors.primary_mut().position = 11;
state.cursors.primary_mut().set_anchor(8);
// Existing cursor on 1st foo
use crate::model::event::CursorId;
// Need to add cursor manually. Since state.cursors is Cursors struct (probably private fields?)
// EditorState has 'cursors: Cursors'.
// We can't clear easily, but we can verify behavior if we assume the first cursor is the primary one we set?
// Actually, let's just use add_cursor_at_next_match to setup.
// Reset state
let mut state = create_state("foo foo foo");
// Select 3rd foo properly
state.cursors.primary_mut().position = 11;
state.cursors.primary_mut().set_anchor(8);
// Manually insert a cursor at 1st foo (0..3)
// We might not have public API to insert duplicate cursor easily from test without using events
// but 'add_cursor_at_next_match' uses 'state.cursors.iter()'.
// Let's use internal specific API if available or just Event apply.
// state.apply(Event::AddCursor ... ) might be cleaner?
// But `state.apply` is available? Yes.
state.apply(&Event::AddCursor {
cursor_id: CursorId(1),
position: 3,
anchor: Some(0),
});
// Now we have cursor on 3rd (Primary) and 1st.
// add_cursor_at_next_match should wrap around, see 1st is taken, and find 2nd "foo" (4..7).
match perform_add_cursor_at_next_match(&mut state) {
AddCursorResult::Success { total_cursors, .. } => {
assert_eq!(total_cursors, 3);
let cursors: Vec<_> = state.cursors.iter().map(|(_, c)| c.position).collect();
assert!(cursors.contains(&7));
}
res => panic!(
"Failed to find middle cursor with wrap: {:?}",
match res {
AddCursorResult::Failed { message } => message,
_ => "".to_string(),
}
),
}
}
}