1use reedline::{ValidationResult, Validator};
9
10pub struct RValidator;
11
12impl Validator for RValidator {
13 fn validate(&self, line: &str) -> ValidationResult {
14 if is_likely_incomplete(line) {
15 ValidationResult::Incomplete
16 } else {
17 ValidationResult::Complete
18 }
19 }
20}
21
22fn is_likely_incomplete(input: &str) -> bool {
23 let mut open_parens = 0i32;
24 let mut open_braces = 0i32;
25 let mut open_brackets = 0i32;
26 let mut in_string = false;
27 let mut string_char = ' ';
28 let mut prev_char = ' ';
29 let mut in_comment = false;
30 let mut in_raw_string = false;
31 let mut raw_close_bracket = ' ';
32 let mut raw_quote = ' ';
33
34 let chars: Vec<char> = input.chars().collect();
35 let len = chars.len();
36 let mut i = 0;
37
38 while i < len {
39 let c = chars[i];
40
41 if in_comment {
42 if c == '\n' {
43 in_comment = false;
44 }
45 prev_char = c;
46 i += 1;
47 continue;
48 }
49
50 if in_raw_string {
51 if c == raw_close_bracket && i + 1 < len && chars[i + 1] == raw_quote {
53 in_raw_string = false;
54 prev_char = raw_quote;
55 i += 2;
56 continue;
57 }
58 prev_char = c;
59 i += 1;
60 continue;
61 }
62
63 if in_string {
64 if c == string_char && prev_char != '\\' {
65 in_string = false;
66 }
67 prev_char = c;
68 i += 1;
69 continue;
70 }
71
72 if (c == 'r' || c == 'R') && i + 2 < len {
74 let quote = chars[i + 1];
75 if quote == '"' || quote == '\'' {
76 let open = chars[i + 2];
77 let close = match open {
78 '(' => Some(')'),
79 '[' => Some(']'),
80 '{' => Some('}'),
81 _ => None,
82 };
83 if let Some(close_ch) = close {
84 in_raw_string = true;
85 raw_close_bracket = close_ch;
86 raw_quote = quote;
87 prev_char = open;
88 i += 3; continue;
90 }
91 }
92 }
93
94 match c {
95 '#' => in_comment = true,
96 '"' | '\'' => {
97 in_string = true;
98 string_char = c;
99 }
100 '(' => open_parens += 1,
101 ')' => open_parens -= 1,
102 '{' => open_braces += 1,
103 '}' => open_braces -= 1,
104 '[' => open_brackets += 1,
105 ']' => open_brackets -= 1,
106 _ => {}
107 }
108 prev_char = c;
109 i += 1;
110 }
111
112 if open_parens > 0 || open_braces > 0 || open_brackets > 0 || in_string || in_raw_string {
113 return true;
114 }
115
116 let trimmed = input.trim_end();
118 let code_end = strip_trailing_comment(trimmed);
120 let trailing = code_end.trim_end();
121
122 if trailing.is_empty() {
123 return false;
124 }
125
126 if trailing.ends_with('+')
128 || trailing.ends_with('*')
129 || trailing.ends_with('/')
130 || trailing.ends_with(',')
131 || trailing.ends_with('|')
132 || trailing.ends_with('&')
133 || trailing.ends_with('~')
134 || trailing.ends_with("<-")
135 || trailing.ends_with("<<-")
136 || trailing.ends_with("|>")
137 || trailing.ends_with("||")
138 || trailing.ends_with("&&")
139 || trailing.ends_with("%>%")
140 {
141 return true;
142 }
143
144 if trailing.ends_with('-') && !trailing.ends_with("->") {
146 return true;
147 }
148
149 false
150}
151
152fn strip_trailing_comment(input: &str) -> &str {
155 let last_line = input
157 .rsplit('\n')
158 .find(|l| !l.trim().is_empty())
159 .unwrap_or(input);
160
161 let mut in_string = false;
163 let mut string_char = ' ';
164 let mut prev = ' ';
165 for (idx, c) in last_line.char_indices() {
166 if in_string {
167 if c == string_char && prev != '\\' {
168 in_string = false;
169 }
170 prev = c;
171 continue;
172 }
173 match c {
174 '"' | '\'' => {
175 in_string = true;
176 string_char = c;
177 }
178 '#' => return &last_line[..idx],
179 _ => {}
180 }
181 prev = c;
182 }
183 last_line
184}