Skip to main content

perl_dap_breakpoint/
suggestion.rs

1//! Nearest valid line suggestion
2//!
3//! This module provides functionality to find the nearest valid line
4//! when a breakpoint is placed on an invalid location (comment, blank, etc.)
5
6use crate::validator::{AstBreakpointValidator, BreakpointValidator};
7
8/// Direction to search for a valid line
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SearchDirection {
11    /// Search forward (towards end of file)
12    Forward,
13    /// Search backward (towards start of file)
14    Backward,
15    /// Search in both directions, returning the nearest
16    Both,
17}
18
19/// Find the nearest valid line to the given line number
20///
21/// # Arguments
22///
23/// * `validator` - The breakpoint validator to use
24/// * `line` - The 1-based line number to start from
25/// * `direction` - The direction to search
26/// * `max_distance` - Maximum number of lines to search (None for unlimited)
27///
28/// # Returns
29///
30/// The nearest valid line number, or None if no valid line is found within the search range.
31pub fn find_nearest_valid_line(
32    validator: &AstBreakpointValidator,
33    line: i64,
34    direction: SearchDirection,
35    max_distance: Option<usize>,
36) -> Option<i64> {
37    let max_dist = max_distance.unwrap_or(usize::MAX);
38
39    match direction {
40        SearchDirection::Forward => find_forward(validator, line, max_dist),
41        SearchDirection::Backward => find_backward(validator, line, max_dist),
42        SearchDirection::Both => {
43            let forward = find_forward(validator, line, max_dist);
44            let backward = find_backward(validator, line, max_dist);
45
46            match (forward, backward) {
47                (Some(f), Some(b)) => {
48                    let f_dist = (f - line).unsigned_abs();
49                    let b_dist = (line - b).unsigned_abs();
50                    if f_dist <= b_dist { Some(f) } else { Some(b) }
51                }
52                (Some(f), None) => Some(f),
53                (None, Some(b)) => Some(b),
54                (None, None) => None,
55            }
56        }
57    }
58}
59
60fn find_forward(
61    validator: &AstBreakpointValidator,
62    start_line: i64,
63    max_distance: usize,
64) -> Option<i64> {
65    for offset in 1..=max_distance {
66        let line = start_line + offset as i64;
67        if validator.is_executable_line(line) {
68            return Some(line);
69        }
70        // Stop if we've gone past end of file
71        let result = validator.validate(line);
72        if result.reason == Some(crate::validator::ValidationReason::LineOutOfRange) {
73            break;
74        }
75    }
76    None
77}
78
79fn find_backward(
80    validator: &AstBreakpointValidator,
81    start_line: i64,
82    max_distance: usize,
83) -> Option<i64> {
84    for offset in 1..=max_distance {
85        let line = start_line - offset as i64;
86        if line < 1 {
87            break;
88        }
89        if validator.is_executable_line(line) {
90            return Some(line);
91        }
92    }
93    None
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use perl_tdd_support::must;
100
101    #[test]
102    fn test_find_nearest_forward() {
103        let source = "# comment\n# comment\nmy $x = 1;\n";
104        let validator = must(AstBreakpointValidator::new(source));
105
106        let result = find_nearest_valid_line(&validator, 1, SearchDirection::Forward, None);
107        assert_eq!(result, Some(3));
108    }
109
110    #[test]
111    fn test_find_nearest_backward() {
112        let source = "my $x = 1;\n# comment\n# comment\n";
113        let validator = must(AstBreakpointValidator::new(source));
114
115        let result = find_nearest_valid_line(&validator, 3, SearchDirection::Backward, None);
116        assert_eq!(result, Some(1));
117    }
118
119    #[test]
120    fn test_find_nearest_both_prefers_closer() {
121        let source = "my $x = 1;\n# comment\n# comment\n# comment\nmy $y = 2;\n";
122        let validator = must(AstBreakpointValidator::new(source));
123
124        // From line 2 (comment), line 1 is closer than line 5
125        let result = find_nearest_valid_line(&validator, 2, SearchDirection::Both, None);
126        assert_eq!(result, Some(1));
127
128        // From line 4 (comment), line 5 is closer than line 1
129        let result = find_nearest_valid_line(&validator, 4, SearchDirection::Both, None);
130        assert_eq!(result, Some(5));
131    }
132
133    #[test]
134    fn test_find_nearest_with_max_distance() {
135        let source = "# comment\n# comment\n# comment\nmy $x = 1;\n";
136        let validator = must(AstBreakpointValidator::new(source));
137
138        // With max distance 2, can't reach line 4
139        let result = find_nearest_valid_line(&validator, 1, SearchDirection::Forward, Some(2));
140        assert_eq!(result, None);
141
142        // With max distance 3, can reach line 4
143        let result = find_nearest_valid_line(&validator, 1, SearchDirection::Forward, Some(3));
144        assert_eq!(result, Some(4));
145    }
146
147    #[test]
148    fn test_find_nearest_no_valid_lines() {
149        let source = "# all comments\n# more comments\n";
150        let validator = must(AstBreakpointValidator::new(source));
151
152        let result = find_nearest_valid_line(&validator, 1, SearchDirection::Both, None);
153        assert_eq!(result, None);
154    }
155}