use std::panic::AssertUnwindSafe;
use std::path::Path;
use std::sync::Arc;
use lsp_types::{Position, SemanticToken, SemanticTokenType, SemanticTokens, SemanticTokensLegend};
use rowan::{TextRange, TextSize};
use smol_str::SmolStr;
use crate::incremental::Analysis;
use crate::index::{ModuleIndex, PackageIndex};
use crate::parser::parse;
use crate::resolve::{
ModulePath, Namespace, PackageSource, Resolution, Resolver, module_at, resolve_submodule,
};
use crate::semantic::{BindingKind, LoadKind, SemanticModel};
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
use crate::text::{LineIndex, PositionEncoding};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HighlightKind {
Keyword = 0,
Macro = 1,
String = 2,
Number = 3,
Function = 4,
Type = 5,
Module = 6,
}
pub(crate) fn legend() -> SemanticTokensLegend {
SemanticTokensLegend {
token_types: vec![
SemanticTokenType::KEYWORD,
SemanticTokenType::MACRO,
SemanticTokenType::STRING,
SemanticTokenType::NUMBER,
SemanticTokenType::FUNCTION,
SemanticTokenType::TYPE,
SemanticTokenType::NAMESPACE,
],
token_modifiers: vec![],
}
}
pub fn compute_semantic_tokens<P: PackageSource>(
text: &str,
encoding: PositionEncoding,
packages: &P,
) -> SemanticTokens {
let root = parse(text).cst;
let model = SemanticModel::build(&root);
tokens_for(&root, &model, packages, None, text, encoding)
}
pub(crate) fn semantic_tokens_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
encoding: PositionEncoding,
) -> SemanticTokens {
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
let root = snapshot.parsed_tree(file);
let model = snapshot.semantic_model(file);
let workspace = snapshot.workspace_member(path);
Some(tokens_for(
&root, model, snapshot, workspace, text, encoding,
))
}));
match cached {
Ok(Some(tokens)) => tokens,
Ok(None) | Err(_) => compute_semantic_tokens(text, encoding, snapshot),
}
}
fn tokens_for<P: PackageSource + ?Sized>(
root: &SyntaxNode,
model: &SemanticModel,
packages: &P,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
text: &str,
encoding: PositionEncoding,
) -> SemanticTokens {
let mut spans = syntax_spans(root);
spans.extend(resolved_spans(model, packages, workspace, text));
spans.sort_by_key(|&(range, _)| (range.start(), range.end()));
let spans = drop_overlaps(spans);
SemanticTokens {
result_id: None,
data: delta_encode(&spans, text, encoding),
}
}
fn syntax_spans(root: &SyntaxNode) -> Vec<(TextRange, HighlightKind)> {
let mut spans: Vec<(TextRange, HighlightKind)> = Vec::new();
for token in root
.descendants_with_tokens()
.filter_map(|el| el.into_token())
{
let Some(kind) = classify(&token) else {
continue;
};
match spans.last_mut() {
Some((range, last)) if *last == kind && range.end() == token.text_range().start() => {
*range = TextRange::new(range.start(), token.text_range().end());
}
_ => spans.push((token.text_range(), kind)),
}
}
spans
}
fn drop_overlaps(spans: Vec<(TextRange, HighlightKind)>) -> Vec<(TextRange, HighlightKind)> {
let mut out: Vec<(TextRange, HighlightKind)> = Vec::with_capacity(spans.len());
for (range, kind) in spans {
match out.last() {
Some((prev, _)) if range.start() < prev.end() => {}
_ => out.push((range, kind)),
}
}
out
}
fn resolved_spans<P: PackageSource + ?Sized>(
model: &SemanticModel,
packages: &P,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
text: &str,
) -> Vec<(TextRange, HighlightKind)> {
let mut spans = Vec::new();
for binding in model.bindings() {
if !is_identifier_shaped(&binding.name) {
continue;
}
if let Some(kind) = classify_binding(binding.kind) {
spans.push((binding.def_range, kind));
}
}
for entry in model.exports() {
if let Some(bid) = entry.binding
&& let Some(kind) = classify_binding(model.binding(bid).kind)
{
spans.push((entry.range, kind));
}
}
let resolver = Resolver::new(model, packages).with_workspace(workspace.clone());
for ident in model.idents() {
if ident.is_macro || !is_identifier_shaped(&ident.name) {
continue;
}
let kind = match ident.binding {
Some(bid) => classify_binding(model.binding(bid).kind),
None => classify_free_read(
model,
packages,
&workspace,
&resolver,
&ident.name,
ident.range.start(),
),
};
if let Some(kind) = kind {
spans.push((ident.range, kind));
}
}
spans.extend(qualified_spans(model, packages, text));
spans
}
fn classify_binding(kind: BindingKind) -> Option<HighlightKind> {
match kind {
BindingKind::Function => Some(HighlightKind::Function),
BindingKind::Macro => Some(HighlightKind::Macro),
BindingKind::Type => Some(HighlightKind::Type),
BindingKind::Module => Some(HighlightKind::Module),
_ => None,
}
}
fn is_identifier_shaped(name: &str) -> bool {
name.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_')
}
fn classify_free_read<P: PackageSource + ?Sized>(
model: &SemanticModel,
packages: &P,
workspace: &Option<(Arc<PackageIndex>, ModulePath)>,
resolver: &Resolver<'_, P>,
name: &str,
offset: TextSize,
) -> Option<HighlightKind> {
match resolver.resolve(name, offset, Namespace::Value) {
Resolution::Binding(bid) => classify_binding(model.binding(bid).kind),
Resolution::Workspace { module, name } => {
let (pkg, _) = workspace.as_ref()?;
library_kind(module_at(&pkg.root, &module)?, &name)
}
Resolution::System { module, name } => {
library_kind(&packages.package(&module)?.root, &name)
}
Resolution::Using { module, name } => using_kind(model, packages, &module, &name),
Resolution::Unresolved => None,
}
}
fn using_kind<P: PackageSource + ?Sized>(
model: &SemanticModel,
packages: &P,
module: &str,
name: &str,
) -> Option<HighlightKind> {
if let Some(pkg) = packages.package(module)
&& let Some(kind) = library_kind(&pkg.root, name)
{
return Some(kind);
}
for load in model.module_loads() {
if load.kind != LoadKind::Using || load.items.is_some() {
continue;
}
let comps = &load.path.components;
if comps.last().map(|c| c.as_str()) != Some(module) {
continue;
}
let Some(first) = comps.first() else { continue };
let Some(pkg) = packages.package(first.as_str()) else {
continue;
};
let rest: Vec<&str> = comps[1..].iter().map(|c| c.as_str()).collect();
if let Some(m) = resolve_submodule(&pkg.root, &rest)
&& let Some(kind) = library_kind(m, name)
{
return Some(kind);
}
}
None
}
fn library_kind(module: &ModuleIndex, name: &str) -> Option<HighlightKind> {
if name.starts_with('@') {
return module
.macros
.iter()
.any(|m| m.name == name)
.then_some(HighlightKind::Macro);
}
if module.functions.iter().any(|f| f.name == name) {
return Some(HighlightKind::Function);
}
if module.types.iter().any(|t| t.name == name) {
return Some(HighlightKind::Type);
}
if module.submodules.iter().any(|m| m.name == name) {
return Some(HighlightKind::Module);
}
None
}
fn qualified_spans<P: PackageSource + ?Sized>(
model: &SemanticModel,
packages: &P,
text: &str,
) -> Vec<(TextRange, HighlightKind)> {
let mut spans = Vec::new();
for q in model.qualified_reads() {
let Some((member, modules)) = q.path.split_last() else {
continue;
};
let Some(head) = modules.first() else {
continue;
};
let Some(pkg) = packages.package(head) else {
continue;
};
let Some(ranges) = component_ranges(text, q.range, &q.path) else {
continue;
};
spans.push((ranges[0], HighlightKind::Module));
let mut module = Some(&pkg.root);
for (i, comp) in modules.iter().enumerate().skip(1) {
module = module.and_then(|m| m.submodules.iter().find(|s| s.name == comp.as_str()));
match module {
Some(_) => spans.push((ranges[i], HighlightKind::Module)),
None => break,
}
}
if let Some(m) = module
&& !q.is_macro
&& let Some(kind) = library_kind(m, member)
{
spans.push((ranges[modules.len()], kind));
}
}
spans
}
fn component_ranges(text: &str, range: TextRange, path: &[SmolStr]) -> Option<Vec<TextRange>> {
let base = usize::from(range.start());
let slice = &text[base..usize::from(range.end())];
let mut ranges = Vec::with_capacity(path.len());
let mut cursor = 0;
for comp in path {
let at = slice[cursor..].find(comp.as_str())? + cursor;
let start = (base + at) as u32;
ranges.push(TextRange::new(
TextSize::new(start),
TextSize::new(start + comp.len() as u32),
));
cursor = at + comp.len();
}
Some(ranges)
}
fn classify(token: &SyntaxToken) -> Option<HighlightKind> {
match token.parent().map(|parent| parent.kind()) {
Some(SyntaxKind::MACRO_NAME) => classify_in_macro_name(token),
Some(SyntaxKind::STRING_LITERAL | SyntaxKind::CMD_LITERAL) => {
classify_in_string(token.kind())
}
_ => classify_by_kind(token.kind()),
}
}
fn classify_in_macro_name(token: &SyntaxToken) -> Option<HighlightKind> {
if token.kind() == SyntaxKind::AT {
return Some(HighlightKind::Macro);
}
if token.kind() != SyntaxKind::RPAREN && is_last_name_token(token) {
return Some(HighlightKind::Macro);
}
None
}
fn is_last_name_token(token: &SyntaxToken) -> bool {
let Some(parent) = token.parent() else {
return false;
};
parent
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| !is_trivia(t.kind()))
.last()
.is_some_and(|last| last == *token)
}
fn classify_in_string(kind: SyntaxKind) -> Option<HighlightKind> {
match kind {
SyntaxKind::STRING_CONTENT
| SyntaxKind::STRING_DELIM_OPEN
| SyntaxKind::STRING_DELIM_CLOSE
| SyntaxKind::CMD_DELIM_OPEN
| SyntaxKind::CMD_DELIM_CLOSE => Some(HighlightKind::String),
SyntaxKind::STRING_PREFIX | SyntaxKind::STRING_SUFFIX => Some(HighlightKind::Macro),
_ => classify_by_kind(kind),
}
}
fn classify_by_kind(kind: SyntaxKind) -> Option<HighlightKind> {
if is_keyword(kind) {
return Some(HighlightKind::Keyword);
}
match kind {
SyntaxKind::CHAR => Some(HighlightKind::String),
SyntaxKind::INTEGER
| SyntaxKind::BIN_INT
| SyntaxKind::OCT_INT
| SyntaxKind::HEX_INT
| SyntaxKind::FLOAT
| SyntaxKind::FLOAT32 => Some(HighlightKind::Number),
_ => None,
}
}
fn is_keyword(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::FUNCTION_KW
| SyntaxKind::MACRO_KW
| SyntaxKind::END_KW
| SyntaxKind::IF_KW
| SyntaxKind::ELSEIF_KW
| SyntaxKind::ELSE_KW
| SyntaxKind::BEGIN_KW
| SyntaxKind::TRUE_KW
| SyntaxKind::FALSE_KW
| SyntaxKind::WHILE_KW
| SyntaxKind::FOR_KW
| SyntaxKind::LET_KW
| SyntaxKind::QUOTE_KW
| SyntaxKind::TRY_KW
| SyntaxKind::CATCH_KW
| SyntaxKind::FINALLY_KW
| SyntaxKind::STRUCT_KW
| SyntaxKind::MUTABLE_KW
| SyntaxKind::MODULE_KW
| SyntaxKind::BAREMODULE_KW
| SyntaxKind::DO_KW
| SyntaxKind::RETURN_KW
| SyntaxKind::BREAK_KW
| SyntaxKind::CONTINUE_KW
| SyntaxKind::CONST_KW
| SyntaxKind::GLOBAL_KW
| SyntaxKind::LOCAL_KW
| SyntaxKind::IMPORT_KW
| SyntaxKind::USING_KW
| SyntaxKind::EXPORT_KW
| SyntaxKind::WHERE_KW
)
}
fn is_trivia(kind: SyntaxKind) -> bool {
matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
}
fn delta_encode(
spans: &[(TextRange, HighlightKind)],
text: &str,
encoding: PositionEncoding,
) -> Vec<SemanticToken> {
let line_index = LineIndex::new(text);
let mut data = Vec::new();
let mut prev = Position::new(0, 0);
for &(range, kind) in spans {
for segment in split_at_line_breaks(range, text) {
let start = line_index.byte_to_position(segment.start().into(), encoding);
let end = line_index.byte_to_position(segment.end().into(), encoding);
debug_assert_eq!(start.line, end.line, "segments never span line breaks");
let delta_line = start.line - prev.line;
let delta_start = if delta_line == 0 {
start.character - prev.character
} else {
start.character
};
data.push(SemanticToken {
delta_line,
delta_start,
length: end.character - start.character,
token_type: kind as u32,
token_modifiers_bitset: 0,
});
prev = start;
}
}
data
}
fn split_at_line_breaks(range: TextRange, text: &str) -> Vec<TextRange> {
let base = usize::from(range.start());
let slice = &text[base..usize::from(range.end())];
let mut segments = Vec::new();
let mut push = |from: usize, to: usize| {
let to = if slice.as_bytes()[from..to].last() == Some(&b'\r') {
to - 1
} else {
to
};
if to > from {
segments.push(TextRange::new(
TextSize::new((base + from) as u32),
TextSize::new((base + to) as u32),
));
}
};
let mut start = 0;
for (i, byte) in slice.bytes().enumerate() {
if byte == b'\n' {
push(start, i);
start = i + 1;
}
}
push(start, slice.len());
segments
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::sync::Arc;
use lsp_types::Position;
use crate::incremental::IncrementalDatabase;
use crate::index::model::{DefLocation, ExportedName, Span, Visibility};
use crate::index::{
ConstDef, FunctionGroup, MacroDef, ModuleIndex, PackageIndex, TypeDef, TypeKind,
};
#[test]
fn legend_order_matches_highlight_kind_discriminants() {
let legend = legend();
for (kind, token_type) in [
(HighlightKind::Keyword, SemanticTokenType::KEYWORD),
(HighlightKind::Macro, SemanticTokenType::MACRO),
(HighlightKind::String, SemanticTokenType::STRING),
(HighlightKind::Number, SemanticTokenType::NUMBER),
(HighlightKind::Function, SemanticTokenType::FUNCTION),
(HighlightKind::Type, SemanticTokenType::TYPE),
(HighlightKind::Module, SemanticTokenType::NAMESPACE),
] {
assert_eq!(legend.token_types[kind as usize], token_type);
}
assert_eq!(legend.token_types.len(), 7, "every kind is in the legend");
assert!(legend.token_modifiers.is_empty());
}
fn loc() -> DefLocation {
DefLocation {
file: "src/x.jl".into(),
range: Span { start: 0, end: 0 },
}
}
fn module(name: &str, exports: &[&str]) -> ModuleIndex {
ModuleIndex {
name: name.to_string(),
bare: false,
loc: loc(),
exports: exports
.iter()
.map(|n| ExportedName {
name: n.to_string(),
visibility: Visibility::Exported,
loc: loc(),
})
.collect(),
functions: Vec::new(),
types: Vec::new(),
consts: Vec::new(),
macros: Vec::new(),
submodules: Vec::new(),
}
}
fn package(root: ModuleIndex) -> Arc<PackageIndex> {
Arc::new(PackageIndex {
name: root.name.clone(),
root,
members: Vec::new(),
member_modules: Default::default(),
diagnostics: Vec::new(),
})
}
fn library(pkgs: Vec<Arc<PackageIndex>>) -> BTreeMap<String, Arc<PackageIndex>> {
pkgs.into_iter().map(|p| (p.name.clone(), p)).collect()
}
fn function(name: &str) -> FunctionGroup {
FunctionGroup {
name: name.to_string(),
owner: None,
methods: Vec::new(),
doc: None,
}
}
fn typedef(name: &str) -> TypeDef {
TypeDef {
name: name.to_string(),
kind: TypeKind::Struct { mutable: false },
type_params: Vec::new(),
supertype: None,
fields: Vec::new(),
doc: None,
loc: loc(),
}
}
fn macrodef(name: &str) -> MacroDef {
MacroDef {
name: name.to_string(),
params: Vec::new(),
doc: None,
loc: loc(),
}
}
fn no_library() -> BTreeMap<String, Arc<PackageIndex>> {
BTreeMap::new()
}
fn painted(src: &str, lib: &BTreeMap<String, Arc<PackageIndex>>) -> Vec<(String, u32)> {
let tokens = compute_semantic_tokens(src, PositionEncoding::Utf8, lib);
let line_index = LineIndex::new(src);
let (mut line, mut character) = (0u32, 0u32);
let mut out = Vec::new();
for t in &tokens.data {
if t.delta_line > 0 {
line += t.delta_line;
character = 0;
}
character += t.delta_start;
let start =
line_index.position_to_byte(Position::new(line, character), PositionEncoding::Utf8);
out.push((
src[start..start + t.length as usize].to_string(),
t.token_type,
));
}
out
}
fn expect(pairs: &[(&str, HighlightKind)]) -> Vec<(String, u32)> {
pairs
.iter()
.map(|&(text, kind)| (text.to_string(), kind as u32))
.collect()
}
#[test]
fn function_names_paint_at_definition_and_use() {
assert_eq!(
painted("square(x) = x * x\nsquare(2)\n", &no_library()),
expect(&[
("square", HighlightKind::Function),
("square", HighlightKind::Function),
("2", HighlightKind::Number),
]),
);
}
#[test]
fn definition_names_paint_by_binding_kind() {
let src = "module M\nfunction f(x)\n x\nend\nmacro m(x)\n x\nend\nstruct S\n a\nend\nend\n";
assert_eq!(
painted(src, &no_library()),
expect(&[
("module", HighlightKind::Keyword),
("M", HighlightKind::Module),
("function", HighlightKind::Keyword),
("f", HighlightKind::Function),
("end", HighlightKind::Keyword),
("macro", HighlightKind::Keyword),
("m", HighlightKind::Macro),
("end", HighlightKind::Keyword),
("struct", HighlightKind::Keyword),
("S", HighlightKind::Type),
("end", HighlightKind::Keyword),
("end", HighlightKind::Keyword),
]),
);
}
#[test]
fn local_uses_paint_by_their_binding_kind() {
assert_eq!(
painted("struct P\n a\nend\nP\n", &no_library()),
expect(&[
("struct", HighlightKind::Keyword),
("P", HighlightKind::Type),
("end", HighlightKind::Keyword),
("P", HighlightKind::Type),
]),
);
assert_eq!(
painted("module M\nend\nM\n", &no_library()),
expect(&[
("module", HighlightKind::Keyword),
("M", HighlightKind::Module),
("end", HighlightKind::Keyword),
("M", HighlightKind::Module),
]),
);
}
#[test]
fn variables_and_parameters_stay_plain() {
assert_eq!(
painted("x = 1\nfunction g(y)\n x + y\nend\n", &no_library()),
expect(&[
("1", HighlightKind::Number),
("function", HighlightKind::Keyword),
("g", HighlightKind::Function),
("end", HighlightKind::Keyword),
]),
);
}
#[test]
fn library_free_reads_classify_by_symbol_kind() {
let mut base = module("Base", &["map", "Dict", "pi", "Threads"]);
base.functions.push(function("map"));
base.types.push(typedef("Dict"));
base.consts.push(ConstDef {
name: "pi".to_string(),
value_repr: None,
doc: None,
loc: loc(),
});
base.submodules.push(module("Threads", &[]));
let lib = library(vec![package(base)]);
assert_eq!(
painted("map(pi, xs)\nDict\nThreads\n", &lib),
expect(&[
("map", HighlightKind::Function),
("Dict", HighlightKind::Type),
("Threads", HighlightKind::Module),
]),
);
}
#[test]
fn using_export_paints_from_the_used_module() {
let mut a = module("A", &["greet"]);
a.functions.push(function("greet"));
let lib = library(vec![package(a)]);
assert_eq!(
painted("using A\ngreet(1)\n", &lib),
expect(&[
("using", HighlightKind::Keyword),
("greet", HighlightKind::Function),
("1", HighlightKind::Number),
]),
);
}
#[test]
fn a_local_shadow_masks_the_library() {
let mut base = module("Base", &["map"]);
base.functions.push(function("map"));
let lib = library(vec![package(base)]);
assert_eq!(
painted("function h(map)\n map\nend\n", &lib),
expect(&[
("function", HighlightKind::Keyword),
("h", HighlightKind::Function),
("end", HighlightKind::Keyword),
]),
);
}
#[test]
fn qualified_reads_paint_namespaces_and_the_member() {
let mut inner = module("Threads", &[]);
inner.macros.push(macrodef("@spawn"));
inner.functions.push(function("nthreads"));
let mut base = module("Base", &[]);
base.submodules.push(inner);
base.functions.push(function("map"));
let lib = library(vec![package(base)]);
assert_eq!(
painted("Base.map(f, xs)\n", &lib),
expect(&[
("Base", HighlightKind::Module),
("map", HighlightKind::Function),
]),
);
assert_eq!(
painted("Base.Threads.nthreads()\n", &lib),
expect(&[
("Base", HighlightKind::Module),
("Threads", HighlightKind::Module),
("nthreads", HighlightKind::Function),
]),
);
assert_eq!(
painted("Base.Threads.@spawn f()\n", &lib),
expect(&[
("Base", HighlightKind::Module),
("Threads", HighlightKind::Module),
("@spawn", HighlightKind::Macro),
]),
);
assert_eq!(
painted("Nope.f(1)\n", &lib),
expect(&[("1", HighlightKind::Number)]),
);
}
#[test]
fn operator_reads_stay_plain_even_when_they_resolve() {
let mut base = module("Base", &["+"]);
base.functions.push(function("+"));
let lib = library(vec![package(base)]);
assert_eq!(
painted("1 + 2\n", &lib),
expect(&[("1", HighlightKind::Number), ("2", HighlightKind::Number),]),
);
}
#[test]
fn export_list_names_paint_by_their_binding_kind() {
assert_eq!(
painted("module M\nexport f\nf(x) = x\nend\n", &no_library()),
expect(&[
("module", HighlightKind::Keyword),
("M", HighlightKind::Module),
("export", HighlightKind::Keyword),
("f", HighlightKind::Function),
("f", HighlightKind::Function),
("end", HighlightKind::Keyword),
]),
);
}
#[test]
fn semantic_tokens_via_db_match_compute_and_fall_back() {
let path = Path::new("/work/a.jl");
let buffer = "function f(x)\n @show x + 1\nend\n";
let mut base = module("Base", &["map"]);
base.functions.push(function("map"));
let lib = library(vec![package(base)]);
let expected = compute_semantic_tokens(buffer, PositionEncoding::Utf8, &lib);
assert!(!expected.data.is_empty(), "fixture must yield tokens");
let mut db = IncrementalDatabase::default();
db.set_library_packages(lib.clone());
db.upsert_file(path, buffer.to_string());
assert_eq!(
semantic_tokens_via_db(&db.snapshot(), path, buffer, PositionEncoding::Utf8),
expected,
"cached-tree tokens must match the re-parse path"
);
let mut stale = IncrementalDatabase::default();
stale.set_library_packages(lib.clone());
stale.upsert_file(path, "y = 1\n".to_string());
assert_eq!(
semantic_tokens_via_db(&stale.snapshot(), path, buffer, PositionEncoding::Utf8),
expected,
"version skew must fall back to the buffer text"
);
let mut empty = IncrementalDatabase::default();
empty.set_library_packages(lib);
assert_eq!(
semantic_tokens_via_db(&empty.snapshot(), path, buffer, PositionEncoding::Utf8),
expected,
"untracked path must fall back to the buffer text"
);
}
}