Skip to main content

mq_edit/
navigation.rs

1use std::path::PathBuf;
2
3/// A location in a file (file path, line, column)
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct FileLocation {
6    pub path: PathBuf,
7    pub line: usize,
8    pub column: usize,
9}
10
11impl FileLocation {
12    pub fn new(path: PathBuf, line: usize, column: usize) -> Self {
13        Self { path, line, column }
14    }
15}
16
17/// Navigation history manager for tracking jump history (back/forward)
18pub struct NavigationHistory {
19    /// Stack of previous locations
20    history: Vec<FileLocation>,
21    /// Current position in history (index)
22    current: usize,
23}
24
25impl NavigationHistory {
26    /// Create a new navigation history
27    pub fn new() -> Self {
28        Self {
29            history: Vec::new(),
30            current: 0,
31        }
32    }
33
34    /// Push a new location to history
35    /// This clears any forward history if we're not at the end
36    pub fn push(&mut self, location: FileLocation) {
37        // Don't add duplicate consecutive locations
38        if let Some(last) = self.history.last()
39            && last == &location
40        {
41            return;
42        }
43
44        // If we're not at the end of history, truncate forward history
45        if self.current < self.history.len() {
46            self.history.truncate(self.current);
47        }
48
49        // Add new location
50        self.history.push(location);
51        self.current = self.history.len();
52    }
53
54    /// Go back in history
55    /// Returns the previous location if available
56    pub fn back(&mut self) -> Option<&FileLocation> {
57        if self.current > 1 {
58            self.current -= 1;
59            self.history.get(self.current - 1)
60        } else {
61            None
62        }
63    }
64
65    /// Go forward in history
66    /// Returns the next location if available
67    pub fn forward(&mut self) -> Option<&FileLocation> {
68        if self.current < self.history.len() {
69            self.current += 1;
70            self.history.get(self.current - 1)
71        } else {
72            None
73        }
74    }
75
76    /// Get the current location
77    pub fn current(&self) -> Option<&FileLocation> {
78        if self.current > 0 && self.current <= self.history.len() {
79            self.history.get(self.current - 1)
80        } else {
81            None
82        }
83    }
84
85    /// Check if we can go back
86    pub fn can_go_back(&self) -> bool {
87        self.current > 1
88    }
89
90    /// Check if we can go forward
91    pub fn can_go_forward(&self) -> bool {
92        self.current < self.history.len()
93    }
94
95    /// Get the size of the history
96    pub fn len(&self) -> usize {
97        self.history.len()
98    }
99
100    /// Check if history is empty
101    pub fn is_empty(&self) -> bool {
102        self.history.is_empty()
103    }
104
105    /// Clear all history
106    pub fn clear(&mut self) {
107        self.history.clear();
108        self.current = 0;
109    }
110}
111
112impl Default for NavigationHistory {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_navigation_history_creation() {
124        let history = NavigationHistory::new();
125        assert!(history.is_empty());
126        assert_eq!(history.len(), 0);
127        assert!(!history.can_go_back());
128        assert!(!history.can_go_forward());
129    }
130
131    #[test]
132    fn test_push_location() {
133        let mut history = NavigationHistory::new();
134        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
135
136        history.push(loc1.clone());
137        assert_eq!(history.len(), 1);
138        assert_eq!(history.current(), Some(&loc1));
139        assert!(!history.can_go_back());
140        assert!(!history.can_go_forward());
141    }
142
143    #[test]
144    fn test_push_multiple_locations() {
145        let mut history = NavigationHistory::new();
146        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
147        let loc2 = FileLocation::new(PathBuf::from("file2.rs"), 20, 10);
148        let loc3 = FileLocation::new(PathBuf::from("file3.rs"), 30, 15);
149
150        history.push(loc1.clone());
151        history.push(loc2.clone());
152        history.push(loc3.clone());
153
154        assert_eq!(history.len(), 3);
155        assert_eq!(history.current(), Some(&loc3));
156        assert!(history.can_go_back());
157        assert!(!history.can_go_forward());
158    }
159
160    #[test]
161    fn test_duplicate_consecutive_locations() {
162        let mut history = NavigationHistory::new();
163        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
164
165        history.push(loc1.clone());
166        history.push(loc1.clone());
167        history.push(loc1.clone());
168
169        assert_eq!(history.len(), 1);
170    }
171
172    #[test]
173    fn test_back_navigation() {
174        let mut history = NavigationHistory::new();
175        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
176        let loc2 = FileLocation::new(PathBuf::from("file2.rs"), 20, 10);
177        let loc3 = FileLocation::new(PathBuf::from("file3.rs"), 30, 15);
178
179        history.push(loc1.clone());
180        history.push(loc2.clone());
181        history.push(loc3.clone());
182
183        // Go back to loc2
184        assert_eq!(history.back(), Some(&loc2));
185        assert_eq!(history.current(), Some(&loc2));
186        assert!(history.can_go_back());
187        assert!(history.can_go_forward());
188
189        // Go back to loc1
190        assert_eq!(history.back(), Some(&loc1));
191        assert_eq!(history.current(), Some(&loc1));
192        assert!(!history.can_go_back());
193        assert!(history.can_go_forward());
194
195        // Try to go back again (should return None)
196        assert_eq!(history.back(), None);
197    }
198
199    #[test]
200    fn test_forward_navigation() {
201        let mut history = NavigationHistory::new();
202        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
203        let loc2 = FileLocation::new(PathBuf::from("file2.rs"), 20, 10);
204        let loc3 = FileLocation::new(PathBuf::from("file3.rs"), 30, 15);
205
206        history.push(loc1.clone());
207        history.push(loc2.clone());
208        history.push(loc3.clone());
209
210        // Go back twice
211        history.back();
212        history.back();
213
214        // Go forward to loc2
215        assert_eq!(history.forward(), Some(&loc2));
216        assert_eq!(history.current(), Some(&loc2));
217
218        // Go forward to loc3
219        assert_eq!(history.forward(), Some(&loc3));
220        assert_eq!(history.current(), Some(&loc3));
221
222        // Try to go forward again (should return None)
223        assert_eq!(history.forward(), None);
224    }
225
226    #[test]
227    fn test_truncate_forward_history() {
228        let mut history = NavigationHistory::new();
229        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
230        let loc2 = FileLocation::new(PathBuf::from("file2.rs"), 20, 10);
231        let loc3 = FileLocation::new(PathBuf::from("file3.rs"), 30, 15);
232        let loc4 = FileLocation::new(PathBuf::from("file4.rs"), 40, 20);
233
234        history.push(loc1.clone());
235        history.push(loc2.clone());
236        history.push(loc3.clone());
237
238        // Go back twice (from loc3 to loc2 to loc1)
239        history.back();
240        history.back();
241
242        // Current should be loc1, and we should be able to push loc4
243        // This should truncate loc2 and loc3 from history
244        history.push(loc4.clone());
245
246        // History should now be: [loc1, loc4]
247        assert_eq!(history.len(), 2);
248        assert_eq!(history.current(), Some(&loc4));
249        assert!(!history.can_go_forward());
250    }
251
252    #[test]
253    fn test_clear_history() {
254        let mut history = NavigationHistory::new();
255        let loc1 = FileLocation::new(PathBuf::from("file1.rs"), 10, 5);
256        let loc2 = FileLocation::new(PathBuf::from("file2.rs"), 20, 10);
257
258        history.push(loc1);
259        history.push(loc2);
260
261        history.clear();
262
263        assert!(history.is_empty());
264        assert_eq!(history.len(), 0);
265        assert!(!history.can_go_back());
266        assert!(!history.can_go_forward());
267        assert_eq!(history.current(), None);
268    }
269}