use lsp_types::{
SemanticToken, SemanticTokenType, SemanticTokens, SemanticTokensLegend, SemanticTokensParams,
SemanticTokensResult,
};
use rowan::TextRange;
use crate::config::Flavor;
use crate::lsp::conversions::offset_to_position;
use crate::lsp::global_state::StateSnapshot;
use crate::syntax::{SyntaxKind, SyntaxNode};
const TOKEN_TYPES: &[&str] = &[
"citation", "crossref", "shortcode", "div", "math", "footnote", "attribute", ];
pub(crate) fn legend() -> SemanticTokensLegend {
SemanticTokensLegend {
token_types: TOKEN_TYPES
.iter()
.map(|&name| SemanticTokenType::new(name))
.collect(),
token_modifiers: Vec::new(),
}
}
fn token_type_for(kind: SyntaxKind) -> Option<u32> {
Some(match kind {
SyntaxKind::CITATION_KEY => 0,
SyntaxKind::CROSSREF_KEY => 1,
SyntaxKind::SHORTCODE => 2,
SyntaxKind::DIV_INFO => 3,
SyntaxKind::INLINE_MATH_MARKER | SyntaxKind::DISPLAY_MATH_MARKER => 4,
SyntaxKind::FOOTNOTE_REFERENCE => 5,
SyntaxKind::SPAN_ATTRIBUTES => 6,
_ => return None,
})
}
pub(crate) fn semantic_tokens_full(
snap: &StateSnapshot,
params: SemanticTokensParams,
) -> Option<SemanticTokensResult> {
let uri = ¶ms.text_document.uri;
if matches!(snap.config(uri).flavor, Flavor::CommonMark | Flavor::Gfm) {
return Some(empty());
}
let (text, root) = snap.document_content_and_tree(uri)?;
let tokens = collect_tokens(&root);
let data = encode(&text, tokens);
Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data,
}))
}
fn empty() -> SemanticTokensResult {
SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data: Vec::new(),
})
}
fn collect_tokens(root: &SyntaxNode) -> Vec<(TextRange, u32)> {
root.descendants_with_tokens()
.filter_map(|element| {
let (kind, range) = match &element {
rowan::NodeOrToken::Node(node) => (node.kind(), node.text_range()),
rowan::NodeOrToken::Token(token) => (token.kind(), token.text_range()),
};
token_type_for(kind).map(|token_type| (range, token_type))
})
.collect()
}
fn encode(text: &str, tokens: Vec<(TextRange, u32)>) -> Vec<SemanticToken> {
let mut data = Vec::with_capacity(tokens.len());
let mut prev_line = 0u32;
let mut prev_start = 0u32;
for (range, token_type) in tokens {
let start = offset_to_position(text, range.start().into());
let end = offset_to_position(text, range.end().into());
if start.line != end.line {
continue;
}
let length = end.character.saturating_sub(start.character);
if length == 0 {
continue;
}
let delta_line = start.line - prev_line;
let delta_start = if delta_line == 0 {
start.character - prev_start
} else {
start.character
};
data.push(SemanticToken {
delta_line,
delta_start,
length,
token_type,
token_modifiers_bitset: 0,
});
prev_line = start.line;
prev_start = start.character;
}
data
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(content: &str, flavor: Flavor) -> SyntaxNode {
let config = crate::config::Config {
flavor,
extensions: crate::config::Extensions::for_flavor(flavor),
..crate::config::Config::default()
};
crate::parser::parse(content, Some(config))
}
fn decode(data: &[SemanticToken]) -> Vec<(u32, u32, u32, u32)> {
let mut out = Vec::new();
let (mut line, mut start) = (0u32, 0u32);
for tok in data {
if tok.delta_line == 0 {
start += tok.delta_start;
} else {
line += tok.delta_line;
start = tok.delta_start;
}
out.push((line, start, tok.length, tok.token_type));
}
out
}
#[test]
fn maps_quarto_constructs_to_types() {
let content = "[@key] @fig-1 $a$ $$b$$ {{< x >}} [^1]\n\n[s]{.cls}\n\n::: {.note}\nhi\n:::\n\n[^1]: note\n";
let root = parse(content, Flavor::Quarto);
let types: std::collections::BTreeSet<u32> =
collect_tokens(&root).into_iter().map(|(_, t)| t).collect();
assert_eq!(types, (0..=6).collect());
}
#[test]
fn encodes_relative_deltas_single_line() {
let content = "see [@a] and [@bb] here\n";
let root = parse(content, Flavor::Pandoc);
let data = encode(content, collect_tokens(&root));
let decoded = decode(&data);
assert_eq!(decoded, vec![(0, 6, 1, 0), (0, 15, 2, 0)]);
}
#[test]
fn utf16_length_and_offsets() {
let content = "é [@kä] x\n";
let root = parse(content, Flavor::Pandoc);
let data = encode(content, collect_tokens(&root));
let decoded = decode(&data);
assert_eq!(decoded, vec![(0, 4, 2, 0)]);
}
#[test]
fn skips_cross_line_token() {
let text = "ab\ncd\n";
let range = TextRange::new(1.into(), 4.into()); let data = encode(text, vec![(range, 0)]);
assert!(data.is_empty(), "cross-line token should be skipped");
}
#[test]
fn crlf_line_deltas() {
let content = "[@a]\r\n[@b]\r\n";
let root = parse(content, Flavor::Pandoc);
let data = encode(content, collect_tokens(&root));
let decoded = decode(&data);
assert_eq!(decoded, vec![(0, 2, 1, 0), (1, 2, 1, 0)]);
}
}