1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MarkTarget {
13 SameFile { line: usize },
14 OtherFile { file_index: usize, line: usize },
15}
16
17pub fn is_valid_mark_name(c: char) -> bool {
19 c.is_ascii_alphanumeric() && (c.is_ascii_lowercase() || c.is_ascii_digit())
20}
21
22pub fn mark_set(
24 marks: &mut HashMap<char, (usize, usize)>,
25 name: char,
26 file_index: usize,
27 top_line: usize,
28) {
29 if is_valid_mark_name(name) {
30 marks.insert(name, (file_index, top_line));
31 }
32}
33
34pub fn mark_jump(
41 marks: &HashMap<char, (usize, usize)>,
42 name: char,
43 current_file_index: usize,
44 previous_position: &mut Option<(usize, usize)>,
45 current_top: usize,
46) -> Option<MarkTarget> {
47 if !is_valid_mark_name(name) {
48 return None;
49 }
50 let (target_file, target_line) = *marks.get(&name)?;
51 *previous_position = Some((current_file_index, current_top));
52 if target_file == current_file_index {
53 Some(MarkTarget::SameFile { line: target_line })
54 } else {
55 Some(MarkTarget::OtherFile {
56 file_index: target_file,
57 line: target_line,
58 })
59 }
60}
61
62pub fn jump_previous(
65 previous_position: &mut Option<(usize, usize)>,
66 current_file_index: usize,
67 current_top: usize,
68) -> Option<MarkTarget> {
69 let (prev_file, prev_line) = previous_position.take()?;
70 *previous_position = Some((current_file_index, current_top));
71 if prev_file == current_file_index {
72 Some(MarkTarget::SameFile { line: prev_line })
73 } else {
74 Some(MarkTarget::OtherFile {
75 file_index: prev_file,
76 line: prev_line,
77 })
78 }
79}
80
81pub fn update_prev_position(
84 previous_position: &mut Option<(usize, usize)>,
85 current_file_index: usize,
86 current_top: usize,
87) {
88 *previous_position = Some((current_file_index, current_top));
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn is_valid_mark_name_accepts_lowercase_letters() {
97 for c in 'a'..='z' {
98 assert!(is_valid_mark_name(c), "{c} should be valid");
99 }
100 }
101
102 #[test]
103 fn is_valid_mark_name_accepts_digits() {
104 for c in '0'..='9' {
105 assert!(is_valid_mark_name(c), "{c} should be valid");
106 }
107 }
108
109 #[test]
110 fn is_valid_mark_name_rejects_uppercase_and_punctuation() {
111 assert!(!is_valid_mark_name('A'));
112 assert!(!is_valid_mark_name('Z'));
113 assert!(!is_valid_mark_name('!'));
114 assert!(!is_valid_mark_name(' '));
115 assert!(!is_valid_mark_name('\''));
116 }
117
118 #[test]
119 fn mark_set_records_top_line() {
120 let mut marks = HashMap::new();
121 mark_set(&mut marks, 'a', 0, 42);
122 assert_eq!(marks.get(&'a'), Some(&(0, 42)));
123 }
124
125 #[test]
126 fn mark_set_invalid_name_is_noop() {
127 let mut marks = HashMap::new();
128 mark_set(&mut marks, '!', 0, 42);
129 mark_set(&mut marks, 'A', 0, 42);
130 assert!(marks.is_empty());
131 }
132
133 #[test]
134 fn mark_set_overwrites_silently() {
135 let mut marks = HashMap::new();
136 mark_set(&mut marks, 'a', 0, 10);
137 mark_set(&mut marks, 'a', 0, 20);
138 assert_eq!(marks.get(&'a'), Some(&(0, 20)));
139 }
140
141 #[test]
142 fn mark_jump_known_mark_returns_value_and_updates_prev() {
143 let mut marks = HashMap::new();
144 marks.insert('a', (0, 50));
145 let mut prev = None;
146 let result = mark_jump(&marks, 'a', 0, &mut prev, 100);
147 assert_eq!(result, Some(MarkTarget::SameFile { line: 50 }));
148 assert_eq!(prev, Some((0, 100)));
149 }
150
151 #[test]
152 fn mark_jump_unknown_mark_returns_none_no_prev_update() {
153 let marks = HashMap::new();
154 let mut prev = None;
155 let result = mark_jump(&marks, 'q', 0, &mut prev, 100);
156 assert_eq!(result, None);
157 assert_eq!(prev, None);
158 }
159
160 #[test]
161 fn mark_jump_invalid_name_returns_none() {
162 let mut marks = HashMap::new();
163 marks.insert('!', (0, 50));
164 let mut prev = None;
165 let result = mark_jump(&marks, '!', 0, &mut prev, 100);
166 assert_eq!(result, None);
167 }
168
169 #[test]
170 fn jump_previous_first_call_returns_none() {
171 let mut prev = None;
172 let result = jump_previous(&mut prev, 0, 50);
173 assert_eq!(result, None);
174 assert_eq!(prev, None);
175 }
176
177 #[test]
178 fn jump_previous_swaps_and_keeps_history() {
179 let mut prev = Some((0, 10));
180 let result = jump_previous(&mut prev, 0, 50);
181 assert_eq!(result, Some(MarkTarget::SameFile { line: 10 }));
182 assert_eq!(prev, Some((0, 50)));
183 }
184
185 #[test]
186 fn jump_previous_repeated_oscillates() {
187 let mut prev = Some((0, 10));
188 let r1 = jump_previous(&mut prev, 0, 50);
189 assert_eq!(r1, Some(MarkTarget::SameFile { line: 10 }));
190 let r2 = jump_previous(&mut prev, 0, 10);
191 assert_eq!(r2, Some(MarkTarget::SameFile { line: 50 }));
192 }
193
194 #[test]
195 fn update_prev_position_overwrites_slot() {
196 let mut prev = Some((0, 7));
197 update_prev_position(&mut prev, 0, 42);
198 assert_eq!(prev, Some((0, 42)));
199 }
200
201 #[test]
202 fn mark_jump_returns_other_file_target_when_set_in_different_file() {
203 let mut marks = HashMap::new();
204 marks.insert('a', (1, 200));
205 let mut prev = None;
206 let result = mark_jump(&marks, 'a', 0, &mut prev, 50);
207 assert_eq!(result, Some(MarkTarget::OtherFile { file_index: 1, line: 200 }));
208 assert_eq!(prev, Some((0, 50)));
209 }
210
211 #[test]
212 fn jump_previous_returns_other_file_when_previous_was_in_another_file() {
213 let mut prev = Some((1, 75));
214 let result = jump_previous(&mut prev, 0, 25);
215 assert_eq!(result, Some(MarkTarget::OtherFile { file_index: 1, line: 75 }));
216 assert_eq!(prev, Some((0, 25)));
217 }
218}