1use std::collections::HashMap;
7
8pub fn is_valid_mark_name(c: char) -> bool {
10 c.is_ascii_lowercase() || c.is_ascii_digit()
11}
12
13pub fn mark_set(marks: &mut HashMap<char, usize>, name: char, top_line: usize) {
15 if is_valid_mark_name(name) {
16 marks.insert(name, top_line);
17 }
18}
19
20pub fn mark_jump(
25 marks: &HashMap<char, usize>,
26 name: char,
27 line_count: usize,
28 previous_position: &mut Option<usize>,
29 current_top: usize,
30) -> Option<usize> {
31 if !is_valid_mark_name(name) {
32 return None;
33 }
34 let raw = *marks.get(&name)?;
35 if line_count == 0 {
36 return None;
37 }
38 *previous_position = Some(current_top);
39 Some(raw.min(line_count - 1))
40}
41
42pub fn jump_previous(
45 previous_position: &mut Option<usize>,
46 current_top: usize,
47) -> Option<usize> {
48 let prev = previous_position.take()?;
49 *previous_position = Some(current_top);
50 Some(prev)
51}
52
53pub fn update_prev_position(previous_position: &mut Option<usize>, current_top: usize) {
56 *previous_position = Some(current_top);
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn is_valid_mark_name_accepts_lowercase_letters() {
65 for c in 'a'..='z' {
66 assert!(is_valid_mark_name(c), "{c} should be valid");
67 }
68 }
69
70 #[test]
71 fn is_valid_mark_name_accepts_digits() {
72 for c in '0'..='9' {
73 assert!(is_valid_mark_name(c), "{c} should be valid");
74 }
75 }
76
77 #[test]
78 fn is_valid_mark_name_rejects_uppercase_and_punctuation() {
79 assert!(!is_valid_mark_name('A'));
80 assert!(!is_valid_mark_name('Z'));
81 assert!(!is_valid_mark_name('!'));
82 assert!(!is_valid_mark_name(' '));
83 assert!(!is_valid_mark_name('\''));
84 }
85
86 #[test]
87 fn mark_set_records_top_line() {
88 let mut marks = HashMap::new();
89 mark_set(&mut marks, 'a', 42);
90 assert_eq!(marks.get(&'a'), Some(&42));
91 }
92
93 #[test]
94 fn mark_set_invalid_name_is_noop() {
95 let mut marks = HashMap::new();
96 mark_set(&mut marks, '!', 42);
97 mark_set(&mut marks, 'A', 42);
98 assert!(marks.is_empty());
99 }
100
101 #[test]
102 fn mark_set_overwrites_silently() {
103 let mut marks = HashMap::new();
104 mark_set(&mut marks, 'a', 10);
105 mark_set(&mut marks, 'a', 20);
106 assert_eq!(marks.get(&'a'), Some(&20));
107 }
108
109 #[test]
110 fn mark_jump_known_mark_returns_value_and_updates_prev() {
111 let mut marks = HashMap::new();
112 marks.insert('a', 50);
113 let mut prev = None;
114 let result = mark_jump(&marks, 'a', 1000, &mut prev, 100);
115 assert_eq!(result, Some(50));
116 assert_eq!(prev, Some(100));
117 }
118
119 #[test]
120 fn mark_jump_unknown_mark_returns_none_no_prev_update() {
121 let marks = HashMap::new();
122 let mut prev = None;
123 let result = mark_jump(&marks, 'q', 1000, &mut prev, 100);
124 assert_eq!(result, None);
125 assert_eq!(prev, None);
126 }
127
128 #[test]
129 fn mark_jump_invalid_name_returns_none() {
130 let mut marks = HashMap::new();
131 marks.insert('!', 50);
132 let mut prev = None;
133 let result = mark_jump(&marks, '!', 1000, &mut prev, 100);
134 assert_eq!(result, None);
135 }
136
137 #[test]
138 fn mark_jump_clamps_to_last_line_when_source_shrank() {
139 let mut marks = HashMap::new();
140 marks.insert('a', 500);
141 let mut prev = None;
142 let result = mark_jump(&marks, 'a', 10, &mut prev, 0);
143 assert_eq!(result, Some(9), "should clamp to line_count - 1");
144 }
145
146 #[test]
147 fn mark_jump_empty_source_returns_none() {
148 let mut marks = HashMap::new();
149 marks.insert('a', 0);
150 let mut prev = None;
151 let result = mark_jump(&marks, 'a', 0, &mut prev, 0);
152 assert_eq!(result, None);
153 assert_eq!(prev, None);
154 }
155
156 #[test]
157 fn jump_previous_first_call_returns_none() {
158 let mut prev = None;
159 let result = jump_previous(&mut prev, 50);
160 assert_eq!(result, None);
161 assert_eq!(prev, None);
162 }
163
164 #[test]
165 fn jump_previous_swaps_and_keeps_history() {
166 let mut prev = Some(10);
167 let result = jump_previous(&mut prev, 50);
168 assert_eq!(result, Some(10));
169 assert_eq!(prev, Some(50));
170 }
171
172 #[test]
173 fn jump_previous_repeated_oscillates() {
174 let mut prev = Some(10);
175 let r1 = jump_previous(&mut prev, 50);
176 assert_eq!(r1, Some(10));
177 let r2 = jump_previous(&mut prev, 10);
178 assert_eq!(r2, Some(50));
179 }
180
181 #[test]
182 fn update_prev_position_overwrites_slot() {
183 let mut prev = Some(7);
184 update_prev_position(&mut prev, 42);
185 assert_eq!(prev, Some(42));
186 }
187}