Skip to content

Commit de8913a

Browse files
authored
playground: add semantic syntax highlighting (#1052)
it's not pretty, but it works
1 parent 12ccf67 commit de8913a

7 files changed

Lines changed: 205 additions & 3 deletions

File tree

.oxfmtrc.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"ignorePatterns": [
77
"build/",
88
"node_modules/",
9-
"playground/",
109
"coverage/",
1110
".venv/",
1211
".mypy_cache/",

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@
1111
},
1212
"[sql]": {
1313
"editor.tabSize": 2
14+
},
15+
"[typescriptreact]": {
16+
"editor.defaultFormatter": "oxc.oxc-vscode"
1417
}
1518
}

crates/squawk_server/src/lsp_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ pub(crate) fn to_semantic_tokens(
242242
prev_start: 0,
243243
};
244244

245+
// Duplicated in squawk-wasm, fyi
245246
for token in &*semantic_tokens {
246247
// Taken from rust-analyzer, this solves the case where we have a multi
247248
// line semantic token which isn't supported by the LSP spec.

crates/squawk_wasm/src/lib.rs

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,104 @@
11
use line_index::LineIndex;
22
use log::info;
3-
use rowan::TextRange;
3+
use rowan::{TextRange, TextSize};
44
use salsa::Setter;
55
use serde::{Deserialize, Serialize};
66
use squawk_ide::builtins::builtins_line_index;
77
use squawk_ide::db::{self, Database, File};
88
use squawk_ide::folding_ranges::{FoldKind, folding_ranges};
99
use squawk_ide::goto_definition::FileId;
10+
use squawk_ide::semantic_tokens::{SemanticTokenType, semantic_tokens};
1011
use squawk_syntax::ast::AstNode;
1112
use wasm_bindgen::prelude::*;
1213
use web_sys::js_sys::Error;
1314

15+
const SEMANTIC_TOKEN_TYPES: &[&str] = &[
16+
"comment",
17+
"function",
18+
"keyword",
19+
"namespace",
20+
"number",
21+
"operator",
22+
"parameter",
23+
"property",
24+
"string",
25+
"struct",
26+
"type",
27+
"variable",
28+
];
29+
30+
const SEMANTIC_TOKEN_MODIFIERS: &[&str] = &["declaration", "definition", "readonly"];
31+
32+
fn semantic_token_type_name(ty: SemanticTokenType) -> &'static str {
33+
match ty {
34+
SemanticTokenType::Bool | SemanticTokenType::Keyword => "keyword",
35+
SemanticTokenType::Comment => "comment",
36+
SemanticTokenType::Function => "function",
37+
SemanticTokenType::Name | SemanticTokenType::NameRef => "variable",
38+
SemanticTokenType::Number => "number",
39+
SemanticTokenType::Operator | SemanticTokenType::Punctuation => "operator",
40+
SemanticTokenType::Parameter | SemanticTokenType::PositionalParam => "parameter",
41+
SemanticTokenType::String => "string",
42+
SemanticTokenType::Type => "type",
43+
}
44+
}
45+
46+
fn semantic_token_type_index(ty: SemanticTokenType) -> u32 {
47+
let name = semantic_token_type_name(ty);
48+
SEMANTIC_TOKEN_TYPES
49+
.iter()
50+
.position(|it| *it == name)
51+
.unwrap() as u32
52+
}
53+
54+
struct EncodedSemanticToken {
55+
line: u32,
56+
start: u32,
57+
length: u32,
58+
token_type: SemanticTokenType,
59+
modifiers: u32,
60+
}
61+
62+
struct SemanticTokenEncoder {
63+
data: Vec<u32>,
64+
prev_line: u32,
65+
prev_start: u32,
66+
}
67+
68+
impl SemanticTokenEncoder {
69+
fn with_capacity(token_count: usize) -> Self {
70+
Self {
71+
data: Vec::with_capacity(token_count * 5),
72+
prev_line: 0,
73+
prev_start: 0,
74+
}
75+
}
76+
77+
fn push(&mut self, token: EncodedSemanticToken) {
78+
let delta_line = token.line - self.prev_line;
79+
let delta_start = if delta_line == 0 {
80+
token.start - self.prev_start
81+
} else {
82+
token.start
83+
};
84+
85+
self.data.extend_from_slice(&[
86+
delta_line,
87+
delta_start,
88+
token.length,
89+
semantic_token_type_index(token.token_type),
90+
token.modifiers,
91+
]);
92+
93+
self.prev_line = token.line;
94+
self.prev_start = token.start;
95+
}
96+
97+
fn finish(self) -> Vec<u32> {
98+
self.data
99+
}
100+
}
101+
14102
#[wasm_bindgen(start)]
15103
pub fn run() {
16104
use log::Level;
@@ -429,6 +517,55 @@ impl SquawkDatabase {
429517
serde_wasm_bindgen::to_value(&results).map_err(into_error)
430518
}
431519

520+
pub fn semantic_tokens(&self) -> Result<Vec<u32>, Error> {
521+
let file = self.file()?;
522+
let line_index = db::line_index(&self.db, file);
523+
let content = file.content(&self.db);
524+
let tokens = semantic_tokens(&self.db, file, None);
525+
526+
let mut encoder = SemanticTokenEncoder::with_capacity(tokens.len());
527+
528+
// Duplicated from squawk-server, fyi
529+
for token in &tokens {
530+
// Taken from rust-analyzer, this solves the case where we have a
531+
// multi line semantic token which isn't supported by the LSP spec.
532+
// see: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/rust-analyzer/src/lsp/to_proto.rs#L781C28-L781C28
533+
for mut text_range in line_index.lines(token.range) {
534+
if content[text_range].ends_with('\n') {
535+
text_range =
536+
TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n'));
537+
}
538+
let start_lc = line_index.line_col(text_range.start());
539+
let end_lc = line_index.line_col(text_range.end());
540+
let start_wide = line_index
541+
.to_wide(line_index::WideEncoding::Utf16, start_lc)
542+
.unwrap();
543+
let end_wide = line_index
544+
.to_wide(line_index::WideEncoding::Utf16, end_lc)
545+
.unwrap();
546+
547+
encoder.push(EncodedSemanticToken {
548+
line: start_wide.line,
549+
start: start_wide.col,
550+
length: end_wide.col - start_wide.col,
551+
token_type: token.token_type,
552+
// TODO: once we get modifiers going, we'll need to update this
553+
modifiers: 0,
554+
});
555+
}
556+
}
557+
558+
Ok(encoder.finish())
559+
}
560+
561+
pub fn semantic_tokens_legend() -> Result<JsValue, Error> {
562+
let legend = SemanticTokensLegend {
563+
token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
564+
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
565+
};
566+
serde_wasm_bindgen::to_value(&legend).map_err(into_error)
567+
}
568+
432569
pub fn completion(&self, line: u32, col: u32) -> Result<JsValue, Error> {
433570
let file = self.file()?;
434571
let line_index = db::line_index(&self.db, file);
@@ -656,6 +793,14 @@ struct WasmSelectionRange {
656793
end_column: u32,
657794
}
658795

796+
#[derive(Serialize)]
797+
struct SemanticTokensLegend {
798+
#[serde(rename = "tokenTypes")]
799+
token_types: Vec<&'static str>,
800+
#[serde(rename = "tokenModifiers")]
801+
token_modifiers: Vec<&'static str>,
802+
}
803+
659804
#[derive(Serialize)]
660805
struct WasmCompletionItem {
661806
label: String,

playground/src/App.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
provideFoldingRanges,
2323
provideSelectionRanges,
2424
provideCompletionItems,
25+
semanticTokensProvider,
2526
} from "./providers"
2627
import BUILTINS_SQL from "./builtins.sql?raw"
2728

@@ -48,7 +49,7 @@ const SETTINGS = {
4849
value: DEFAULT_CONTENT,
4950
language: "pgsql",
5051
tabSize: 2,
51-
theme: "vs-dark",
52+
theme: "squawk-dark",
5253
minimap: { enabled: false },
5354
automaticLayout: true,
5455
scrollBeyondLastLine: false,
@@ -64,6 +65,7 @@ const SETTINGS = {
6465
renderWhitespace: "boundary",
6566
guides: { indentation: false },
6667
lineNumbersMinChars: 3,
68+
"semanticHighlighting.enabled": true,
6769
} satisfies monaco.editor.IStandaloneEditorConstructionOptions
6870

6971
function clx(...args: (string | undefined | number | false)[]): string {
@@ -288,6 +290,15 @@ function registerMonacoProvidersOnce() {
288290
return
289291
}
290292
monacoGlobalProvidersRegistered = true
293+
// vs-dark maps variable to a blue color which makes everything look like a
294+
// keyword. So we use white instead which was what the `foo` in `select 1 foo`
295+
// was before semantic syntax highlighting.
296+
monaco.editor.defineTheme("squawk-dark", {
297+
base: "vs-dark",
298+
inherit: true,
299+
rules: [{ token: "variable", foreground: "D4D4D4" }],
300+
colors: {},
301+
})
291302
const languageConfig = monaco.languages.setLanguageConfiguration("pgsql", {
292303
comments: {
293304
lineComment: "--",
@@ -473,6 +484,12 @@ function registerMonacoProvidersOnce() {
473484
},
474485
)
475486

487+
const documentSemanticTokensProvider =
488+
monaco.languages.registerDocumentSemanticTokensProvider(
489+
"pgsql",
490+
semanticTokensProvider,
491+
)
492+
476493
return () => {
477494
languageConfig.dispose()
478495
codeActionProvider.dispose()
@@ -484,6 +501,7 @@ function registerMonacoProvidersOnce() {
484501
inlayHintsProvider.dispose()
485502
selectionRangeProvider.dispose()
486503
completionProvider.dispose()
504+
documentSemanticTokensProvider.dispose()
487505
tokenProvider.dispose()
488506
}
489507
}

playground/src/providers.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
hover,
1010
inlay_hints,
1111
selection_ranges,
12+
semantic_tokens,
13+
semantic_tokens_legend,
1214
DocumentSymbol,
1315
} from "./squawk"
1416

@@ -315,6 +317,27 @@ function convertCompletionKind(
315317
}
316318
}
317319

320+
export const semanticTokensProvider: monaco.languages.DocumentSemanticTokensProvider =
321+
{
322+
getLegend() {
323+
return semantic_tokens_legend()
324+
},
325+
provideDocumentSemanticTokens(model) {
326+
const content = model.getValue()
327+
const version = model.getVersionId()
328+
if (!content) return null
329+
330+
try {
331+
const data = semantic_tokens(content, version)
332+
return { data, resultId: undefined }
333+
} catch (e) {
334+
console.error("Error in provideDocumentSemanticTokens:", e)
335+
return null
336+
}
337+
},
338+
releaseDocumentSemanticTokens() {},
339+
}
340+
318341
export async function provideCompletionItems(
319342
model: monaco.editor.ITextModel,
320343
position: monaco.Position,

playground/src/squawk.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ export function completion(
121121
return getDb(content, version).completion(line, column)
122122
}
123123

124+
export interface SemanticTokensLegend {
125+
tokenTypes: string[]
126+
tokenModifiers: string[]
127+
}
128+
129+
export function semantic_tokens(content: string, version: number): Uint32Array {
130+
return getDb(content, version).semantic_tokens()
131+
}
132+
133+
export function semantic_tokens_legend(): SemanticTokensLegend {
134+
return SquawkDatabase.semantic_tokens_legend()
135+
}
136+
124137
export function dump_cst(content: string, version: number): string {
125138
return getDb(content, version).dump_cst()
126139
}

0 commit comments

Comments
 (0)