Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 214 additions & 6 deletions crates/squawk_ide/src/semantic_tokens.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use rowan::{NodeOrToken, TextRange};
use rustc_hash::FxHashSet;
use salsa::Database as Db;
use squawk_syntax::{
SyntaxElement, SyntaxKind,
SyntaxElement, SyntaxKind, SyntaxNode,
ast::{self, AstNode},
};

use crate::db::{File, parse};
use crate::db::{EmbeddedFile, File, FileId, embedded_body, parse};
use crate::file::InFile;
use crate::goto_definition::goto_definition;
use crate::location::LocationKind;
Expand Down Expand Up @@ -270,6 +271,26 @@ impl TryFrom<LocationKind> for SemanticTokenType {
}
}

fn embedded_token_type(kind: SyntaxKind) -> Option<SemanticTokenType> {
match kind {
SyntaxKind::TRUE_KW | SyntaxKind::FALSE_KW => Some(SemanticTokenType::Bool),
SyntaxKind::BIT_STRING
| SyntaxKind::BYTE_STRING
| SyntaxKind::DOLLAR_QUOTED_STRING
| SyntaxKind::ESC_STRING
| SyntaxKind::NATIONAL_STRING
| SyntaxKind::STRING
| SyntaxKind::UNICODE_ESC_STRING => Some(SemanticTokenType::String),
SyntaxKind::INT_NUMBER | SyntaxKind::NUMERIC_NUMBER => Some(SemanticTokenType::Number),
SyntaxKind::COMMENT => Some(SemanticTokenType::Comment),
SyntaxKind::IDENT => Some(SemanticTokenType::NameRef),
_ if kind.is_keyword() => Some(SemanticTokenType::Keyword),
_ if kind.is_punctuation() => Some(SemanticTokenType::Punctuation),
_ if kind.is_operator() => Some(SemanticTokenType::Operator),
_ => None,
}
}

fn token_type_for_node<T: AstNode>(db: &dyn Db, node: InFile<&T>) -> Option<SemanticTokenType> {
let offset = node.value.syntax().text_range().start();
let location = goto_definition(db, InFile::new(node.file_id, offset))
Expand All @@ -282,6 +303,7 @@ fn token_type_for_node<T: AstNode>(db: &dyn Db, node: InFile<&T>) -> Option<Sema
#[derive(Default)]
struct SemanticTokenBuilder {
tokens: Vec<SemanticToken>,
highlighted_ranges: FxHashSet<TextRange>,
}

impl SemanticTokenBuilder {
Expand All @@ -299,13 +321,22 @@ impl SemanticTokenBuilder {
self.push_token(syntax_element, SemanticTokenType::Type);
}

fn contains_range(&self, range: TextRange) -> bool {
self.highlighted_ranges.contains(&range)
}

fn push_token(&mut self, syntax_element: SyntaxElement, token_type: SemanticTokenType) {
self.tokens.push(SemanticToken {
self.push(SemanticToken {
range: syntax_element.text_range(),
token_type,
modifiers: None,
});
}

fn push(&mut self, token: SemanticToken) {
self.highlighted_ranges.insert(token.range);
self.tokens.push(token);
}
}

#[salsa::tracked]
Expand Down Expand Up @@ -334,7 +365,17 @@ pub fn semantic_tokens(
};

let mut out = SemanticTokenBuilder::default();
highlight(db, file.into(), &root, range_to_highlight, &mut out);
out.build()
}

fn highlight(
db: &dyn Db,
file: FileId,
root: &SyntaxNode,
range_to_highlight: TextRange,
out: &mut SemanticTokenBuilder,
) {
// Taken from: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/ide/src/syntax_highlighting.rs#L267C21-L267C21
let preorder = root.preorder_with_tokens();
for event in preorder {
Expand All @@ -358,11 +399,15 @@ pub fn semantic_tokens(
}

if let Some(ty) = ast::Type::cast(node.clone()) {
highlight_type(&mut out, ty);
highlight_type(out, ty);
}

if let Some(mode) = ast::ParamMode::cast(node.clone()) {
highlight_param_mode(&mut out, mode);
highlight_param_mode(out, mode);
}

if let Some(literal) = ast::Literal::cast(node.clone()) {
highlight_embedded(db, file, literal, range_to_highlight, out);
}

// Cleanup various operators that the textmate grammar
Expand Down Expand Up @@ -390,13 +435,50 @@ pub fn semantic_tokens(
}
if token.kind() == SyntaxKind::POSITIONAL_PARAM {
out.push_token(token.into(), SemanticTokenType::PositionalParam);
} else if matches!(file, FileId::Embedded(_))
&& !out.contains_range(token.text_range())
&& let Some(token_type) = embedded_token_type(token.kind())
{
out.push_token(token.into(), token_type);
}
}
Leave(_) => {}
}
}
}

out.build()
fn highlight_embedded(
db: &dyn Db,
file: FileId,
literal: ast::Literal,
range_to_highlight: TextRange,
out: &mut SemanticTokenBuilder,
) {
let Some(parent) = literal.syntax().parent() else {
return;
};
if !ast::AsDefinition::can_cast(parent.kind()) {
return;
}
let embedded = EmbeddedFile::new(db, file, literal.syntax().text_range());
let Some(body) = embedded_body(db, embedded) else {
return;
};
let root = body.syntax();
let mut embedded_out = SemanticTokenBuilder::default();
highlight(
db,
FileId::Embedded(embedded),
&root,
root.text_range(),
&mut embedded_out,
);
for token in embedded_out.tokens {
let range = body.source_range(token.range);
if range_to_highlight.intersect(range).is_some() {
out.push(SemanticToken { range, ..token });
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -457,6 +539,10 @@ language sql;
"d" @ 103..104: Parameter
"int" @ 105..108: Type
"int" @ 121..124: Type
"select" @ 129..135: Keyword
"$1" @ 136..138: PositionalParam
"+" @ 139..140: Operator
"$2" @ 141..143: PositionalParam
"#);
}

Expand Down Expand Up @@ -682,6 +768,8 @@ language sql;
"f" @ 17..18: Function
"setof" @ 29..34: Type
"int" @ 35..38: Type
"select" @ 43..49: Keyword
"1" @ 50..51: Number
"#);
}

Expand Down Expand Up @@ -808,6 +896,8 @@ select f();
), @r#"
"f" @ 17..18: Function
"int" @ 29..32: Type
"select" @ 37..43: Keyword
"1" @ 44..45: Number
"f" @ 68..69: Function
"#);
}
Expand All @@ -827,6 +917,8 @@ select b(t), t.b from t;
"b" @ 40..41: Function
"t" @ 42..43: Type
"int" @ 53..56: Type
"select" @ 61..67: Keyword
"1" @ 68..69: Number
"b" @ 92..93: Function
"t" @ 94..95: Table
"t" @ 98..99: Table
Expand All @@ -851,6 +943,8 @@ create policy p on t
"x" @ 40..41: Function
"t" @ 42..43: Type
"int" @ 53..56: Type
"select" @ 61..67: Keyword
"1" @ 68..69: Number
"t" @ 104..105: Table
"t" @ 120..121: Table
"x" @ 122..123: Function
Expand Down Expand Up @@ -907,4 +1001,118 @@ select s.t.a from s.t;
"t" @ 63..64: Table
"#);
}

#[test]
fn create_function_embedded_sql() {
assert_snapshot!(semantic_tokens(
"
create table t(a int);
create function f(x int) returns int
as $$ select a from t where a = x and a = $1 $$
language sql;
",
), @r#"
"t" @ 14..15: Table
"a" @ 16..17: Column
"int" @ 18..21: Type
"f" @ 40..41: Function
"x" @ 42..43: Parameter
"int" @ 44..47: Type
"int" @ 57..60: Type
"select" @ 67..73: Keyword
"a" @ 74..75: Column
"from" @ 76..80: Keyword
"t" @ 81..82: Table
"where" @ 83..88: Keyword
"a" @ 89..90: Column
"=" @ 91..92: Operator
"x" @ 93..94: Parameter
"and" @ 95..98: Keyword
"a" @ 99..100: Column
"=" @ 101..102: Operator
"$1" @ 103..105: PositionalParam
"#);
}

#[test]
fn create_procedure_embedded_sql_escaped() {
assert_snapshot!(semantic_tokens(
"
create table t(a text);
create procedure p()
language sql
as 'insert into t(a) select ''x'' from t';
",
), @r#"
"t" @ 14..15: Table
"a" @ 16..17: Column
"text" @ 18..22: Type
"p" @ 42..43: Function
"insert" @ 63..69: Keyword
"into" @ 70..74: Keyword
"t" @ 75..76: Table
"(" @ 76..77: Punctuation
"a" @ 77..78: Column
")" @ 78..79: Punctuation
"select" @ 80..86: Keyword
"''x''" @ 87..92: String
"from" @ 93..97: Keyword
"t" @ 98..99: Table
"#);
}

#[test]
fn create_function_embedded_non_sql() {
assert_snapshot!(semantic_tokens(
"
create table t(a int);
create function f() returns int
as $$ select a from t $$
language plpgsql;
",
), @r#"
"t" @ 14..15: Table
"a" @ 16..17: Column
"int" @ 18..21: Type
"f" @ 40..41: Function
"int" @ 52..55: Type
"#);
}

#[test]
fn embedded_sql_lexical_tokens() {
assert_snapshot!(semantic_tokens(
"
create function f() returns int language sql as $body$
-- embedded
select unknown_name, 42, 1.5, true, false, null, 'text', E'escape', B'01', X'ff';
$body$;
",
), @r#"
"f" @ 17..18: Function
"int" @ 29..32: Type
"-- embedded" @ 56..67: Comment
"select" @ 68..74: Keyword
"unknown_name" @ 75..87: NameRef
"," @ 87..88: Punctuation
"42" @ 89..91: Number
"," @ 91..92: Punctuation
"1.5" @ 93..96: Number
"," @ 96..97: Punctuation
"true" @ 98..102: Bool
"," @ 102..103: Punctuation
"false" @ 104..109: Bool
"," @ 109..110: Punctuation
"null" @ 111..115: Keyword
"," @ 115..116: Punctuation
"'text'" @ 117..123: String
"," @ 123..124: Punctuation
"E'escape'" @ 125..134: String
"," @ 134..135: Punctuation
"B'01'" @ 136..141: String
"," @ 141..142: Punctuation
"X'ff'" @ 143..148: String
";" @ 148..149: Punctuation
"#);
}
}
Loading
Loading