vtcode_commons/
lexical.rs1#[derive(Debug, Clone, Copy)]
21pub struct LanguageSyntax {
22 pub line_comments: &'static [&'static str],
24 pub block_comments: &'static [(&'static str, &'static str)],
26 pub string_delimiters: &'static [&'static str],
31 pub backslash_escapes: bool,
33}
34
35impl LanguageSyntax {
36 fn string_delimiter_at(&self, bytes: &[u8], index: usize) -> Option<&'static str> {
41 let rest = bytes.get(index..)?;
42 self.string_delimiters
43 .iter()
44 .filter(|delimiter| !delimiter.is_empty() && rest.starts_with(delimiter.as_bytes()))
45 .max_by_key(|delimiter| delimiter.len())
46 .copied()
47 }
48}
49
50impl Default for LanguageSyntax {
51 fn default() -> Self {
52 Self {
53 line_comments: &[],
54 block_comments: &[],
55 string_delimiters: &["\""],
56 backslash_escapes: true,
57 }
58 }
59}
60
61#[must_use]
66pub fn code_mask(source: &str, syntax: &LanguageSyntax) -> Vec<bool> {
67 let bytes = source.as_bytes();
68 let mut mask = vec![true; bytes.len()];
69 let mut index = 0usize;
70
71 while index < bytes.len() {
72 if let Some(end) = match_line_comment(bytes, index, syntax) {
73 mark(&mut mask, index, end);
74 index = end;
75 continue;
76 }
77 if let Some(end) = match_block_comment(bytes, index, syntax) {
78 mark(&mut mask, index, end);
79 index = end;
80 continue;
81 }
82 if let Some(end) = match_string(bytes, index, syntax) {
83 mark(&mut mask, index, end);
84 index = end;
85 continue;
86 }
87 index += 1;
88 }
89
90 mask
91}
92
93fn mark(mask: &mut [bool], start: usize, end: usize) {
95 let Some(range) = mask.get_mut(start..end.min(mask.len())) else {
96 return;
97 };
98 for slot in range {
99 *slot = false;
100 }
101}
102
103fn match_line_comment(bytes: &[u8], index: usize, syntax: &LanguageSyntax) -> Option<usize> {
105 let rest = bytes.get(index..)?;
106 for prefix in syntax.line_comments {
107 if prefix.is_empty() {
110 continue;
111 }
112 if rest.starts_with(prefix.as_bytes()) {
113 return Some(
114 rest.iter()
115 .position(|byte| *byte == b'\n')
116 .map_or(bytes.len(), |offset| index + offset),
117 );
118 }
119 }
120 None
121}
122
123fn match_block_comment(bytes: &[u8], index: usize, syntax: &LanguageSyntax) -> Option<usize> {
125 let rest = bytes.get(index..)?;
126 for (open, close) in syntax.block_comments {
127 if open.is_empty() || close.is_empty() {
130 continue;
131 }
132 if !rest.starts_with(open.as_bytes()) {
133 continue;
134 }
135 let body_start = index + open.len();
136 return Some(
137 match bytes
138 .get(body_start..)
139 .unwrap_or_default()
140 .windows(close.len())
141 .position(|window| window == close.as_bytes())
142 {
143 Some(offset) => body_start + offset + close.len(),
144 None => bytes.len(),
145 },
146 );
147 }
148 None
149}
150
151fn match_string(bytes: &[u8], index: usize, syntax: &LanguageSyntax) -> Option<usize> {
153 let delimiter = syntax.string_delimiter_at(bytes, index)?;
154 let body_start = index + delimiter.len();
155 let mut cursor = body_start;
156
157 while let Some(byte) = bytes.get(cursor) {
158 if syntax.backslash_escapes && *byte == b'\\' {
159 cursor = (cursor + 2).min(bytes.len());
160 continue;
161 }
162 if bytes.get(cursor..).unwrap_or_default().starts_with(delimiter.as_bytes()) {
163 return Some(cursor + delimiter.len());
164 }
165 if delimiter.len() == 1 && *byte == b'\n' {
168 return Some(cursor);
169 }
170 cursor += 1;
171 }
172
173 Some(bytes.len())
174}
175
176#[cfg(test)]
177mod tests {
178 use super::{LanguageSyntax, code_mask};
179
180 const RUST: LanguageSyntax = LanguageSyntax {
181 line_comments: &["//"],
182 block_comments: &[("/*", "*/")],
183 string_delimiters: &["\""],
184 backslash_escapes: true,
185 };
186
187 fn code_at(source: &str, needle: &str) -> bool {
188 let mask = code_mask(source, &RUST);
189 let start = source.find(needle).expect("needle present");
190 mask[start]
191 }
192
193 #[test]
194 fn identifier_occurrences_are_code() {
195 assert!(code_at("let Widget = 1;", "Widget"));
196 }
197
198 #[test]
199 fn string_contents_are_not_code() {
200 assert!(!code_at("let label = \"Widget\";", "Widget"));
201 }
202
203 #[test]
204 fn comment_contents_are_not_code() {
205 assert!(!code_at("// Widget comment", "Widget"));
206 assert!(!code_at("/* Widget */", "Widget"));
207 }
208
209 #[test]
210 fn escaped_quotes_do_not_end_the_string_early() {
211 let source = "let label = \"a\\\"Widget\"; let x = Widget;";
212 assert!(!code_at(source, "Widget\""));
213 assert!(code_at(source, "Widget;"));
214 }
215
216 #[test]
217 fn block_comments_span_lines() {
218 let source = "/* start\nWidget\nend */ let y = Widget;";
219 let mask = code_mask(source, &RUST);
220 let widget = source.find("Widget").expect("needle present");
221 assert!(!mask[widget], "the commented occurrence is not code");
222 let last = source.rfind("Widget").expect("second occurrence");
223 assert!(mask[last], "the code occurrence is code");
224 }
225
226 #[test]
227 fn languages_without_comments_treat_everything_as_code() {
228 let syntax = LanguageSyntax { line_comments: &[], ..LanguageSyntax::default() };
229 let source = "// Widget";
230 assert!(code_mask(source, &syntax)[source.find("Widget").expect("needle present")]);
231 }
232
233 #[test]
234 fn python_style_comments_are_recognised() {
235 let syntax = LanguageSyntax {
236 line_comments: &["#"],
237 block_comments: &[],
238 string_delimiters: &["\"", "'"],
239 backslash_escapes: true,
240 };
241 let source = "# Widget\nx = 'Widget'\ny = Widget";
242 let mask = code_mask(source, &syntax);
243 let comment = source.find("Widget").expect("needle present");
244 let string = source.find("'Widget'").expect("string present") + 1;
245 let code = source.rfind("Widget").expect("needle present");
246 assert!(!mask[comment], "a comment mention is not code");
247 assert!(!mask[string], "a string literal is not code");
248 assert!(mask[code], "a bare identifier is code");
249 }
250}