Skip to content

Commit cc6c58e

Browse files
authored
ide: semantic syntax highlighting (#1048)
We're not really doing anything semantic yet, that will come in a follow up PR. We need to rework goto def so we can easily get the kind when we goto def a given name/name_ref. With this change, we now properly highlight: ```sql select 1 and; select 2 select; ``` `and` and `select` are both column labels in these cases, not keywords.
1 parent f636532 commit cc6c58e

13 files changed

Lines changed: 382 additions & 17 deletions

File tree

crates/squawk_ide/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod offsets;
1818
mod quote;
1919
mod resolve;
2020
mod scope;
21+
pub mod semantic_tokens;
2122
mod symbols;
2223
#[cfg(test)]
2324
pub mod test_utils;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use rowan::{NodeOrToken, TextRange};
2+
use salsa::Database as Db;
3+
use squawk_syntax::{
4+
SyntaxKind,
5+
ast::{self, AstNode},
6+
};
7+
8+
use crate::db::{File, parse};
9+
10+
/// A semantic token with its position and classification.
11+
#[derive(Debug, Clone, PartialEq, Eq)]
12+
pub struct SemanticToken {
13+
pub range: TextRange,
14+
pub token_type: SemanticTokenType,
15+
pub modifiers: Option<SemanticTokenModifier>,
16+
}
17+
18+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
19+
#[repr(u8)]
20+
pub enum SemanticTokenModifier {
21+
Definition = 0,
22+
Readonly,
23+
Documentation,
24+
}
25+
26+
/// Semantic token types supported by the language server.
27+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28+
pub enum SemanticTokenType {
29+
Keyword,
30+
String,
31+
Bool,
32+
Number,
33+
Function,
34+
Operator,
35+
Punctuation,
36+
Name,
37+
NameRef,
38+
Comment,
39+
Type,
40+
Parameter,
41+
PositionalParam,
42+
}
43+
44+
#[salsa::tracked]
45+
pub fn semantic_tokens(
46+
db: &dyn Db,
47+
file: File,
48+
range_to_highlight: Option<TextRange>,
49+
) -> Vec<SemanticToken> {
50+
let parse = parse(db, file);
51+
let tree = parse.tree();
52+
let root = tree.syntax();
53+
54+
// Determine the root based on the given range.
55+
let (root, range_to_highlight) = {
56+
let source_file = root;
57+
match range_to_highlight {
58+
Some(range) => {
59+
let node = match source_file.covering_element(range) {
60+
NodeOrToken::Node(it) => it,
61+
NodeOrToken::Token(it) => it.parent().unwrap_or_else(|| source_file.clone()),
62+
};
63+
(node, range)
64+
}
65+
None => (source_file.clone(), source_file.text_range()),
66+
}
67+
};
68+
69+
let mut out = vec![];
70+
71+
// Taken from: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/ide/src/syntax_highlighting.rs#L267C21-L267C21
72+
let preorder = root.preorder_with_tokens();
73+
for event in preorder {
74+
use rowan::WalkEvent::{Enter, Leave};
75+
76+
let range = match &event {
77+
Enter(it) | Leave(it) => it.text_range(),
78+
};
79+
80+
// Element outside of the viewport, no need to highlight
81+
if range_to_highlight.intersect(range).is_none() {
82+
continue;
83+
}
84+
85+
match event {
86+
Enter(NodeOrToken::Node(node)) => {
87+
if let Some(target) = ast::Target::cast(node)
88+
&& let Some(as_name) = target.as_name()
89+
&& let Some(name) = as_name.name()
90+
{
91+
let range = name.syntax().text_range();
92+
out.push(SemanticToken {
93+
range,
94+
token_type: SemanticTokenType::Name,
95+
modifiers: None,
96+
});
97+
};
98+
}
99+
Enter(NodeOrToken::Token(token)) => {
100+
if token.kind() == SyntaxKind::WHITESPACE {
101+
continue;
102+
}
103+
if token.kind() == SyntaxKind::POSITIONAL_PARAM {
104+
out.push(SemanticToken {
105+
range: token.text_range(),
106+
token_type: SemanticTokenType::PositionalParam,
107+
modifiers: None,
108+
})
109+
}
110+
}
111+
Leave(_) => {}
112+
}
113+
}
114+
out
115+
}
116+
117+
#[cfg(test)]
118+
mod test {
119+
use crate::db::{Database, File};
120+
use insta::assert_snapshot;
121+
use std::fmt::Write;
122+
123+
fn semantic_tokens(sql: &str) -> String {
124+
let db = Database::default();
125+
let file = File::new(&db, sql.to_string().into());
126+
let tokens = super::semantic_tokens(&db, file, None);
127+
128+
let mut result = String::new();
129+
for token in tokens {
130+
let start: usize = token.range.start().into();
131+
let end: usize = token.range.end().into();
132+
let token_text = &sql[start..end];
133+
// TODO: once we get modfifiers, we'll need to update this
134+
let modifiers_text = "";
135+
writeln!(
136+
result,
137+
"{:?} @ {}..{}: {:?}{}",
138+
token_text, start, end, token.token_type, modifiers_text
139+
)
140+
.unwrap();
141+
}
142+
result
143+
}
144+
145+
#[test]
146+
fn create_function() {
147+
assert_snapshot!(semantic_tokens("
148+
create function add(a int, b int) returns int
149+
as 'select $1 + $2'
150+
language sql;
151+
"), @"");
152+
}
153+
154+
#[test]
155+
fn select_keywords() {
156+
assert_snapshot!(semantic_tokens("
157+
select 1 and, 2 select;
158+
"), @r#"
159+
"and" @ 10..13: Name
160+
"select" @ 17..23: Name
161+
"#)
162+
}
163+
164+
#[test]
165+
fn positional_param() {
166+
assert_snapshot!(semantic_tokens("
167+
select $1, $2;
168+
"), @r#"
169+
"$1" @ 8..10: PositionalParam
170+
"$2" @ 12..14: PositionalParam
171+
"#)
172+
}
173+
}

crates/squawk_parser/src/grammar.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,7 +2511,9 @@ fn expr_bp(p: &mut Parser<'_>, bp: u8, r: &Restrictions) -> Option<CompletedMark
25112511
// could be start of `is distinct from`
25122512
&& !(p.at(IS_KW) && p.nth_at(1, DISTINCT_KW))
25132513
{
2514+
let m = p.start();
25142515
col_label(p);
2516+
m.complete(p, AS_NAME);
25152517
return Some(lhs);
25162518
}
25172519
if r.order_by_allowed && p.at(ORDER_KW) {

crates/squawk_parser/tests/snapshots/tests__select_ok.snap

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5780,8 +5780,9 @@ SOURCE_FILE
57805780
LITERAL
57815781
INT_NUMBER "1"
57825782
WHITESPACE " "
5783-
NAME
5784-
IS_KW "is"
5783+
AS_NAME
5784+
NAME
5785+
IS_KW "is"
57855786
SEMICOLON ";"
57865787
WHITESPACE "\n\n"
57875788
SELECT
@@ -5793,8 +5794,9 @@ SOURCE_FILE
57935794
LITERAL
57945795
INT_NUMBER "1"
57955796
WHITESPACE " "
5796-
NAME
5797-
AND_KW "and"
5797+
AS_NAME
5798+
NAME
5799+
AND_KW "and"
57985800
SEMICOLON ";"
57995801
WHITESPACE "\n\n"
58005802
SELECT
@@ -5806,8 +5808,9 @@ SOURCE_FILE
58065808
LITERAL
58075809
INT_NUMBER "1"
58085810
WHITESPACE " "
5809-
NAME
5810-
OR_KW "or"
5811+
AS_NAME
5812+
NAME
5813+
OR_KW "or"
58115814
SEMICOLON ";"
58125815
WHITESPACE "\n\n"
58135816
SELECT
@@ -5819,8 +5822,9 @@ SOURCE_FILE
58195822
LITERAL
58205823
INT_NUMBER "1"
58215824
WHITESPACE " "
5822-
NAME
5823-
COLLATE_KW "collate"
5825+
AS_NAME
5826+
NAME
5827+
COLLATE_KW "collate"
58245828
SEMICOLON ";"
58255829
WHITESPACE "\n\n"
58265830
SELECT

crates/squawk_server/src/global_state.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ use squawk_thread::TaskPool;
1616
use lsp_types::request::{
1717
CodeActionRequest, Completion, DocumentDiagnosticRequest, DocumentSymbolRequest,
1818
FoldingRangeRequest, GotoDefinition, HoverRequest, InlayHintRequest, References,
19-
SelectionRangeRequest, Shutdown,
19+
SelectionRangeRequest, SemanticTokensFullRequest, SemanticTokensRangeRequest, Shutdown,
2020
};
2121

2222
use crate::dispatch::{NotificationDispatcher, RequestDispatcher};
2323
use crate::handlers::{
2424
SyntaxTreeRequest, TokensRequest, handle_cancel, handle_code_action, handle_completion,
2525
handle_did_change, handle_did_close, handle_did_open, handle_document_diagnostic,
2626
handle_document_symbol, handle_folding_range, handle_goto_definition, handle_hover,
27-
handle_inlay_hints, handle_references, handle_selection_range, handle_shutdown,
28-
handle_syntax_tree, handle_tokens,
27+
handle_inlay_hints, handle_references, handle_selection_range, handle_semantic_tokens_full,
28+
handle_semantic_tokens_range, handle_shutdown, handle_syntax_tree, handle_tokens,
2929
};
3030

3131
type ReqQueue = lsp_server::ReqQueue<(String, Instant), ()>;
@@ -230,6 +230,8 @@ impl GlobalState {
230230
.on::<NO_RETRY, SyntaxTreeRequest>(handle_syntax_tree)
231231
.on::<NO_RETRY, TokensRequest>(handle_tokens)
232232
.on::<NO_RETRY, References>(handle_references)
233+
.on::<NO_RETRY, SemanticTokensFullRequest>(handle_semantic_tokens_full)
234+
.on::<NO_RETRY, SemanticTokensRangeRequest>(handle_semantic_tokens_range)
233235
.finish();
234236
}
235237
}

crates/squawk_server/src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod inlay_hints;
99
mod notifications;
1010
mod references;
1111
mod selection_range;
12+
mod semantic_tokens;
1213
mod shutdown;
1314
mod syntax_tree;
1415
mod tokens;
@@ -26,6 +27,7 @@ pub(crate) use notifications::{
2627
};
2728
pub(crate) use references::handle_references;
2829
pub(crate) use selection_range::handle_selection_range;
30+
pub(crate) use semantic_tokens::{handle_semantic_tokens_full, handle_semantic_tokens_range};
2931
pub(crate) use shutdown::handle_shutdown;
3032
pub(crate) use syntax_tree::{SyntaxTreeRequest, handle_syntax_tree};
3133
pub(crate) use tokens::{TokensRequest, handle_tokens};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use anyhow::Result;
2+
use lsp_types::{
3+
SemanticTokens, SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
4+
SemanticTokensResult,
5+
};
6+
7+
use squawk_ide::db::line_index;
8+
use squawk_ide::semantic_tokens::semantic_tokens;
9+
10+
use crate::global_state::Snapshot;
11+
use crate::lsp_utils;
12+
13+
pub(crate) fn handle_semantic_tokens_full(
14+
snapshot: &Snapshot,
15+
params: SemanticTokensParams,
16+
) -> Result<Option<SemanticTokensResult>> {
17+
let uri = params.text_document.uri;
18+
let db = snapshot.db();
19+
let file = snapshot.file(&uri).unwrap();
20+
let line_index = line_index(db, file);
21+
let text = file.content(db);
22+
let tokens = semantic_tokens(db, file, None);
23+
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
24+
result_id: None,
25+
data: lsp_utils::to_semantic_tokens(text, line_index, tokens),
26+
})))
27+
}
28+
29+
pub(crate) fn handle_semantic_tokens_range(
30+
snapshot: &Snapshot,
31+
params: SemanticTokensRangeParams,
32+
) -> Result<Option<SemanticTokensRangeResult>> {
33+
let uri = params.text_document.uri;
34+
let db = snapshot.db();
35+
let file = snapshot.file(&uri).unwrap();
36+
let line_index = line_index(db, file);
37+
let range_to_highlight = lsp_utils::text_range(&line_index, params.range);
38+
let tokens = semantic_tokens(db, file, range_to_highlight);
39+
let text = file.content(db);
40+
Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
41+
result_id: None,
42+
data: lsp_utils::to_semantic_tokens(text, line_index, tokens),
43+
})))
44+
}

crates/squawk_server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod ignore;
66
mod lint;
77
mod lsp_utils;
88
mod panic;
9+
mod semantic_tokens;
910
mod server;
1011

1112
pub use server::run;

0 commit comments

Comments
 (0)