use cpp_code_analysis::semantic::{Name, NameKind};
use cpp_code_analysis::{FileView, VfsFile};
use lsp_types::{
ClientCapabilities, SemanticToken, SemanticTokenType, SemanticTokens,
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams,
SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::uri_to_file_path;
mod builder;
pub use builder::{Builder, Modifier, TypeKind};
fn legend() -> SemanticTokensLegend {
SemanticTokensLegend {
token_types: Builder::legend(),
token_modifiers: Modifier::all_modifiers(),
}
}
fn type_of_a_name_kind(kind: NameKind) -> Option<TypeKind> {
Some(match kind {
NameKind::Namespace => TypeKind::Namespace,
NameKind::Type => TypeKind::Type,
NameKind::TypeParameter => TypeKind::TypeParameter,
NameKind::EnumMember => TypeKind::EnumMember,
NameKind::Function => TypeKind::Function,
NameKind::Method => TypeKind::Method,
NameKind::Variable => TypeKind::Variable,
NameKind::VariableTemplate => TypeKind::VariableTemplate,
NameKind::Parameter => TypeKind::Parameter,
NameKind::Macro => TypeKind::Macro,
})
}
fn modifiers_of(name: &Name) -> Modifier {
use cpp_code_analysis::semantic::NameModifier;
let mut bits = Modifier::none();
if name.declaration {
bits |= Modifier::DECLARATION;
}
for modifier in &name.modifiers {
bits |= match modifier {
NameModifier::ReadOnly => Modifier::READONLY,
NameModifier::Static => Modifier::STATIC,
NameModifier::Deprecated => Modifier::DEPRECATED,
};
}
bits
}
fn push_names(builder: &mut Builder<'_>, names: &[Name]) {
for name in names {
let Some(kind) = type_of_a_name_kind(name.kind) else {
continue;
};
let pushed = builder.push_range(
name.range.start_offset,
name.range.end_offset(),
kind,
modifiers_of(name),
);
if !pushed && kind == TypeKind::VariableTemplate {
builder.push_range(
name.range.start_offset,
name.range.end_offset(),
TypeKind::Variable,
modifiers_of(name),
);
}
}
}
fn push_keywords(builder: &mut Builder<'_>, view: Option<&FileView>) {
let Some(view) = view else {
return;
};
for token in view.tree.get_tokens() {
let drawn = match token.kind {
cpp_parser::CppTokenKind::Identifier => {
let Some(text) = view.get_token_text(token.range) else {
continue;
};
if !is_contextual_keyword(text) {
continue;
}
TypeKind::Keyword
}
cpp_parser::CppTokenKind::BoolLiteral => match view.get_token_text(token.range) {
Some("bool") => TypeKind::Type,
_ => TypeKind::Variable,
},
other if cpp_parser::is_keyword(other) => {
kind_of_keyword(other).unwrap_or(TypeKind::Keyword)
}
_ => continue,
};
builder.push_range(
token.range.start_offset,
token.range.end_offset(),
drawn,
Modifier::none(),
);
}
}
fn push_module_declarations(builder: &mut Builder<'_>, view: Option<&FileView>) {
let Some(view) = view else {
return;
};
for node in view.root.descendants() {
let directive = cpp_parser::CppSyntaxKind::from(node.kind());
if !matches!(
directive,
cpp_parser::CppSyntaxKind::ModuleDecl | cpp_parser::CppSyntaxKind::ImportDecl
) {
continue;
}
for element in node.descendants_with_tokens() {
let Some(token) = element.into_token() else {
continue;
};
let range = cpp_parser::source_range(token.text_range());
let Some(text) = view.get_token_text(range) else {
continue;
};
let in_a_module_name = token.parent().is_some_and(|parent| {
cpp_parser::CppSyntaxKind::from(parent.kind())
== cpp_parser::CppSyntaxKind::ModuleName
});
let drawn = match text {
"module" | "import" | "export" => TypeKind::Keyword,
_ if in_a_module_name
&& token.kind() == cpp_parser::CppTokenKind::Identifier.into() =>
{
TypeKind::Namespace
}
_ => continue,
};
builder.push_range(
range.start_offset,
range.end_offset(),
drawn,
Modifier::none(),
);
}
}
}
pub fn encode(
file: &VfsFile,
view: Option<&FileView>,
names: &[Name],
drawable: &Option<Vec<SemanticTokenType>>,
multi_line_support: bool,
formats: &[cpp_code_analysis::format::FormatLiteral],
) -> Vec<SemanticToken> {
let mut builder = Builder::new(file, drawable.as_deref(), multi_line_support);
push_names(&mut builder, names);
push_keywords(&mut builder, view);
push_module_declarations(&mut builder, view);
push_doc_commands(&mut builder, view);
crate::handlers::format::draw(&mut builder, formats);
if builder.unplaceable() > 0 {
log::debug!(
"{} claim(s) dropped: the file cannot place their offsets",
builder.unplaceable()
);
}
builder.build()
}
fn is_contextual_keyword(text: &str) -> bool {
matches!(text, "requires" | "concept")
}
fn kind_of_keyword(kind: cpp_parser::CppTokenKind) -> Option<TypeKind> {
use cpp_parser::CppTokenKind;
Some(match kind {
CppTokenKind::CharKeyword
| CppTokenKind::ShortKeyword
| CppTokenKind::IntKeyword
| CppTokenKind::LongKeyword
| CppTokenKind::SignedKeyword
| CppTokenKind::UnsignedKeyword
| CppTokenKind::FloatKeyword
| CppTokenKind::DoubleKeyword
| CppTokenKind::VoidKeyword => TypeKind::Type,
CppTokenKind::ThisKeyword | CppTokenKind::NullptrKeyword => TypeKind::Variable,
CppTokenKind::TrueKeyword | CppTokenKind::FalseKeyword => TypeKind::Variable,
CppTokenKind::ConstKeyword
| CppTokenKind::ConstexprKeyword
| CppTokenKind::ConstevalKeyword
| CppTokenKind::ConstinitKeyword
| CppTokenKind::VolatileKeyword
| CppTokenKind::MutableKeyword
| CppTokenKind::StaticKeyword
| CppTokenKind::ExternKeyword
| CppTokenKind::RegisterKeyword
| CppTokenKind::ThreadLocalKeyword
| CppTokenKind::InlineKeyword
| CppTokenKind::VirtualKeyword
| CppTokenKind::ExplicitKeyword
| CppTokenKind::FriendKeyword
| CppTokenKind::TypenameKeyword
| CppTokenKind::AutoKeyword
| CppTokenKind::DecltypeKeyword
| CppTokenKind::NoexceptKeyword => TypeKind::Modifier,
_ => return None,
})
}
fn push_doc_commands(builder: &mut Builder<'_>, view: Option<&FileView>) {
let Some(view) = view else {
return;
};
let source: &str = view.written_text().unwrap_or(&view.source);
let mut in_a_block = false;
let mut line_start = 0usize;
for line in source.split_inclusive('\n') {
let trimmed = line.trim_start();
let indentation = line.len() - trimmed.len();
let is_a_documentation_line = if in_a_block {
true
} else {
trimmed.starts_with("///")
|| trimmed.starts_with("//!")
|| trimmed.starts_with("/**")
|| trimmed.starts_with("/*!")
};
if is_a_documentation_line {
let bytes = line.as_bytes();
let mut at = 0usize;
while at < bytes.len() {
if !matches!(bytes[at], b'@' | b'\\') {
at += 1;
continue;
}
let word_start = at;
let mut word_end = at + 1;
while word_end < bytes.len()
&& (bytes[word_end].is_ascii_alphanumeric() || bytes[word_end] == b'_')
{
word_end += 1;
}
if word_end > word_start + 1 {
let name = &line[word_start + 1..word_end];
if cpp_parser::command_kind(name).is_some() {
builder.push_range(
line_start + word_start,
line_start + word_end,
TypeKind::DocCommand,
Modifier::none(),
);
}
}
at = word_end.max(at + 1);
}
}
if in_a_block {
if trimmed.contains("*/") {
in_a_block = false;
}
} else if (trimmed.starts_with("/**") || trimmed.starts_with("/*!"))
&& !trimmed.contains("*/")
{
in_a_block = true;
}
let _ = indentation;
line_start += line.len();
}
}
fn drawable_types(supported: Option<Vec<SemanticTokenType>>) -> Option<Vec<SemanticTokenType>> {
match supported {
Some(types) if !types.is_empty() => Some(types),
_ => None,
}
}
fn extended_with_the_custom_types(
drawable: Option<Vec<SemanticTokenType>>,
client: crate::context::ClientId,
) -> Option<Vec<SemanticTokenType>> {
let mut drawable = drawable?;
if !client.is_vscode() {
return Some(drawable);
}
for kind in [
TypeKind::FormatSpecifier,
TypeKind::FormatArgument,
TypeKind::FormatConversion,
TypeKind::FormatEscape,
] {
let name = kind.to_semantic_token_type();
if !drawable.contains(&name) {
drawable.push(name);
}
}
Some(drawable)
}
fn multi_line_support(context: &ServerContextSnapshot) -> bool {
context.lsp_features().supports_multiline_tokens()
}
pub async fn on_semantic_tokens(
context: ServerContextSnapshot,
params: SemanticTokensParams,
cancel_token: CancellationToken,
) -> RequestOutcome<SemanticTokensResult> {
let uri = params.text_document.uri;
let drawable = extended_with_the_custom_types(
drawable_types(context.lsp_features().semantic_token_types()),
context.lsp_features().client_id(),
);
let multi_line = multi_line_support(&context);
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
context.analysis().catch_up(&path).await;
}
snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let view = session.view_of_the_file(&path)?;
let held = session.files().held(&view.path)?;
if !session.is_locally_ready(&view.path) || !session.is_closure_ready(&view.path) {
log::debug!(
"no semantic tokens yet: {} file(s) are queued, and a name in one of them would be uncoloured",
session.pending()
);
return None;
}
let names = session.classified_names(&view);
let formats = {
let Some(config) = crate::handlers::format::config_for(session) else {
return Some(SemanticTokensResult::Tokens(SemanticTokens { result_id: None, data: encode(held, Some(&view), &names, &drawable, multi_line, &[]) }));
};
if config.is_off() {
Vec::new()
} else {
session.format_literals(&view, &config)
}
};
Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data: encode(held, Some(&view), &names, &drawable, multi_line, &formats),
}))
})
.await
}
pub struct SemanticTokenCapabilities;
impl RegisterCapabilities for SemanticTokenCapabilities {
fn register_capabilities(
server_capabilities: &mut ServerCapabilities,
client: &ClientCapabilities,
) {
let semantic_tokens = client
.text_document
.as_ref()
.and_then(|text_document| text_document.semantic_tokens.as_ref());
let Some(semantic_tokens) = semantic_tokens else {
return;
};
if semantic_tokens.requests.full.is_none() {
return;
}
server_capabilities.semantic_tokens_provider = Some(
SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
work_done_progress_options: Default::default(),
legend: legend(),
range: Some(false),
full: Some(SemanticTokensFullOptions::Bool(true)),
}),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use cpp_parser::SourceRange;
#[test]
fn a_template_parameter_is_a_type_parameter_and_a_variable_template_is_a_variable_template() {
let text = "\
template <class T>
constexpr bool is_trivially_destructible_v = __is_trivially_destructible(T);
template <class To, class From>
constexpr bool is_assignable_v = __is_assignable(To, From);
";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
for (word, expected, why) in [
(
"T",
TypeKind::TypeParameter,
"a template parameter is not a type",
),
("To", TypeKind::TypeParameter, "and neither is this one"),
("From", TypeKind::TypeParameter, "nor this"),
(
"is_trivially_destructible_v",
TypeKind::VariableTemplate,
"a variable template names a family of values, not one",
),
(
"is_assignable_v",
TypeKind::VariableTemplate,
"and so does this one",
),
] {
let at = text[..text.find(word).expect("the fixture writes it")].len();
assert_eq!(
at_type.get(&at),
Some(&index_of(expected)),
"`{word}`: {why}"
);
}
let standard = Some(vec![
lsp_types::SemanticTokenType::VARIABLE,
lsp_types::SemanticTokenType::TYPE_PARAMETER,
]);
let fallback = by_offset(
text,
&encode(file, Some(&view), &names, &standard, false, &[]),
);
let at = text[..text
.find("is_trivially_destructible_v")
.expect("the fixture writes it")]
.len();
assert_eq!(
fallback.get(&at),
Some(&index_of(TypeKind::Variable)),
"a client that cannot draw `variableTemplate` is answered with `variable`"
);
assert_eq!(
at_type.get(&text.find("T>").expect("the fixture writes it")),
Some(&index_of(TypeKind::TypeParameter)),
"the declaration is a template parameter"
);
let in_the_macro = text.rfind("(T)").expect("the fixture writes it") + 1;
assert_eq!(
at_type.get(&in_the_macro),
None,
"NOT YET: a template parameter written as a macro's argument gets no token"
);
}
#[test]
fn the_template_keyword_is_a_keyword_in_both_positions() {
let text = "\
template <template <typename, typename> class Container, typename T>
using ContainerOf = Container<int, int>;
";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = by_offset(text, &encode(file, Some(&view), &names, &None, false, &[]));
let outer = text.find("template").expect("the fixture writes it");
assert_eq!(
tokens.get(&outer),
Some(&index_of(TypeKind::Keyword)),
"the declaration's own `template`"
);
let inner = text[outer + 1..]
.find("template")
.map(|at| at + outer + 1)
.expect("the fixture writes a template template parameter");
assert_eq!(
tokens.get(&inner),
Some(&index_of(TypeKind::Keyword)),
"the template template parameter's `template`"
);
}
fn a_held_file(text: &str) -> (Session<MemoryFiles>, std::path::PathBuf) {
let files = MemoryFiles::new().with_file("/p/a.cpp", text);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = cpp_code_analysis::Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.load("/p/a.cpp");
(session, std::path::PathBuf::from("/p/a.cpp"))
}
#[test]
#[ignore = "a measurement: the fixture is deliberately huge"]
fn a_big_file_measures_what_a_walk_costs() {
let mut valid = String::from("namespace demo {\n");
for index in 0..2000 {
valid.push_str(&format!(
"int value_{index}(int a, int b) {{ return a + b; }}\n"
));
}
valid.push_str("}\n");
for (name, text) in [
("a namespace of 2 000 functions", valid),
(
"4 000 `for` statements at file scope",
std::iter::repeat_n(
"for (int i = 0; i < n; ++i) { total += values[i].size(); }\n",
4000,
)
.collect::<String>(),
),
] {
measure_one(name, &text);
}
}
fn measure_one(name: &str, text: &str) {
let (session, path) = a_held_file(text);
let started = std::time::Instant::now();
let view = session.view_of_the_file(&path).expect("a view");
let view_time = started.elapsed();
let file = session.files().held(&path).expect("the file is held");
let started = std::time::Instant::now();
let names = session.classified_names(&view);
let classify_time = started.elapsed();
let tokens = view.tree.get_tokens().len();
let mut fetched = 0;
let mut keywords = 0;
for token in view.tree.get_tokens() {
let needs_text = matches!(
token.kind,
cpp_parser::CppTokenKind::Identifier | cpp_parser::CppTokenKind::BoolLiteral
);
let keyword = cpp_parser::is_keyword(token.kind);
fetched += usize::from(needs_text && !keyword);
keywords += usize::from(keyword);
}
let started = std::time::Instant::now();
let rounds = 20;
let mut drawn = 0;
for _ in 0..rounds {
drawn = encode(file, Some(&view), &names, &None, false, &[]).len();
}
let each = started.elapsed() / rounds;
eprintln!(
"PROBE {name}: {} bytes, {tokens} tokens ({keywords} keyword kinds, {fetched} need a text fetch), \
{drawn} tokens drawn | view {view_time:?}, classification {classify_time:?}, \
{each:?} per encode",
text.len()
);
}
fn by_offset(
text: &str,
tokens: &[lsp_types::SemanticToken],
) -> std::collections::HashMap<usize, u32> {
let mut line_starts = vec![0usize];
for (at, ch) in text.char_indices() {
if ch == '\n' {
line_starts.push(at + 1);
}
}
let mut out = std::collections::HashMap::new();
let mut line = 0usize;
let mut character = 0u32;
for token in tokens {
line += token.delta_line as usize;
character = if token.delta_line == 0 {
character + token.delta_start
} else {
token.delta_start
};
let Some(&line_start) = line_starts.get(line) else {
continue;
};
let line_end = line_starts
.get(line + 1)
.copied()
.unwrap_or(text.len())
.min(text.len());
let column = text
.get(line_start..line_end)
.map(|body| {
let mut utf16 = 0u32;
let mut bytes = 0usize;
for ch in body.chars() {
if utf16 >= character {
break;
}
utf16 += ch.len_utf16() as u32;
bytes += ch.len_utf8();
}
bytes
})
.unwrap_or(0);
out.insert(line_start + column, token.token_type);
}
out
}
#[test]
fn a_constructors_name_is_a_type_and_requires_is_a_keyword() {
let text = "template <class Err>\n\
class ExpectedVoid {\n\
public:\n\
\x20 constexpr ExpectedVoid() {}\n\
\x20 ExpectedVoid(const ExpectedVoid& other) = delete;\n\
\x20 constexpr ~ExpectedVoid() requires is_trivially_destructible_v<Err> = default;\n\
};\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
let a_type = index_of(TypeKind::Type);
let a_keyword = index_of(TypeKind::Keyword);
let a_parameter = index_of(TypeKind::Parameter);
let a_type_parameter = index_of(TypeKind::TypeParameter);
let written: Vec<usize> = text
.match_indices("ExpectedVoid")
.map(|(at, _)| at)
.collect();
assert_eq!(
written.len(),
5,
"the class name, three constructors/destructors, and the parameter's type: {written:?}"
);
for at in written {
assert_eq!(
at_type.get(&at),
Some(&a_type),
"`ExpectedVoid` at {at} is the class's name — a constructor and a parameter's type are the class too"
);
}
let other = text.find("other").expect("the fixture names it");
assert_eq!(
at_type.get(&other),
Some(&a_parameter),
"the parameter's own name is a parameter"
);
assert_eq!(
at_type.get(&text.find("Err").expect("the fixture names it")),
Some(&a_type_parameter)
);
let requires = text.find("requires").expect("the fixture writes it");
assert_eq!(
at_type.get(&requires),
Some(&a_keyword),
"`requires` is a keyword to a reader even though the lexer leaves it an identifier"
);
}
#[test]
fn a_concept_is_a_keyword_and_its_name_is_a_type() {
let text = "template <class T>\nconcept UnaryCopyable = requires(T t) { t.f(); };\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
let a_keyword = index_of(TypeKind::Keyword);
let a_type = index_of(TypeKind::Type);
assert_eq!(
at_type.get(&text.find("concept").expect("the fixture writes it")),
Some(&a_keyword),
"`concept` introduces a concept, it is not one"
);
assert_eq!(
at_type.get(&text.find("UnaryCopyable").expect("the fixture writes it")),
Some(&a_type),
"the name a `concept` declares is the concept, and a theme colours that as a type"
);
assert_eq!(
at_type.get(&text.find("requires").expect("the fixture writes it")),
Some(&a_keyword)
);
}
fn name(start: usize, length: usize, kind: NameKind, declaration: bool) -> Name {
Name {
range: SourceRange::new(start, length),
kind,
declaration,
modifiers: Vec::new(),
provenance: cpp_code_analysis::semantic::Provenance::DeclaredHere,
}
}
#[test]
fn the_encoding_is_a_walk_a_client_can_rebuild() {
let text = "int alpha;\nint beta;\n int gamma;\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let at = |spelling: &str| {
let start = text.find(spelling).expect("the fixture writes it");
name(start, spelling.len(), NameKind::Variable, true)
};
let names = vec![at("alpha"), at("beta"), at("gamma")];
let tokens = encode(file, None, &names, &None, false, &[]);
let mut line = 0u32;
let mut character = 0u32;
let rebuilt: Vec<(u32, u32, u32)> = tokens
.iter()
.map(|token| {
if token.delta_line == 0 {
character += token.delta_start;
} else {
line += token.delta_line;
character = token.delta_start;
}
(line, character, token.length)
})
.collect();
assert_eq!(
rebuilt,
vec![(0, 4, 5), (1, 4, 4), (2, 8, 5)],
"the client's own reconstruction: {tokens:?}"
);
assert!(
tokens
.iter()
.all(|token| token.token_type == index_of(TypeKind::Variable)),
"every one is a variable"
);
assert!(
tokens.iter().all(|token| token.token_modifiers_bitset == 1),
"and a declaration"
);
}
#[test]
fn a_kind_the_client_cannot_draw_is_skipped_without_moving_the_others() {
let text = "int alpha;\nnamespace ns { }\nint beta;\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let variables = || {
["alpha", "beta"].map(|spelling| {
let start = text.find(spelling).expect("the fixture writes it");
name(start, spelling.len(), NameKind::Variable, true)
})
};
let namespace = {
let start = text.find("namespace").expect("the fixture writes it");
name(start, "namespace".len(), NameKind::Namespace, true)
};
let mut names = variables().to_vec();
names.insert(1, namespace);
assert_eq!(encode(file, None, &names, &None, false, &[]).len(), 3);
let drawable = Some(vec![SemanticTokenType::VARIABLE]);
let tokens = encode(file, None, &names, &drawable, false, &[]);
assert_eq!(tokens.len(), 2);
assert_eq!((tokens[0].delta_line, tokens[0].delta_start), (0, 4));
assert_eq!(
(tokens[1].delta_line, tokens[1].delta_start),
(2, 4),
"the namespace's token is not a step: {tokens:?}"
);
}
#[test]
fn a_token_past_the_end_of_the_file_is_dropped() {
let text = "int alpha;\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let names = vec![
name(4, 5, NameKind::Variable, true),
name(text.len() + 40, 4, NameKind::Variable, true),
];
assert_eq!(encode(file, None, &names, &None, false, &[]).len(), 1);
}
#[test]
fn the_advertised_legend_is_the_list_the_numbers_index() {
let advertised = legend();
for kind in TypeKind::ALL {
let position = kind.to_u32() as usize;
assert!(
position < advertised.token_types.len(),
"{kind:?} is numbered {position}, past the legend's {} entries",
advertised.token_types.len()
);
assert_eq!(
advertised.token_types[position],
kind.to_semantic_token_type(),
"the type at {position} is the one that number means"
);
}
assert_eq!(advertised.token_types.len(), TypeKind::ALL.len());
assert_eq!(advertised.token_modifiers, Modifier::all_modifiers());
assert_eq!(advertised.token_modifiers.len(), Modifier::ALL.len());
for kind in [
NameKind::Namespace,
NameKind::Type,
NameKind::TypeParameter,
NameKind::EnumMember,
NameKind::Function,
NameKind::Method,
NameKind::Variable,
NameKind::Parameter,
NameKind::Macro,
] {
assert!(
type_of_a_name_kind(kind).is_some(),
"{kind:?} is classified by the analysis and has no colour"
);
}
}
#[test]
fn every_answer_is_ordered_and_non_overlapping() {
let text = "struct Widget { int size; };\n\
Widget operator+(const Widget& left, const Widget& right);\n\
int f(Widget& w) { return w.size; }\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
assert!(!tokens.is_empty(), "the fixture must produce tokens");
let mut line = 0u32;
let mut character = 0u32;
let mut previous_end: Option<(u32, u32)> = None;
for (at, token) in tokens.iter().enumerate() {
line += token.delta_line;
character = if token.delta_line == 0 {
character + token.delta_start
} else {
token.delta_start
};
assert!(
token.length > 0,
"token {at} has no length, which the protocol does not allow: {token:?}"
);
assert!(
token.token_type < TypeKind::ALL.len() as u32,
"token {at} names a type past the legend: {token:?}"
);
if let Some((previous_line, previous_column)) = previous_end {
let ordered =
line > previous_line || (line == previous_line && character >= previous_column);
assert!(
ordered,
"token {at} at {line}:{character} comes before the previous one ends at \
{previous_line}:{previous_column}"
);
}
previous_end = Some((line, character + token.length));
}
}
#[test]
fn a_spanning_token_is_split_for_a_client_that_cannot_draw_one() {
let text = "int a;\n/* one\ntwo\nthree */\nint b;\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let start = text.find("/*").expect("the fixture writes it");
let end = text.find("*/").expect("the fixture writes it") + 2;
let mut whole = Builder::new(file, None, true);
assert!(whole.push_range(start, end, TypeKind::Property, Modifier::none()));
let whole = whole.build();
assert_eq!(whole.len(), 1, "one claim, one token: {whole:?}");
assert_eq!(
whole[0].delta_line, 1,
"the comment begins on the second line of the file: {whole:?}"
);
let mut split = Builder::new(file, None, false);
assert!(split.push_range(start, end, TypeKind::Property, Modifier::none()));
let split = split.build();
assert_eq!(
split.len(),
3,
"the comment covers three lines and each gets its own token: {split:?}"
);
assert_eq!(
(split[0].delta_line, split[0].delta_start),
(1, 0),
"the first piece is where the claim begins"
);
for (at, token) in split.iter().enumerate().skip(1) {
assert_eq!(
(token.delta_line, token.delta_start),
(1, 0),
"piece {at} begins at the start of the next line"
);
}
assert_eq!(split[0].length, 6, "`/* one` is six columns");
assert_eq!(split[1].length, 3, "`two` is the whole of its line");
assert_eq!(split[2].length, 8, "`three */` ends at column 8");
}
#[test]
fn a_doxygen_command_is_drawn_with_its_introducer() {
let text = "/// @brief The width.\n\
/// @param x the x coordinate\n\
/// \\returns the width\n\
int width(int x);\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
let a_command = index_of(TypeKind::DocCommand);
for introducer in ["@brief", "@param", "\\returns"] {
let at = text.find(introducer).expect("the fixture writes it");
assert_eq!(
at_type.get(&at),
Some(&a_command),
"`{introducer}` is a command, introducer and all: {at_type:?}"
);
let after = at + introducer.len();
assert!(
at_type.get(&after).is_none_or(|kind| *kind != a_command),
"the command's token ends after `{introducer}`"
);
}
let prose = text.find("coordinate").expect("the fixture writes it");
assert_ne!(
at_type.get(&prose),
Some(&a_command),
"a word of the description is not a command"
);
}
#[test]
fn a_module_declaration_is_drawn_and_a_variable_called_import_is_not() {
let text = "export module math;\nimport math;\nint import = 1;\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_offset = by_offset(text, &tokens);
let at = |needle: &str| {
let start = text.find(needle).expect("the fixture writes it");
at_offset.get(&start).copied()
};
assert_eq!(
at("export"),
Some(index_of(TypeKind::Keyword)),
"`export` in `export module math;`"
);
assert_eq!(
at("module"),
Some(index_of(TypeKind::Keyword)),
"the module keyword"
);
assert_eq!(
at("import"),
Some(index_of(TypeKind::Keyword)),
"and the import keyword"
);
assert_eq!(
at("math"),
Some(index_of(TypeKind::Namespace)),
"the module name is a scope name, which is how a theme draws a qualified-name root"
);
let variable = text.rfind("import").expect("the fixture writes it");
assert_eq!(
at_offset.get(&variable).copied(),
Some(index_of(TypeKind::Variable)),
"a variable called `import` is a name, not a keyword: {:?}",
at_offset
);
}
#[test]
fn a_doc_command_is_drawn_where_the_file_wrote_it() {
let text = "\
/// @param value the value
/// @return nothing
// a plain comment with @param in it
int f(int value);
// mail me at someone@example.com
";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
for (offset, kind) in &at_type {
let word = &text[*offset..(*offset + 12).min(text.len())];
println!("DOC {offset} -> {kind:?} {word:?}");
}
for word in ["@param", "@return"] {
let at = text.find(word).expect("the fixture writes it");
assert_eq!(
at_type.get(&at),
Some(&index_of(TypeKind::DocCommand)),
"`{word}` is a command this parser knows, and it is drawn with its introducer"
);
}
let plain = text.find("// a plain comment").expect("the fixture")
+ "// a plain comment with ".len();
assert_eq!(
at_type.get(&plain),
None,
"a `//` comment is a comment, not documentation"
);
let mail = text.find("someone@example.com").expect("the fixture") + "someone".len();
assert_eq!(
at_type.get(&mail),
None,
"an `@` in prose is not a command — `example` is not one either"
);
}
#[test]
fn a_declaration_specifier_is_drawn_as_a_modifier() {
let text = "struct Marker {\n\
\x20 explicit Marker(int pos);\n\
\x20 virtual ~Marker();\n\
\x20 constexpr int size() const noexcept;\n\
};\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
for word in ["explicit", "virtual", "constexpr", "const", "noexcept"] {
let at = text[..text.find(word).expect("the fixture writes it")].len();
assert_eq!(
at_type.get(&at),
Some(&index_of(TypeKind::Modifier)),
"`{word}` is a declaration specifier: {at_type:?}"
);
}
}
#[test]
fn a_keyword_is_drawn_as_what_it_is() {
let text = "constexpr bool ready() const { this->x = nullptr; return true; }\n\
auto y = 1; const int z = 2;\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at_type = by_offset(text, &tokens);
for (word, expected, why) in [
("bool", TypeKind::Type, "`bool` names a type"),
(
"constexpr",
TypeKind::Modifier,
"a qualifier is a property of the declaration",
),
(
"this",
TypeKind::Variable,
"`this` is a value, and the report asks for it to match `bool`'s *kind* — a type it is not",
),
("nullptr", TypeKind::Variable, "and so is `nullptr`"),
(
"true",
TypeKind::Variable,
"`true` is the literal, not the type it shares a token kind with",
),
(
"return",
TypeKind::Keyword,
"`return` is about the syntax, which is what `keyword` means",
),
("const", TypeKind::Modifier, "the trailing `const` too"),
(
"auto",
TypeKind::Modifier,
"a declaration's placeholder is a property of the declaration, not a statement keyword",
),
(
"const int",
TypeKind::Modifier,
"and the leading `const` of `const int z`",
),
] {
let at = text[..text.find(word).expect("the fixture writes it")].len();
assert_eq!(
at_type.get(&at),
Some(&index_of(expected)),
"`{word}`: {why}"
);
}
let bool_at = text.find("bool").expect("the fixture writes it");
let true_at = text.rfind("true").expect("the fixture writes it");
assert_ne!(
at_type.get(&bool_at),
at_type.get(&true_at),
"the type and its literal are not one colour"
);
}
#[test]
fn a_keyword_is_coloured_and_nothing_overlaps() {
let text = "struct Widget { int size; };\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let tokens = encode(file, Some(&view), &names, &None, false, &[]);
let at = |word: &str| text.find(word).expect("the fixture writes it") as u32;
let mut by_column: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
let mut line = 0u32;
let mut character = 0u32;
for token in &tokens {
line += token.delta_line;
character = if token.delta_line == 0 {
character + token.delta_start
} else {
token.delta_start
};
if line == 0 {
by_column.insert(character, token.token_type);
}
}
let keyword = index_of(TypeKind::Keyword);
let a_type = index_of(TypeKind::Type);
assert_eq!(
by_column.get(&at("struct")),
Some(&keyword),
"`struct` introduces a type, it is not one"
);
assert_eq!(
by_column.get(&at("int")),
Some(&a_type),
"`int` names a type"
);
assert_eq!(
by_column.get(&at("Widget")),
Some(&a_type),
"and so does the class it names"
);
assert_ne!(
keyword, a_type,
"the two are different colours, which is the point"
);
let mut spans = Vec::new();
let (mut line, mut character) = (0u32, 0u32);
for token in &tokens {
line += token.delta_line;
character = if token.delta_line == 0 {
character + token.delta_start
} else {
token.delta_start
};
spans.push((line, character, token.length));
}
assert!(
!spans.iter().any(|(_, _, length)| *length == 0),
"a zero-length token is invalid in this encoding: {spans:?}"
);
assert_eq!(spans[0], (0, 0, 6));
assert_eq!(
spans
.iter()
.find(|(line, character, _)| *line == 0 && *character == 7)
.expect("`Widget` is coloured at column 7")
.2,
6
);
for pair in spans.windows(2) {
let (line, character, length) = pair[0];
let (next_line, next_character, _) = pair[1];
assert!(
next_line > line || next_character >= character + length,
"tokens overlap: {pair:?}"
);
}
}
#[test]
fn a_client_that_supports_none_of_our_types_gets_nothing_to_draw() {
let text = "int alpha;\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let names = vec![name(4, 5, NameKind::Variable, true)];
let drawable = Some(vec![SemanticTokenType::COMMENT, SemanticTokenType::STRING]);
assert!(encode(file, None, &names, &drawable, false, &[]).is_empty());
}
fn index_of(kind: TypeKind) -> u32 {
kind.to_u32()
}
#[test]
fn a_format_string_is_drawn_piece_by_piece() {
let text = "\
void f(int y, double d, char* s) {
printf(\"%-8.2f|%d\", d, y);
scanf(\"%3d\", &y);
std::string message = std::format(\"值 = {:.3f}\", d);
}
";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let formats = session.format_literals(&view, &Default::default());
assert_eq!(formats.len(), 3, "three format strings: {formats:#?}");
let tokens = encode(file, Some(&view), &names, &None, false, &formats);
let at_type = by_offset(text, &tokens);
let printf = text.find(r#""%-8.2f|%d""#).expect("the fixture writes it");
for (offset, expected, why) in [
(
printf + 1,
TypeKind::FormatSpecifier,
"`%-8.2` is the field's syntax",
),
(
printf + 6,
TypeKind::FormatConversion,
"`f` is its conversion",
),
(
printf + 8,
TypeKind::FormatSpecifier,
"the second conversion's `%`",
),
(printf + 9, TypeKind::FormatConversion, "and its `d`"),
] {
assert_eq!(at_type.get(&offset), Some(&index_of(expected)), "{why}");
}
assert_eq!(
at_type.get(&(printf + 7)),
None,
"the `|` between two conversions is text, and is not painted"
);
let scanf = text.find(r#""%3d""#).expect("the fixture writes it");
assert_eq!(
at_type.get(&(scanf + 1)),
Some(&index_of(TypeKind::FormatSpecifier))
);
assert_eq!(
at_type.get(&(scanf + 3)),
Some(&index_of(TypeKind::FormatConversion))
);
let format = text
.find(r#""值 = {:.3f}""#)
.expect("the fixture writes it");
let field = format + 1 + "值 = ".len();
for (offset, expected, why) in [
(field, TypeKind::FormatSpecifier, "`{:.3`"),
(field + 4, TypeKind::FormatConversion, "`f`"),
(field + 5, TypeKind::FormatSpecifier, "`}`"),
] {
assert_eq!(at_type.get(&offset), Some(&index_of(expected)), "{why}");
}
let escaped = "\
void g() {
printf(\"100%%\\n\");
}
";
let (session, path) = a_held_file(escaped);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let formats = session.format_literals(&view, &Default::default());
let tokens = encode(file, Some(&view), &names, &None, false, &formats);
let at_type = by_offset(escaped, &tokens);
let literal = escaped.find(r#""100%%\n""#).expect("the fixture writes it");
assert_eq!(
at_type.get(&(literal + 4)),
Some(&index_of(TypeKind::FormatEscape)),
"`%%` is an escape: {at_type:?}"
);
}
#[test]
fn a_client_without_the_custom_types_is_answered_with_their_fallbacks() {
let text = "\
void f(int width, double d) {
printf(\"%*d|%%\", width, 1);
}
";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let formats = session.format_literals(&view, &Default::default());
let standard = Some(vec![
SemanticTokenType::REGEXP,
SemanticTokenType::PARAMETER,
SemanticTokenType::KEYWORD,
SemanticTokenType::STRING,
SemanticTokenType::VARIABLE,
]);
let tokens = encode(file, Some(&view), &names, &standard, false, &formats);
let at_type = by_offset(text, &tokens);
let literal = text.find(r#""%*d|%%""#).expect("the fixture writes it");
assert_eq!(
at_type.get(&(literal + 1)),
Some(&index_of(TypeKind::Regexp)),
"the field's syntax falls back to `regexp`, which is the pattern language it is"
);
assert_eq!(
at_type.get(&(literal + 2)),
Some(&index_of(TypeKind::Parameter)),
"an argument reference falls back to `parameter`"
);
assert_eq!(
at_type.get(&(literal + 3)),
Some(&index_of(TypeKind::Keyword)),
"a conversion falls back to `keyword`"
);
assert_eq!(
at_type.get(&(literal + 5)),
None,
"an escape has no fallback: the literal already has the colour it would be given"
);
}
#[test]
fn a_vscode_client_is_sent_the_custom_format_types() {
let standard = vec![
lsp_types::SemanticTokenType::REGEXP,
lsp_types::SemanticTokenType::KEYWORD,
];
let drawn = extended_with_the_custom_types(
Some(standard.clone()),
crate::context::ClientId::VSCode,
)
.expect("a client that named its types keeps a list");
for name in [
"formatSpecifier",
"formatArgument",
"formatConversion",
"formatEscape",
] {
assert!(
drawn
.iter()
.any(|kind| *kind == lsp_types::SemanticTokenType::new(name)),
"{name} is missing from {drawn:?}"
);
}
let kept = extended_with_the_custom_types(
Some(standard.clone()),
crate::context::ClientId::Neovim,
)
.expect("a client that named its types keeps a list");
assert_eq!(
kept, standard,
"no custom type is added for a client whose theme has no name for it"
);
assert!(
extended_with_the_custom_types(None, crate::context::ClientId::VSCode).is_none(),
"a client with no capability list gets the whole legend, custom types included"
);
}
#[test]
fn a_project_that_turned_the_colouring_off_gets_no_format_tokens() {
let text = "void f() { printf(\"%d\", 1); }\n";
let (session, path) = a_held_file(text);
let view = session.view_of_the_file(&path).expect("a view");
let file = session.files().held(&path).expect("the file is held");
let names = session.classified_names(&view);
let off = cpp_code_analysis::format::FormatConfig {
std_format: false,
printf: false,
scanf: false,
functions: Vec::new(),
};
assert!(off.is_off());
assert!(
session.format_literals(&view, &off).is_empty(),
"the walk does not even look at a token"
);
let without = encode(file, Some(&view), &names, &None, false, &[]);
let literal = text.find(r#""%d""#).expect("the fixture writes it");
let at_type = by_offset(text, &without);
assert_eq!(
at_type.get(&(literal + 1)),
None,
"no token inside the literal: {at_type:?}"
);
}
}