use std::sync::OnceLock;
use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxReference};
use super::syntax::syntax_set;
use super::{TokenKind, TokenSpan};
pub(crate) struct LineTokenizer {
parse: ParseState,
stack: ScopeStack,
}
impl LineTokenizer {
pub(crate) fn new(syntax: &SyntaxReference) -> Self {
Self {
parse: ParseState::new(syntax),
stack: ScopeStack::new(),
}
}
pub(crate) fn next_line(&mut self, raw: &str) -> Vec<TokenSpan> {
let with_nl = format!("{raw}\n");
let ops = match self.parse.parse_line(&with_nl, syntax_set()) {
Ok(ops) => ops,
Err(_) => return Vec::new(),
};
let mut spans: Vec<TokenSpan> = Vec::new();
let mut last = 0usize;
for (offset, op) in ops {
push_span(
&mut spans,
last,
offset.min(raw.len()),
classify(&self.stack),
);
let _ = self.stack.apply(&op); last = offset;
}
push_span(
&mut spans,
last.min(raw.len()),
raw.len(),
classify(&self.stack),
);
coalesce_drop_plain(spans)
}
}
fn classifier() -> &'static [(Scope, TokenKind)] {
static TABLE: OnceLock<Vec<(Scope, TokenKind)>> = OnceLock::new();
TABLE.get_or_init(|| {
let s = |name: &str| Scope::new(name).expect("static classifier scope parses");
vec![
(s("comment"), TokenKind::Comment),
(s("string"), TokenKind::String),
(s("constant.numeric"), TokenKind::Number),
(s("constant"), TokenKind::Constant),
(s("keyword.operator"), TokenKind::Operator),
(s("keyword"), TokenKind::Keyword),
(s("storage"), TokenKind::Keyword),
(s("entity.name.function"), TokenKind::Function),
(s("support.function"), TokenKind::Function),
(s("entity.name.type"), TokenKind::Type),
(s("support.type"), TokenKind::Type),
(s("entity.name.tag"), TokenKind::Type),
(s("variable"), TokenKind::Variable),
(s("entity.other.attribute-name"), TokenKind::Variable),
(s("punctuation"), TokenKind::Punctuation),
]
})
}
fn classify(stack: &ScopeStack) -> TokenKind {
for scope in stack.as_slice().iter().rev() {
for (cls, kind) in classifier() {
if cls.is_prefix_of(*scope) {
return *kind;
}
}
}
TokenKind::Plain
}
fn push_span(spans: &mut Vec<TokenSpan>, start: usize, end: usize, kind: TokenKind) {
if start >= end {
return;
}
spans.push(TokenSpan { start, end, kind });
}
fn coalesce_drop_plain(spans: Vec<TokenSpan>) -> Vec<TokenSpan> {
let mut out: Vec<TokenSpan> = Vec::with_capacity(spans.len());
for span in spans {
if span.kind == TokenKind::Plain {
continue;
}
if let Some(last) = out.last_mut()
&& last.kind == span.kind
&& last.end == span.start
{
last.end = span.end;
continue;
}
out.push(span);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::highlight::TokenKind;
use crate::highlight::syntax::syntax_for_paths;
#[test]
fn tokenizes_a_rust_line_into_kinds_at_byte_ranges() {
let syntax = syntax_for_paths(Some("a.rs"), None).unwrap();
let mut tk = LineTokenizer::new(syntax);
let line = "let x = 1; // x";
let spans = tk.next_line(line);
assert!(
spans
.iter()
.any(|s| s.kind == TokenKind::Keyword && s.start == 0 && s.end == 3)
);
assert!(spans.iter().any(|s| s.kind == TokenKind::Number));
assert!(spans.iter().any(|s| s.kind == TokenKind::Comment));
assert!(
spans
.iter()
.all(|s| s.kind != TokenKind::Plain && s.end <= line.len())
);
assert!(spans.windows(2).all(|w| w[0].end <= w[1].start));
}
#[test]
fn tokenizes_a_typescript_line() {
let syntax = syntax_for_paths(Some("app.ts"), None).unwrap();
let mut tk = LineTokenizer::new(syntax);
let spans = tk.next_line("const x: number = 1;");
assert!(spans.iter().any(|s| s.kind == TokenKind::Keyword)); assert!(spans.iter().any(|s| s.kind == TokenKind::Number)); }
#[test]
fn carries_state_across_lines_for_block_comments() {
let syntax = syntax_for_paths(Some("a.rs"), None).unwrap();
let mut tk = LineTokenizer::new(syntax);
let _ = tk.next_line("/* open");
let spans = tk.next_line("still comment */"); assert!(spans.iter().any(|s| s.kind == TokenKind::Comment));
}
#[test]
fn classify_picks_most_specific_then_table_order() {
use syntect::parsing::{Scope, ScopeStack};
let stack = |scopes: &[&str]| {
let mut s = ScopeStack::new();
for name in scopes {
s.push(Scope::new(name).unwrap());
}
s
};
assert_eq!(
classify(&stack(&["source.rust", "constant.numeric.integer.rust"])),
TokenKind::Number
);
assert_eq!(
classify(&stack(&["source.rust", "keyword.operator.assignment.rust"])),
TokenKind::Operator
);
assert_eq!(
classify(&stack(&["source.rust", "entity.name.function.rust"])),
TokenKind::Function
);
assert_eq!(
classify(&stack(&["source.rust", "meta.block.rust"])),
TokenKind::Plain
);
}
#[test]
fn coalesce_merges_adjacent_same_kind_and_drops_plain() {
let merged = coalesce_drop_plain(vec![
TokenSpan {
start: 0,
end: 3,
kind: TokenKind::Keyword,
},
TokenSpan {
start: 3,
end: 5,
kind: TokenKind::Keyword,
},
TokenSpan {
start: 5,
end: 7,
kind: TokenKind::Plain,
},
TokenSpan {
start: 7,
end: 9,
kind: TokenKind::Number,
},
]);
assert_eq!(
merged,
vec![
TokenSpan {
start: 0,
end: 5,
kind: TokenKind::Keyword,
},
TokenSpan {
start: 7,
end: 9,
kind: TokenKind::Number,
},
]
);
}
#[test]
fn same_kind_spans_split_by_a_gap_are_not_merged() {
let merged = coalesce_drop_plain(vec![
TokenSpan {
start: 0,
end: 3,
kind: TokenKind::Keyword,
},
TokenSpan {
start: 5,
end: 8,
kind: TokenKind::Keyword,
},
]);
assert_eq!(merged.len(), 2);
}
}