use std::ops::Range;
use gpui::Hsla;
use crate::theme::{ColorName, Theme};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TokenKind {
Keyword,
Ident,
Number,
StringLit,
Comment,
Punct,
Type,
Function,
}
impl TokenKind {
pub const ALL: [TokenKind; 8] = [
TokenKind::Keyword,
TokenKind::Ident,
TokenKind::Number,
TokenKind::StringLit,
TokenKind::Comment,
TokenKind::Punct,
TokenKind::Type,
TokenKind::Function,
];
pub fn index(self) -> usize {
self as usize
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct LineState {
block_depth: u32,
}
pub trait Highlighter {
fn line(&self, text: &str, state: &mut LineState) -> Vec<(Range<usize>, TokenKind)>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Language {
#[default]
None,
Rust,
Sql,
Json,
}
impl Highlighter for Language {
fn line(&self, text: &str, state: &mut LineState) -> Vec<(Range<usize>, TokenKind)> {
match self {
Language::None => Vec::new(),
Language::Rust => tokenize(&RUST, text, state),
Language::Sql => tokenize(&SQL, text, state),
Language::Json => tokenize(&JSON, text, state),
}
}
}
pub fn token_color(kind: TokenKind, t: &Theme) -> Hsla {
let shade = if t.scheme.is_dark() { 4 } else { 7 };
match kind {
TokenKind::Keyword => t.color(ColorName::Violet, shade).hsla(),
TokenKind::StringLit => t.color(ColorName::Green, shade).hsla(),
TokenKind::Number => t.color(ColorName::Orange, shade).hsla(),
TokenKind::Type => t.color(ColorName::Teal, shade).hsla(),
TokenKind::Function => t.color(ColorName::Blue, shade).hsla(),
TokenKind::Comment => t.dimmed().hsla(),
TokenKind::Ident | TokenKind::Punct => t.text().hsla(),
}
}
#[derive(Clone, Copy)]
enum Escape {
Backslash,
Doubled,
}
struct Syntax {
line_comment: Option<&'static str>,
block_comment: Option<(&'static str, &'static str)>,
nested_blocks: bool,
strings: &'static [(char, Escape)],
keywords: &'static [&'static str],
types: &'static [&'static str],
case_insensitive: bool,
uppercase_types: bool,
}
const RUST: Syntax = Syntax {
line_comment: Some("//"),
block_comment: Some(("/*", "*/")),
nested_blocks: true,
strings: &[('"', Escape::Backslash)],
keywords: &[
"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", "union", "unsafe", "use", "where", "while",
],
types: &[],
case_insensitive: false,
uppercase_types: true,
};
const SQL: Syntax = Syntax {
line_comment: Some("--"),
block_comment: Some(("/*", "*/")),
nested_blocks: false,
strings: &[('\'', Escape::Doubled)],
keywords: &[
"add",
"all",
"alter",
"and",
"as",
"asc",
"begin",
"between",
"by",
"case",
"cast",
"check",
"column",
"commit",
"constraint",
"create",
"cross",
"default",
"delete",
"desc",
"distinct",
"drop",
"else",
"end",
"exists",
"false",
"foreign",
"from",
"full",
"group",
"having",
"if",
"in",
"index",
"inner",
"insert",
"into",
"is",
"join",
"key",
"left",
"like",
"limit",
"not",
"null",
"offset",
"on",
"or",
"order",
"outer",
"primary",
"references",
"replace",
"returning",
"right",
"rollback",
"select",
"set",
"table",
"then",
"transaction",
"true",
"union",
"unique",
"update",
"values",
"view",
"when",
"where",
"with",
],
types: &[
"bigint",
"blob",
"bool",
"boolean",
"bytea",
"char",
"date",
"decimal",
"double",
"float",
"int",
"integer",
"interval",
"json",
"jsonb",
"numeric",
"real",
"serial",
"smallint",
"text",
"time",
"timestamp",
"timestamptz",
"uuid",
"varchar",
],
case_insensitive: true,
uppercase_types: false,
};
const JSON: Syntax = Syntax {
line_comment: None,
block_comment: None,
nested_blocks: false,
strings: &[('"', Escape::Backslash)],
keywords: &["false", "null", "true"],
types: &[],
case_insensitive: false,
uppercase_types: false,
};
fn tokenize(syntax: &Syntax, text: &str, state: &mut LineState) -> Vec<(Range<usize>, TokenKind)> {
let chars: Vec<(usize, char)> = text.char_indices().collect();
let n = chars.len();
let byte_at = |i: usize| chars.get(i).map(|&(b, _)| b).unwrap_or(text.len());
let mut out: Vec<(Range<usize>, TokenKind)> = Vec::new();
let mut i = 0;
if state.block_depth > 0 {
match syntax.block_comment {
Some((open, close)) => {
let (end, depth) = scan_block(
&chars,
0,
open,
close,
syntax.nested_blocks,
state.block_depth,
);
state.block_depth = depth;
if byte_at(end) > 0 {
out.push((0..byte_at(end), TokenKind::Comment));
}
i = end;
}
None => state.block_depth = 0,
}
}
while i < n {
let (b, c) = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
if let Some(lc) = syntax.line_comment {
if starts_with_at(&chars, i, lc) {
out.push((b..text.len(), TokenKind::Comment));
break;
}
}
if let Some((open, close)) = syntax.block_comment {
if starts_with_at(&chars, i, open) {
let after_open = i + open.chars().count();
let (end, depth) =
scan_block(&chars, after_open, open, close, syntax.nested_blocks, 1);
state.block_depth = depth;
out.push((b..byte_at(end), TokenKind::Comment));
i = end;
continue;
}
}
if let Some(&(_, esc)) = syntax.strings.iter().find(|&&(q, _)| q == c) {
let end = scan_string(&chars, i + 1, c, esc);
out.push((b..byte_at(end), TokenKind::StringLit));
i = end;
continue;
}
if c.is_ascii_digit() {
let end = scan_number(&chars, i);
out.push((b..byte_at(end), TokenKind::Number));
i = end;
continue;
}
if c.is_alphabetic() || c == '_' {
let end = scan_ident(&chars, i);
let word = &text[b..byte_at(end)];
out.push((b..byte_at(end), classify_word(syntax, word, &chars, end)));
i = end;
continue;
}
out.push((b..byte_at(i + 1), TokenKind::Punct));
i += 1;
}
coalesce(out)
}
fn starts_with_at(chars: &[(usize, char)], i: usize, pat: &str) -> bool {
let mut j = i;
for p in pat.chars() {
match chars.get(j) {
Some(&(_, c)) if c == p => j += 1,
_ => return false,
}
}
true
}
fn scan_block(
chars: &[(usize, char)],
mut i: usize,
open: &str,
close: &str,
nested: bool,
mut depth: u32,
) -> (usize, u32) {
let n = chars.len();
while i < n {
if nested && starts_with_at(chars, i, open) {
depth += 1;
i += open.chars().count();
} else if starts_with_at(chars, i, close) {
depth -= 1;
i += close.chars().count();
if depth == 0 {
return (i, 0);
}
} else {
i += 1;
}
}
(n, depth)
}
fn scan_string(chars: &[(usize, char)], mut i: usize, quote: char, esc: Escape) -> usize {
let n = chars.len();
while i < n {
let c = chars[i].1;
match esc {
Escape::Backslash if c == '\\' => {
i += 2;
continue;
}
Escape::Doubled if c == quote => {
if i + 1 < n && chars[i + 1].1 == quote {
i += 2;
continue;
}
return i + 1;
}
_ if c == quote => return i + 1,
_ => i += 1,
}
}
n
}
fn scan_number(chars: &[(usize, char)], mut i: usize) -> usize {
let n = chars.len();
if chars[i].1 == '0' && i + 1 < n && matches!(chars[i + 1].1, 'x' | 'X' | 'b' | 'B' | 'o' | 'O')
{
i += 2;
while i < n && (chars[i].1.is_ascii_alphanumeric() || chars[i].1 == '_') {
i += 1;
}
return i;
}
while i < n && (chars[i].1.is_ascii_digit() || chars[i].1 == '_') {
i += 1;
}
if i + 1 < n && chars[i].1 == '.' && chars[i + 1].1.is_ascii_digit() {
i += 1;
while i < n && (chars[i].1.is_ascii_digit() || chars[i].1 == '_') {
i += 1;
}
}
if i < n && matches!(chars[i].1, 'e' | 'E') {
let mut j = i + 1;
if j < n && matches!(chars[j].1, '+' | '-') {
j += 1;
}
if j < n && chars[j].1.is_ascii_digit() {
i = j;
while i < n && chars[i].1.is_ascii_digit() {
i += 1;
}
}
}
while i < n && (chars[i].1.is_ascii_alphanumeric() || chars[i].1 == '_') {
i += 1;
}
i
}
fn scan_ident(chars: &[(usize, char)], mut i: usize) -> usize {
let n = chars.len();
while i < n && (chars[i].1.is_alphanumeric() || chars[i].1 == '_') {
i += 1;
}
i
}
fn classify_word(syntax: &Syntax, word: &str, chars: &[(usize, char)], end: usize) -> TokenKind {
let in_set = |set: &[&str]| {
if syntax.case_insensitive {
set.iter().any(|k| k.eq_ignore_ascii_case(word))
} else {
set.contains(&word)
}
};
if in_set(syntax.keywords) {
return TokenKind::Keyword;
}
if in_set(syntax.types) {
return TokenKind::Type;
}
if syntax.uppercase_types && word.chars().next().is_some_and(char::is_uppercase) {
return TokenKind::Type;
}
match chars.get(end).map(|&(_, c)| c) {
Some('(') => TokenKind::Function,
Some('!') if matches!(chars.get(end + 1), Some(&(_, '('))) => TokenKind::Function,
_ => TokenKind::Ident,
}
}
fn coalesce(tokens: Vec<(Range<usize>, TokenKind)>) -> Vec<(Range<usize>, TokenKind)> {
let mut out: Vec<(Range<usize>, TokenKind)> = Vec::new();
for (range, kind) in tokens {
if let Some((last, last_kind)) = out.last_mut() {
if *last_kind == kind && last.end == range.start {
last.end = range.end;
continue;
}
}
out.push((range, kind));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(lang: Language, line: &str) -> Vec<(String, TokenKind)> {
let mut state = LineState::default();
lang.line(line, &mut state)
.into_iter()
.map(|(r, k)| (line[r].to_string(), k))
.collect()
}
fn kind_of(lang: Language, line: &str, word: &str) -> TokenKind {
kinds(lang, line)
.into_iter()
.find(|(w, _)| w == word)
.map(|(_, k)| k)
.unwrap_or_else(|| panic!("token {word:?} not found in {line:?}"))
}
#[test]
fn none_language_emits_nothing() {
assert!(kinds(Language::None, "let x = 1;").is_empty());
}
#[test]
fn ranges_are_ascending_and_in_bounds() {
let line = "let s = \"héllo\"; // café";
let mut state = LineState::default();
let tokens = Language::Rust.line(line, &mut state);
let mut at = 0;
for (range, _) in &tokens {
assert!(range.start >= at, "overlapping range");
assert!(range.end <= line.len());
assert!(line.is_char_boundary(range.start));
assert!(line.is_char_boundary(range.end));
at = range.end;
}
}
#[test]
fn rust_basics() {
assert_eq!(
kind_of(Language::Rust, "let x = 1;", "let"),
TokenKind::Keyword
);
assert_eq!(kind_of(Language::Rust, "let x = 1;", "x"), TokenKind::Ident);
assert_eq!(
kind_of(Language::Rust, "let x = 10.5e3;", "10.5e3"),
TokenKind::Number
);
assert_eq!(
kind_of(Language::Rust, "let n = 0xff_u8;", "0xff_u8"),
TokenKind::Number
);
assert_eq!(
kind_of(
Language::Rust,
r#"let s = "hi \" there";"#,
r#""hi \" there""#
),
TokenKind::StringLit
);
assert_eq!(
kind_of(Language::Rust, "let v: Vec<u8>;", "Vec"),
TokenKind::Type
);
assert_eq!(
kind_of(Language::Rust, "foo(1)", "foo"),
TokenKind::Function
);
assert_eq!(
kind_of(Language::Rust, "println!(\"x\")", "println"),
TokenKind::Function
);
assert_eq!(
kind_of(Language::Rust, "a + b // sum", "// sum"),
TokenKind::Comment
);
}
#[test]
fn rust_block_comment_carries_state() {
let mut state = LineState::default();
let t1 = Language::Rust.line("start /* open", &mut state);
assert_eq!(
t1.last()
.map(|(r, k)| ("start /* open"[r.clone()].to_string(), *k)),
Some(("/* open".to_string(), TokenKind::Comment))
);
let t2 = Language::Rust.line("all comment", &mut state);
assert_eq!(t2, vec![(0..11, TokenKind::Comment)]);
let t3 = Language::Rust.line("done */ let x", &mut state);
assert_eq!(t3[0], (0..7, TokenKind::Comment));
assert_eq!("done */ let x"[t3[1].clone().0].to_string(), "let");
assert_eq!(state, LineState::default());
}
#[test]
fn rust_block_comments_nest() {
let mut state = LineState::default();
Language::Rust.line("/* a /* b */ still", &mut state);
assert_ne!(state, LineState::default());
let t = Language::Rust.line("c */ code", &mut state);
assert_eq!(t[0], (0..4, TokenKind::Comment));
assert_eq!(state, LineState::default());
}
#[test]
fn sql_keywords_are_case_insensitive() {
assert_eq!(
kind_of(Language::Sql, "SELECT * FROM users;", "SELECT"),
TokenKind::Keyword
);
assert_eq!(
kind_of(Language::Sql, "select * from users;", "select"),
TokenKind::Keyword
);
assert_eq!(
kind_of(Language::Sql, "id INT PRIMARY KEY", "INT"),
TokenKind::Type
);
assert_eq!(
kind_of(Language::Sql, "count(*)", "count"),
TokenKind::Function
);
}
#[test]
fn sql_comments_and_strings() {
assert_eq!(
kind_of(Language::Sql, "x -- note", "-- note"),
TokenKind::Comment
);
assert_eq!(
kind_of(Language::Sql, "name = 'it''s'", "'it''s'"),
TokenKind::StringLit
);
let mut state = LineState::default();
Language::Sql.line("/* multi", &mut state);
let t = Language::Sql.line("line */ SELECT", &mut state);
assert_eq!(t[0], (0..7, TokenKind::Comment));
assert_eq!(state, LineState::default());
}
#[test]
fn sql_blocks_do_not_nest() {
let mut state = LineState::default();
Language::Sql.line("/* a /* b */ tail", &mut state);
assert_eq!(state, LineState::default());
}
#[test]
fn json_tokens() {
let line = r#"{"key": [1.5, true, null, "value"]}"#;
assert_eq!(
kind_of(Language::Json, line, r#""key""#),
TokenKind::StringLit
);
assert_eq!(kind_of(Language::Json, line, "1.5"), TokenKind::Number);
assert_eq!(kind_of(Language::Json, line, "true"), TokenKind::Keyword);
assert_eq!(kind_of(Language::Json, line, "null"), TokenKind::Keyword);
}
#[test]
fn multibyte_idents_and_strings() {
let line = "let café = \"日本語\"; // été";
assert_eq!(kind_of(Language::Rust, line, "café"), TokenKind::Ident);
assert_eq!(
kind_of(Language::Rust, line, "\"日本語\""),
TokenKind::StringLit
);
assert_eq!(kind_of(Language::Rust, line, "// été"), TokenKind::Comment);
}
#[test]
fn unterminated_string_stops_at_line_end() {
let line = "let s = \"open";
assert_eq!(
kind_of(Language::Rust, line, "\"open"),
TokenKind::StringLit
);
let mut state = LineState::default();
Language::Rust.line(line, &mut state);
assert_eq!(state, LineState::default());
}
#[test]
fn punctuation_coalesces() {
let tokens = kinds(Language::Rust, "a->b");
assert_eq!(
tokens,
vec![
("a".to_string(), TokenKind::Ident),
("->".to_string(), TokenKind::Punct),
("b".to_string(), TokenKind::Ident),
]
);
}
#[test]
fn empty_line_inside_block_comment() {
let mut state = LineState::default();
Language::Rust.line("/* open", &mut state);
let t = Language::Rust.line("", &mut state);
assert!(t.is_empty());
assert_ne!(state, LineState::default());
}
}