1use line_index::LineIndex;
2use log::info;
3use serde::Serialize;
4use wasm_bindgen::prelude::*;
5use web_sys::js_sys::Error;
6
7#[wasm_bindgen(start)]
8pub fn run() {
9 use log::Level;
10
11 #[cfg(feature = "console_error_panic_hook")]
18 console_error_panic_hook::set_once();
19 console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
20 info!("init!");
21}
22
23#[wasm_bindgen]
24pub fn dump_cst(text: String) -> String {
25 let parse = squawk_syntax::SourceFile::parse(&text);
26 format!("{:#?}", parse.syntax_node())
27}
28
29#[wasm_bindgen]
30pub fn dump_tokens(text: String) -> String {
31 let tokens = squawk_lexer::tokenize(&text);
32 let mut start = 0;
33 let mut out = String::new();
34 for token in tokens {
35 let end = start + token.len;
36 let content = &text[start as usize..(end) as usize];
37 out += &format!("{:?}@{start}..{end} {:?}\n", token.kind, content);
38 start += token.len;
39 }
40 out
41}
42
43#[expect(unused)]
44#[derive(Serialize)]
45enum Severity {
46 Hint,
47 Info,
48 Warning,
49 Error,
50}
51
52#[derive(Serialize)]
53struct LintError {
54 severity: Severity,
55 code: String,
56 message: String,
57 start_line_number: u32,
58 start_column: u32,
59 end_line_number: u32,
60 end_column: u32,
61 range_start: usize,
63 range_end: usize,
65 messages: Vec<String>,
67 fix: Option<Fix>,
68}
69
70#[derive(Serialize)]
71struct Fix {
72 title: String,
73 edits: Vec<TextEdit>,
74}
75
76#[derive(Serialize)]
77struct TextEdit {
78 start_line_number: u32,
79 start_column: u32,
80 end_line_number: u32,
81 end_column: u32,
82 text: String,
83}
84
85#[wasm_bindgen]
86pub fn lint(text: String) -> Result<JsValue, Error> {
87 let mut linter = squawk_linter::Linter::with_all_rules();
88 let parse = squawk_syntax::SourceFile::parse(&text);
89 let parse_errors = parse.errors();
90
91 let line_index = LineIndex::new(&text);
92
93 let parse_errors = parse_errors.iter().map(|x| {
95 let range_start = x.range().start();
96 let range_end = x.range().end();
97 let start = line_index.line_col(range_start);
98 let end = line_index.line_col(range_end);
99 let start = line_index
100 .to_wide(line_index::WideEncoding::Utf16, start)
101 .unwrap();
102 let end = line_index
103 .to_wide(line_index::WideEncoding::Utf16, end)
104 .unwrap();
105 LintError {
106 severity: Severity::Error,
107 code: "syntax-error".to_string(),
108 message: x.message().to_string(),
109 start_line_number: start.line,
110 start_column: start.col,
111 end_line_number: end.line,
112 end_column: end.col,
113 range_start: range_start.into(),
114 range_end: range_end.into(),
115 messages: vec![],
116 fix: None,
117 }
118 });
119
120 let lint_errors = linter.lint(&parse, &text);
121 let errors = lint_errors.into_iter().map(|x| {
122 let start = line_index.line_col(x.text_range.start());
123 let end = line_index.line_col(x.text_range.end());
124 let start = line_index
125 .to_wide(line_index::WideEncoding::Utf16, start)
126 .unwrap();
127 let end = line_index
128 .to_wide(line_index::WideEncoding::Utf16, end)
129 .unwrap();
130
131 let messages = x.help.into_iter().collect();
132
133 let fix = x.fix.map(|fix| {
134 let edits = fix
135 .edits
136 .into_iter()
137 .map(|edit| {
138 let start_pos = line_index.line_col(edit.text_range.start());
139 let end_pos = line_index.line_col(edit.text_range.end());
140 let start_wide = line_index
141 .to_wide(line_index::WideEncoding::Utf16, start_pos)
142 .unwrap();
143 let end_wide = line_index
144 .to_wide(line_index::WideEncoding::Utf16, end_pos)
145 .unwrap();
146
147 TextEdit {
148 start_line_number: start_wide.line,
149 start_column: start_wide.col,
150 end_line_number: end_wide.line,
151 end_column: end_wide.col,
152 text: edit.text.unwrap_or_default(),
153 }
154 })
155 .collect();
156
157 Fix {
158 title: fix.title,
159 edits,
160 }
161 });
162
163 LintError {
164 code: x.code.to_string(),
165 range_start: x.text_range.start().into(),
166 range_end: x.text_range.end().into(),
167 message: x.message.clone(),
168 messages,
169 severity: Severity::Warning,
171 start_line_number: start.line,
172 start_column: start.col,
173 end_line_number: end.line,
174 end_column: end.col,
175 fix,
176 }
177 });
178
179 let mut errors_to_dump = errors.chain(parse_errors).collect::<Vec<_>>();
180 errors_to_dump.sort_by_key(|k| (k.start_line_number, k.start_column));
181
182 serde_wasm_bindgen::to_value(&errors_to_dump).map_err(into_error)
183}
184
185fn into_error<E: std::fmt::Display>(err: E) -> Error {
186 Error::new(&err.to_string())
187}
188
189#[wasm_bindgen]
190pub fn goto_definition(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
191 let parse = squawk_syntax::SourceFile::parse(&content);
192 let line_index = LineIndex::new(&content);
193 let offset = position_to_offset(&line_index, line, col)?;
194 let result = squawk_ide::goto_definition::goto_definition(parse.tree(), offset);
195
196 let response: Vec<LocationRange> = result
197 .into_iter()
198 .map(|range| {
199 let start = line_index.line_col(range.start());
200 let end = line_index.line_col(range.end());
201 let start_wide = line_index
202 .to_wide(line_index::WideEncoding::Utf16, start)
203 .unwrap();
204 let end_wide = line_index
205 .to_wide(line_index::WideEncoding::Utf16, end)
206 .unwrap();
207
208 LocationRange {
209 start_line: start_wide.line,
210 start_column: start_wide.col,
211 end_line: end_wide.line,
212 end_column: end_wide.col,
213 }
214 })
215 .collect();
216
217 serde_wasm_bindgen::to_value(&response).map_err(into_error)
218}
219
220#[wasm_bindgen]
221pub fn hover(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
222 let parse = squawk_syntax::SourceFile::parse(&content);
223 let line_index = LineIndex::new(&content);
224 let offset = position_to_offset(&line_index, line, col)?;
225 let result = squawk_ide::hover::hover(&parse.tree(), offset);
226
227 serde_wasm_bindgen::to_value(&result).map_err(into_error)
228}
229
230#[wasm_bindgen]
231pub fn find_references(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
232 let parse = squawk_syntax::SourceFile::parse(&content);
233 let line_index = LineIndex::new(&content);
234 let offset = position_to_offset(&line_index, line, col)?;
235 let references = squawk_ide::find_references::find_references(&parse.tree(), offset);
236
237 let locations: Vec<LocationRange> = references
238 .iter()
239 .map(|range| {
240 let start = line_index.line_col(range.start());
241 let end = line_index.line_col(range.end());
242 let start_wide = line_index
243 .to_wide(line_index::WideEncoding::Utf16, start)
244 .unwrap();
245 let end_wide = line_index
246 .to_wide(line_index::WideEncoding::Utf16, end)
247 .unwrap();
248
249 LocationRange {
250 start_line: start_wide.line,
251 start_column: start_wide.col,
252 end_line: end_wide.line,
253 end_column: end_wide.col,
254 }
255 })
256 .collect();
257
258 serde_wasm_bindgen::to_value(&locations).map_err(into_error)
259}
260
261#[wasm_bindgen]
262pub fn document_symbols(content: String) -> Result<JsValue, Error> {
263 let parse = squawk_syntax::SourceFile::parse(&content);
264 let line_index = LineIndex::new(&content);
265 let symbols = squawk_ide::document_symbols::document_symbols(&parse.tree());
266
267 let converted: Vec<WasmDocumentSymbol> = symbols
268 .into_iter()
269 .map(|s| convert_document_symbol(&line_index, s))
270 .collect();
271
272 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
273}
274
275#[wasm_bindgen]
276pub fn code_actions(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
277 let parse = squawk_syntax::SourceFile::parse(&content);
278 let line_index = LineIndex::new(&content);
279 let offset = position_to_offset(&line_index, line, col)?;
280 let actions = squawk_ide::code_actions::code_actions(parse.tree(), offset);
281
282 let converted = actions.map(|actions| {
283 actions
284 .into_iter()
285 .map(|action| {
286 let edits = action
287 .edits
288 .into_iter()
289 .map(|edit| {
290 let start_pos = line_index.line_col(edit.text_range.start());
291 let end_pos = line_index.line_col(edit.text_range.end());
292 let start_wide = line_index
293 .to_wide(line_index::WideEncoding::Utf16, start_pos)
294 .unwrap();
295 let end_wide = line_index
296 .to_wide(line_index::WideEncoding::Utf16, end_pos)
297 .unwrap();
298
299 TextEdit {
300 start_line_number: start_wide.line,
301 start_column: start_wide.col,
302 end_line_number: end_wide.line,
303 end_column: end_wide.col,
304 text: edit.text.unwrap_or_default(),
305 }
306 })
307 .collect();
308
309 WasmCodeAction {
310 title: action.title,
311 edits,
312 kind: match action.kind {
313 squawk_ide::code_actions::ActionKind::QuickFix => "quickfix",
314 squawk_ide::code_actions::ActionKind::RefactorRewrite => "refactor.rewrite",
315 }
316 .to_string(),
317 }
318 })
319 .collect::<Vec<_>>()
320 });
321
322 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
323}
324
325fn position_to_offset(
326 line_index: &LineIndex,
327 line: u32,
328 col: u32,
329) -> Result<rowan::TextSize, Error> {
330 let wide_pos = line_index::WideLineCol { line, col };
331
332 let pos = line_index
333 .to_utf8(line_index::WideEncoding::Utf16, wide_pos)
334 .ok_or_else(|| Error::new("Invalid position"))?;
335
336 line_index
337 .offset(pos)
338 .ok_or_else(|| Error::new("Invalid position offset"))
339}
340
341#[derive(Serialize)]
342struct LocationRange {
343 start_line: u32,
344 start_column: u32,
345 end_line: u32,
346 end_column: u32,
347}
348
349#[derive(Serialize)]
350struct WasmCodeAction {
351 title: String,
352 edits: Vec<TextEdit>,
353 kind: String,
354}
355
356#[derive(Serialize)]
357struct WasmDocumentSymbol {
358 name: String,
359 detail: Option<String>,
360 kind: String,
361 start_line: u32,
362 start_column: u32,
363 end_line: u32,
364 end_column: u32,
365 selection_start_line: u32,
366 selection_start_column: u32,
367 selection_end_line: u32,
368 selection_end_column: u32,
369 children: Vec<WasmDocumentSymbol>,
370}
371
372fn convert_document_symbol(
373 line_index: &LineIndex,
374 symbol: squawk_ide::document_symbols::DocumentSymbol,
375) -> WasmDocumentSymbol {
376 let full_start = line_index.line_col(symbol.full_range.start());
377 let full_end = line_index.line_col(symbol.full_range.end());
378 let full_start_wide = line_index
379 .to_wide(line_index::WideEncoding::Utf16, full_start)
380 .unwrap();
381 let full_end_wide = line_index
382 .to_wide(line_index::WideEncoding::Utf16, full_end)
383 .unwrap();
384
385 let focus_start = line_index.line_col(symbol.focus_range.start());
386 let focus_end = line_index.line_col(symbol.focus_range.end());
387 let focus_start_wide = line_index
388 .to_wide(line_index::WideEncoding::Utf16, focus_start)
389 .unwrap();
390 let focus_end_wide = line_index
391 .to_wide(line_index::WideEncoding::Utf16, focus_end)
392 .unwrap();
393
394 WasmDocumentSymbol {
395 name: symbol.name,
396 detail: symbol.detail,
397 kind: match symbol.kind {
398 squawk_ide::document_symbols::DocumentSymbolKind::Schema => "schema",
399 squawk_ide::document_symbols::DocumentSymbolKind::Table => "table",
400 squawk_ide::document_symbols::DocumentSymbolKind::View => "view",
401 squawk_ide::document_symbols::DocumentSymbolKind::MaterializedView => {
402 "materialized_view"
403 }
404 squawk_ide::document_symbols::DocumentSymbolKind::Function => "function",
405 squawk_ide::document_symbols::DocumentSymbolKind::Aggregate => "aggregate",
406 squawk_ide::document_symbols::DocumentSymbolKind::Procedure => "procedure",
407 squawk_ide::document_symbols::DocumentSymbolKind::Type => "type",
408 squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum",
409 squawk_ide::document_symbols::DocumentSymbolKind::Column => "column",
410 squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant",
411 }
412 .to_string(),
413 start_line: full_start_wide.line,
414 start_column: full_start_wide.col,
415 end_line: full_end_wide.line,
416 end_column: full_end_wide.col,
417 selection_start_line: focus_start_wide.line,
418 selection_start_column: focus_start_wide.col,
419 selection_end_line: focus_end_wide.line,
420 selection_end_column: focus_end_wide.col,
421 children: symbol
422 .children
423 .into_iter()
424 .map(|child| convert_document_symbol(line_index, child))
425 .collect(),
426 }
427}
428
429#[wasm_bindgen]
430pub fn inlay_hints(content: String) -> Result<JsValue, Error> {
431 let parse = squawk_syntax::SourceFile::parse(&content);
432 let line_index = LineIndex::new(&content);
433 let hints = squawk_ide::inlay_hints::inlay_hints(&parse.tree());
434
435 let converted: Vec<WasmInlayHint> = hints
436 .into_iter()
437 .map(|hint| {
438 let position = line_index.line_col(hint.position);
439 let position_wide = line_index
440 .to_wide(line_index::WideEncoding::Utf16, position)
441 .unwrap();
442
443 WasmInlayHint {
444 line: position_wide.line,
445 column: position_wide.col,
446 label: hint.label,
447 kind: match hint.kind {
448 squawk_ide::inlay_hints::InlayHintKind::Type => "type",
449 squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter",
450 }
451 .to_string(),
452 }
453 })
454 .collect();
455
456 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
457}
458
459#[derive(Serialize)]
460struct WasmInlayHint {
461 line: u32,
462 column: u32,
463 label: String,
464 kind: String,
465}