1use std::ops::Range;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Language {
8 Rust,
10 Toml,
12 Plain,
14}
15
16impl Language {
17 #[must_use]
19 pub fn from_tag(tag: &str) -> Self {
20 match tag.trim().to_ascii_lowercase().as_str() {
21 "rust" | "rs" => Self::Rust,
22 "toml" => Self::Toml,
23 _ => Self::Plain,
24 }
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(crate) enum Token {
31 Keyword,
32 Type,
33 Function,
34 Macro,
35 String,
36 Number,
37 Comment,
38 Attribute,
39 Lifetime,
40 Punctuation,
41 Table,
42 Key,
43 Plain,
44}
45
46impl Token {
47 pub(crate) fn variant(self) -> &'static str {
48 match self {
49 Self::Keyword => "keyword",
50 Self::Type => "type",
51 Self::Function => "function",
52 Self::Macro => "macro",
53 Self::String => "string",
54 Self::Number => "number",
55 Self::Comment => "comment",
56 Self::Attribute => "attribute",
57 Self::Lifetime => "lifetime",
58 Self::Punctuation => "punctuation",
59 Self::Table => "table",
60 Self::Key => "key",
61 Self::Plain => "plain",
62 }
63 }
64}
65
66const RUST_KEYWORDS: [&str; 41] = [
67 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
68 "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
69 "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "yield", "gen", "try",
70];
71
72pub(crate) fn highlight(code: &str, language: Language) -> Vec<(Range<usize>, Token)> {
74 let tokens = match language {
75 Language::Rust => rust(code),
76 Language::Toml => toml(code),
77 Language::Plain => vec![(0..code.len(), Token::Plain)],
78 };
79 fill_gaps(code.len(), tokens)
80}
81
82fn fill_gaps(len: usize, tokens: Vec<(Range<usize>, Token)>) -> Vec<(Range<usize>, Token)> {
83 let mut out = Vec::with_capacity(tokens.len() * 2);
84 let mut position = 0;
85 for (range, token) in tokens {
86 if range.start > position {
87 out.push((position..range.start, Token::Plain));
88 }
89 if range.end > range.start {
90 position = range.end;
91 out.push((range, token));
92 }
93 }
94 if position < len {
95 out.push((position..len, Token::Plain));
96 }
97 out
98}
99
100struct Scanner<'a> {
101 text: &'a str,
102 pos: usize,
103}
104
105impl Scanner<'_> {
106 fn peek(&self) -> Option<char> {
107 self.text[self.pos..].chars().next()
108 }
109
110 fn peek_at(&self, n: usize) -> Option<char> {
111 self.text[self.pos..].chars().nth(n)
112 }
113
114 fn starts_with(&self, s: &str) -> bool {
115 self.text[self.pos..].starts_with(s)
116 }
117
118 fn bump(&mut self) -> Option<char> {
119 let c = self.peek()?;
120 self.pos += c.len_utf8();
121 Some(c)
122 }
123
124 fn eat_while(&mut self, keep: impl Fn(char) -> bool) {
125 while self.peek().is_some_and(&keep) {
126 self.bump();
127 }
128 }
129
130 fn skip_line(&mut self) {
131 self.eat_while(|c| c != '\n');
132 }
133
134 fn quoted(&mut self, quote: char) {
136 self.bump();
137 while let Some(c) = self.bump() {
138 if c == '\\' {
139 self.bump();
140 } else if c == quote {
141 break;
142 }
143 }
144 }
145}
146
147fn is_ident_start(c: char) -> bool {
148 c.is_alphabetic() || c == '_'
149}
150
151fn is_ident(c: char) -> bool {
152 c.is_alphanumeric() || c == '_'
153}
154
155fn rust(code: &str) -> Vec<(Range<usize>, Token)> {
156 let mut s = Scanner { text: code, pos: 0 };
157 let mut out = Vec::new();
158 let mut previous_word = String::new();
159 while let Some(c) = s.peek() {
160 let start = s.pos;
161 if s.starts_with("//") {
162 s.skip_line();
163 out.push((start..s.pos, Token::Comment));
164 } else if s.starts_with("/*") {
165 match code[s.pos + 2..].find("*/") {
166 Some(end) => s.pos += 2 + end + 2,
167 None => s.pos = code.len(),
168 }
169 out.push((start..s.pos, Token::Comment));
170 } else if s.starts_with("#[") || s.starts_with("#![") {
171 let mut depth = 0;
172 while let Some(c) = s.bump() {
173 match c {
174 '[' => depth += 1,
175 ']' => {
176 depth -= 1;
177 if depth == 0 {
178 break;
179 }
180 }
181 '\n' => break,
182 _ => {}
183 }
184 }
185 out.push((start..s.pos, Token::Attribute));
186 } else if c == '"' || ((c == 'b') && s.peek_at(1) == Some('"')) {
187 if c == 'b' {
188 s.bump();
189 }
190 s.quoted('"');
191 out.push((start..s.pos, Token::String));
192 } else if c == 'r' && (s.peek_at(1) == Some('"') || (s.peek_at(1) == Some('#') && s.peek_at(2) != Some('['))) {
193 s.bump();
194 let mut hashes = 0;
195 while s.peek() == Some('#') {
196 s.bump();
197 hashes += 1;
198 }
199 if s.peek() == Some('"') {
200 let closing = format!("\"{}", "#".repeat(hashes));
201 s.bump();
202 match code[s.pos..].find(&closing) {
203 Some(end) => s.pos += end + closing.len(),
204 None => s.pos = code.len(),
205 }
206 out.push((start..s.pos, Token::String));
207 } else {
208 s.eat_while(is_ident);
209 }
210 } else if c == '\'' {
211 let is_char = s.peek_at(1) == Some('\\') || s.peek_at(2) == Some('\'');
212 if is_char {
213 s.quoted('\'');
214 out.push((start..s.pos, Token::String));
215 } else {
216 s.bump();
217 s.eat_while(is_ident);
218 out.push((start..s.pos, Token::Lifetime));
219 }
220 } else if c.is_ascii_digit() {
221 s.eat_while(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.');
222 if code[start..s.pos].ends_with('.') {
223 s.pos -= 1;
224 }
225 out.push((start..s.pos, Token::Number));
226 } else if is_ident_start(c) {
227 s.eat_while(is_ident);
228 let word = &code[start..s.pos];
229 let token = if RUST_KEYWORDS.contains(&word) {
230 Token::Keyword
231 } else if s.peek() == Some('!') && s.peek_at(1) != Some('=') {
232 s.bump();
233 Token::Macro
234 } else if previous_word == "fn" || s.peek() == Some('(') || s.starts_with("::<") {
235 Token::Function
236 } else if word.chars().next().is_some_and(char::is_uppercase) {
237 Token::Type
238 } else {
239 Token::Plain
240 };
241 previous_word = word.to_owned();
242 out.push((start..s.pos, token));
243 continue;
244 } else if c.is_whitespace() {
245 s.bump();
246 continue;
247 } else {
248 s.bump();
249 out.push((start..s.pos, Token::Punctuation));
250 }
251 previous_word.clear();
252 }
253 out
254}
255
256fn toml(code: &str) -> Vec<(Range<usize>, Token)> {
257 let mut out = Vec::new();
258 let mut line_start = 0;
259 for line in code.split_inclusive('\n') {
260 let mut s = Scanner { text: code, pos: line_start };
261 let end = line_start + line.len();
262 s.eat_while(|c| c == ' ' || c == '\t');
263 if s.peek() == Some('[') {
264 let header_start = s.pos;
265 while s.pos < end && s.peek() != Some(']') {
266 if s.peek() == Some('"') {
267 s.quoted('"');
268 } else {
269 s.bump();
270 }
271 }
272 s.eat_while(|c| c == ']');
273 out.push((header_start..s.pos, Token::Table));
274 } else if s.peek().is_some_and(|c| is_ident(c) || c == '"' || c == '-') {
275 let key_start = s.pos;
276 while s.pos < end && !matches!(s.peek(), Some('=') | Some('\n') | Some('#')) {
277 if s.peek() == Some('"') {
278 s.quoted('"');
279 } else {
280 s.bump();
281 }
282 }
283 let key_end = key_start + code[key_start..s.pos].trim_end().len();
284 if s.peek() == Some('=') {
285 out.push((key_start..key_end, Token::Key));
286 } else {
287 s.pos = key_start;
288 }
289 }
290 while s.pos < end {
291 let start = s.pos;
292 match s.peek() {
293 Some('#') => {
294 s.skip_line();
295 out.push((start..s.pos, Token::Comment));
296 }
297 Some(q @ ('"' | '\'')) => {
298 s.quoted(q);
299 out.push((start..s.pos.min(end), Token::String));
300 s.pos = s.pos.min(end);
301 }
302 Some(c) if c.is_ascii_digit() || (c == '-' && s.peek_at(1).is_some_and(|d| d.is_ascii_digit())) => {
303 s.bump();
304 s.eat_while(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '-'));
305 out.push((start..s.pos, Token::Number));
306 }
307 Some(c) if is_ident_start(c) => {
308 s.eat_while(is_ident);
309 let token = match &code[start..s.pos] {
310 "true" | "false" => Token::Keyword,
311 _ => Token::Plain,
312 };
313 out.push((start..s.pos, token));
314 }
315 Some(c) if c.is_whitespace() => {
316 s.bump();
317 }
318 Some(_) => {
319 s.bump();
320 out.push((start..s.pos, Token::Punctuation));
321 }
322 None => break,
323 }
324 }
325 line_start = end;
326 }
327 out
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn kinds(code: &str, language: Language) -> Vec<(&str, Token)> {
335 highlight(code, language)
336 .into_iter()
337 .filter(|(r, t)| *t != Token::Plain || !code[r.clone()].trim().is_empty())
338 .map(|(r, t)| (&code[r], t))
339 .collect()
340 }
341
342 #[test]
343 fn rust_tokens() {
344 let code = "#[derive(Debug)]\nfn view(&self, ui: &mut View<'_, Msg>) {\n ui.add(Text::new(t!(\"hi\")) // note\n .width(3));\n}";
345 let tokens = kinds(code, Language::Rust);
346 assert!(tokens.contains(&("#[derive(Debug)]", Token::Attribute)));
347 assert!(tokens.contains(&("fn", Token::Keyword)));
348 assert!(tokens.contains(&("view", Token::Function)));
349 assert!(tokens.contains(&("View", Token::Type)));
350 assert!(tokens.contains(&("'_", Token::Lifetime)));
351 assert!(tokens.contains(&("t!", Token::Macro)));
352 assert!(tokens.contains(&("\"hi\"", Token::String)));
353 assert!(tokens.contains(&("// note", Token::Comment)));
354 assert!(tokens.contains(&("3", Token::Number)));
355 assert!(tokens.contains(&("new", Token::Function)));
356 }
357
358 #[test]
359 fn toml_tokens() {
360 let code = "[style.\"button:hover\"]\nbg = \"$raised\" # surface\npadding = [0, 2]\nslide = true\n";
361 let tokens = kinds(code, Language::Toml);
362 assert!(tokens.contains(&("[style.\"button:hover\"]", Token::Table)));
363 assert!(tokens.contains(&("bg", Token::Key)));
364 assert!(tokens.contains(&("\"$raised\"", Token::String)));
365 assert!(tokens.contains(&("# surface", Token::Comment)));
366 assert!(tokens.contains(&("2", Token::Number)));
367 assert!(tokens.contains(&("true", Token::Keyword)));
368 }
369
370 #[test]
371 fn ranges_cover_the_whole_text() {
372 let code = "let x = 'a'; r#\"raw\"#";
373 let ranges = highlight(code, Language::Rust);
374 let joined: String = ranges.iter().map(|(r, _)| &code[r.clone()]).collect();
375 assert_eq!(joined, code);
376 assert!(ranges.iter().any(|(r, t)| &code[r.clone()] == "r#\"raw\"#" && *t == Token::String));
377 }
378}