#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Text,
Keyword,
Type,
Str,
Number,
Comment,
Attribute,
Punct,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub kind: Kind,
}
const KEYWORDS: &[&str] = &[
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
"ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while",
];
const PRIMITIVES: &[&str] = &[
"bool", "char", "str", "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64",
"i128", "isize", "f32", "f64",
];
fn is_ident_start(c: char) -> bool {
c == '_' || c.is_ascii_alphabetic()
}
fn is_ident_continue(c: char) -> bool {
c == '_' || c.is_ascii_alphanumeric()
}
#[must_use]
pub fn highlight(src: &str) -> Vec<Span> {
let b = src.as_bytes();
let n = b.len();
let mut out: Vec<Span> = Vec::new();
let mut i = 0;
let push = |out: &mut Vec<Span>, s: usize, e: usize, k: Kind| {
if e > s {
out.push(Span { start: s, end: e, kind: k });
}
};
while i < n {
let c = b[i] as char;
if c == '/' && i + 1 < n && b[i + 1] == b'/' {
let s = i;
while i < n && b[i] != b'\n' {
i += 1;
}
push(&mut out, s, i, Kind::Comment);
continue;
}
if c == '/' && i + 1 < n && b[i + 1] == b'*' {
let s = i;
i += 2;
while i + 1 < n && !(b[i] == b'*' && b[i + 1] == b'/') {
i += 1;
}
i = (i + 2).min(n);
push(&mut out, s, i, Kind::Comment);
continue;
}
if c == '"' {
let s = i;
i += 1;
while i < n {
if b[i] == b'\\' {
i += 2;
continue;
}
if b[i] == b'"' {
i += 1;
break;
}
i += 1;
}
push(&mut out, s, i.min(n), Kind::Str);
continue;
}
if c == '\'' && i + 1 < n {
let s = i;
let mut j = i + 1;
if b[j] == b'\\' {
j += 2;
} else {
j += 1;
}
if j < n && b[j] == b'\'' {
j += 1;
push(&mut out, s, j, Kind::Str);
i = j;
continue;
}
}
if c == '#' && i + 1 < n && (b[i + 1] == b'[' || b[i + 1] == b'!') {
let s = i;
while i < n && b[i] != b'[' {
i += 1;
}
let mut depth = 0i32;
while i < n {
if b[i] == b'[' {
depth += 1;
} else if b[i] == b']' {
depth -= 1;
if depth == 0 {
i += 1;
break;
}
}
i += 1;
}
push(&mut out, s, i.min(n), Kind::Attribute);
continue;
}
if c.is_ascii_digit() {
let s = i;
while i < n {
let d = b[i] as char;
if d.is_ascii_alphanumeric() || d == '_' || d == '.' {
i += 1;
} else {
break;
}
}
push(&mut out, s, i, Kind::Number);
continue;
}
if is_ident_start(c) {
let s = i;
while i < n && is_ident_continue(b[i] as char) {
i += 1;
}
let word = &src[s..i];
let kind = if KEYWORDS.contains(&word) {
Kind::Keyword
} else if PRIMITIVES.contains(&word) || word.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
Kind::Type
} else {
Kind::Text
};
push(&mut out, s, i, kind);
continue;
}
if c.is_ascii_whitespace() {
let s = i;
while i < n && (b[i] as char).is_ascii_whitespace() {
i += 1;
}
push(&mut out, s, i, Kind::Text);
continue;
}
push(&mut out, i, i + 1, Kind::Punct);
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds_of<'a>(src: &'a str, spans: &[Span]) -> Vec<(&'a str, Kind)> {
spans.iter().map(|s| (&src[s.start..s.end], s.kind)).collect()
}
#[test]
fn spans_tile_the_whole_source() {
let src = "fn main() { let x = 42; }";
let spans = highlight(src);
assert!(!spans.is_empty());
assert_eq!(spans.first().unwrap().start, 0);
assert_eq!(spans.last().unwrap().end, src.len());
for w in spans.windows(2) {
assert_eq!(w[0].end, w[1].start, "contiguous, no gap/overlap");
}
}
#[test]
fn classifies_the_main_token_kinds() {
let src = "#[derive(Clone)]\nfn f() -> u32 { let s = \"hi\"; /* c */ return 7; } // tail";
let got = kinds_of(src, &highlight(src));
let find = |needle: &str, k: Kind| got.iter().any(|(t, kk)| *t == needle && *kk == k);
assert!(find("fn", Kind::Keyword), "fn is a keyword");
assert!(find("let", Kind::Keyword) && find("return", Kind::Keyword));
assert!(find("u32", Kind::Type), "u32 is a primitive type");
assert!(find("Clone", Kind::Type) || got.iter().any(|(t, k)| t.contains("Clone") && *k == Kind::Attribute));
assert!(got.iter().any(|(t, k)| *t == "\"hi\"" && *k == Kind::Str), "string incl quotes");
assert!(got.iter().any(|(t, k)| t.contains("/* c */") && *k == Kind::Comment));
assert!(got.iter().any(|(t, k)| t.contains("// tail") && *k == Kind::Comment));
assert!(got.iter().any(|(t, k)| *t == "7" && *k == Kind::Number));
assert!(got.iter().any(|(t, k)| t.starts_with("#[derive") && *k == Kind::Attribute));
}
#[test]
fn deterministic() {
let src = "struct S { a: Vec<u8> }";
assert_eq!(highlight(src), highlight(src));
}
#[test]
fn empty_is_empty() {
assert!(highlight("").is_empty());
}
}