Skip to main content

tess/
marks.rs

1//! Pure helpers for marks (`m<x>` / `'<x>`) and previous-position swap (`^X^X`).
2//!
3//! Marks are session-local: a HashMap<char, usize> owned by `app::run`.
4//! Previous-position is a single `Option<usize>` slot also owned by `app::run`.
5
6use std::collections::HashMap;
7
8/// True iff `c` is a valid mark name (ASCII lowercase letter or ASCII digit).
9pub fn is_valid_mark_name(c: char) -> bool {
10    c.is_ascii_lowercase() || c.is_ascii_digit()
11}
12
13/// Set mark `name` to `top_line`. Silently no-ops on invalid mark names.
14pub 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
20/// Jump to mark `name`. Returns the line number to jump to (clamped to
21/// `[0, line_count-1]`), or `None` if the mark is unknown / name is invalid /
22/// the source is empty. On a successful jump, records current top into
23/// `previous_position`.
24pub 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
42/// Swap current top_line with the previous-position slot. Returns the new
43/// top_line, or `None` if no previous position has been recorded.
44pub 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
53/// Helper for big-jump dispatch sites: record the current top_line as the
54/// previous position before performing a discontinuous move.
55pub 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}