Skip to main content

squawk_wasm/
lib.rs

1use line_index::LineIndex;
2use log::info;
3use rowan::TextRange;
4use serde::{Deserialize, Serialize};
5use squawk_ide::builtins::BUILTINS_SQL;
6use squawk_ide::goto_definition::FileId;
7use squawk_syntax::ast::AstNode;
8use wasm_bindgen::prelude::*;
9use web_sys::js_sys::Error;
10
11#[wasm_bindgen(start)]
12pub fn run() {
13    use log::Level;
14
15    // When the `console_error_panic_hook` feature is enabled, we can call the
16    // `set_panic_hook` function at least once during initialization, and then
17    // we will get better error messages if our code ever panics.
18    //
19    // For more details see
20    // https://github.com/rustwasm/console_error_panic_hook#readme
21    #[cfg(feature = "console_error_panic_hook")]
22    console_error_panic_hook::set_once();
23    console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
24    info!("init!");
25}
26
27#[wasm_bindgen]
28pub fn dump_cst(text: String) -> String {
29    let parse = squawk_syntax::SourceFile::parse(&text);
30    format!("{:#?}", parse.syntax_node())
31}
32
33#[wasm_bindgen]
34pub fn dump_tokens(text: String) -> String {
35    let tokens = squawk_lexer::tokenize(&text);
36    let mut start = 0;
37    let mut out = String::new();
38    for token in tokens {
39        let end = start + token.len;
40        let content = &text[start as usize..(end) as usize];
41        out += &format!("{:?}@{start}..{end} {:?}\n", token.kind, content);
42        start += token.len;
43    }
44    out
45}
46
47#[expect(unused)]
48#[derive(Serialize)]
49enum Severity {
50    Hint,
51    Info,
52    Warning,
53    Error,
54}
55
56#[derive(Serialize)]
57struct LintError {
58    severity: Severity,
59    code: String,
60    message: String,
61    start_line_number: u32,
62    start_column: u32,
63    end_line_number: u32,
64    end_column: u32,
65    // used for the linter tab
66    range_start: usize,
67    // used for the linter tab
68    range_end: usize,
69    // used for the linter tab
70    messages: Vec<String>,
71    fix: Option<Fix>,
72}
73
74#[derive(Serialize)]
75struct Fix {
76    title: String,
77    edits: Vec<TextEdit>,
78}
79
80#[derive(Serialize)]
81struct TextEdit {
82    start_line_number: u32,
83    start_column: u32,
84    end_line_number: u32,
85    end_column: u32,
86    text: String,
87}
88
89#[wasm_bindgen]
90pub fn lint(text: String) -> Result<JsValue, Error> {
91    let mut linter = squawk_linter::Linter::with_all_rules();
92    let parse = squawk_syntax::SourceFile::parse(&text);
93    let parse_errors = parse.errors();
94
95    let line_index = LineIndex::new(&text);
96
97    // TODO: chain these with other stuff
98    let parse_errors = parse_errors.iter().map(|x| {
99        let range_start = x.range().start();
100        let range_end = x.range().end();
101        let start = line_index.line_col(range_start);
102        let end = line_index.line_col(range_end);
103        let start = line_index
104            .to_wide(line_index::WideEncoding::Utf16, start)
105            .unwrap();
106        let end = line_index
107            .to_wide(line_index::WideEncoding::Utf16, end)
108            .unwrap();
109        LintError {
110            severity: Severity::Error,
111            code: "syntax-error".to_string(),
112            message: x.message().to_string(),
113            start_line_number: start.line,
114            start_column: start.col,
115            end_line_number: end.line,
116            end_column: end.col,
117            range_start: range_start.into(),
118            range_end: range_end.into(),
119            messages: vec![],
120            fix: None,
121        }
122    });
123
124    let lint_errors = linter.lint(&parse, &text);
125    let errors = lint_errors.into_iter().map(|x| {
126        let start = line_index.line_col(x.text_range.start());
127        let end = line_index.line_col(x.text_range.end());
128        let start = line_index
129            .to_wide(line_index::WideEncoding::Utf16, start)
130            .unwrap();
131        let end = line_index
132            .to_wide(line_index::WideEncoding::Utf16, end)
133            .unwrap();
134
135        let messages = x.help.into_iter().collect();
136
137        let fix = x.fix.map(|fix| {
138            let edits = fix
139                .edits
140                .into_iter()
141                .map(|edit| {
142                    let start_pos = line_index.line_col(edit.text_range.start());
143                    let end_pos = line_index.line_col(edit.text_range.end());
144                    let start_wide = line_index
145                        .to_wide(line_index::WideEncoding::Utf16, start_pos)
146                        .unwrap();
147                    let end_wide = line_index
148                        .to_wide(line_index::WideEncoding::Utf16, end_pos)
149                        .unwrap();
150
151                    TextEdit {
152                        start_line_number: start_wide.line,
153                        start_column: start_wide.col,
154                        end_line_number: end_wide.line,
155                        end_column: end_wide.col,
156                        text: edit.text.unwrap_or_default(),
157                    }
158                })
159                .collect();
160
161            Fix {
162                title: fix.title,
163                edits,
164            }
165        });
166
167        LintError {
168            code: x.code.to_string(),
169            range_start: x.text_range.start().into(),
170            range_end: x.text_range.end().into(),
171            message: x.message.clone(),
172            messages,
173            // parser errors should be error
174            severity: Severity::Warning,
175            start_line_number: start.line,
176            start_column: start.col,
177            end_line_number: end.line,
178            end_column: end.col,
179            fix,
180        }
181    });
182
183    let mut errors_to_dump = errors.chain(parse_errors).collect::<Vec<_>>();
184    errors_to_dump.sort_by_key(|k| (k.start_line_number, k.start_column));
185
186    serde_wasm_bindgen::to_value(&errors_to_dump).map_err(into_error)
187}
188
189fn into_error<E: std::fmt::Display>(err: E) -> Error {
190    Error::new(&err.to_string())
191}
192
193#[wasm_bindgen]
194pub fn goto_definition(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
195    let parse = squawk_syntax::SourceFile::parse(&content);
196    let current_line_index = LineIndex::new(&content);
197    let builtins_line_index = LineIndex::new(BUILTINS_SQL);
198    let offset = position_to_offset(&current_line_index, line, col)?;
199    let result = squawk_ide::goto_definition::goto_definition(&parse.tree(), offset);
200
201    let response: Vec<LocationRange> = result
202        .into_iter()
203        .map(|location| {
204            let range = location.range;
205            let (file, line_index) = match location.file {
206                FileId::Current => ("current", &current_line_index),
207                FileId::Builtins => ("builtins", &builtins_line_index),
208            };
209            let start = line_index.line_col(range.start());
210            let end = line_index.line_col(range.end());
211            let start_wide = line_index
212                .to_wide(line_index::WideEncoding::Utf16, start)
213                .unwrap();
214            let end_wide = line_index
215                .to_wide(line_index::WideEncoding::Utf16, end)
216                .unwrap();
217
218            LocationRange {
219                file: file.to_string(),
220                start_line: start_wide.line,
221                start_column: start_wide.col,
222                end_line: end_wide.line,
223                end_column: end_wide.col,
224            }
225        })
226        .collect();
227
228    serde_wasm_bindgen::to_value(&response).map_err(into_error)
229}
230
231#[wasm_bindgen]
232pub fn hover(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
233    let parse = squawk_syntax::SourceFile::parse(&content);
234    let line_index = LineIndex::new(&content);
235    let offset = position_to_offset(&line_index, line, col)?;
236    let result = squawk_ide::hover::hover(&parse.tree(), offset);
237
238    serde_wasm_bindgen::to_value(&result).map_err(into_error)
239}
240
241#[wasm_bindgen]
242pub fn find_references(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
243    let parse = squawk_syntax::SourceFile::parse(&content);
244    let line_index = LineIndex::new(&content);
245    let offset = position_to_offset(&line_index, line, col)?;
246    let references = squawk_ide::find_references::find_references(&parse.tree(), offset);
247
248    let builtins_line_index = LineIndex::new(BUILTINS_SQL);
249    let locations: Vec<LocationRange> = references
250        .iter()
251        .map(|loc| {
252            let (li, file) = match loc.file {
253                FileId::Current => (&line_index, "current"),
254                FileId::Builtins => (&builtins_line_index, "builtin"),
255            };
256            let start = li.line_col(loc.range.start());
257            let end = li.line_col(loc.range.end());
258            let start_wide = li.to_wide(line_index::WideEncoding::Utf16, start).unwrap();
259            let end_wide = li.to_wide(line_index::WideEncoding::Utf16, end).unwrap();
260
261            LocationRange {
262                file: file.to_string(),
263                start_line: start_wide.line,
264                start_column: start_wide.col,
265                end_line: end_wide.line,
266                end_column: end_wide.col,
267            }
268        })
269        .collect();
270
271    serde_wasm_bindgen::to_value(&locations).map_err(into_error)
272}
273
274#[wasm_bindgen]
275pub fn document_symbols(content: String) -> Result<JsValue, Error> {
276    let parse = squawk_syntax::SourceFile::parse(&content);
277    let line_index = LineIndex::new(&content);
278    let symbols = squawk_ide::document_symbols::document_symbols(&parse.tree());
279
280    let converted: Vec<WasmDocumentSymbol> = symbols
281        .into_iter()
282        .map(|s| convert_document_symbol(&line_index, s))
283        .collect();
284
285    serde_wasm_bindgen::to_value(&converted).map_err(into_error)
286}
287
288#[wasm_bindgen]
289pub fn code_actions(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
290    let parse = squawk_syntax::SourceFile::parse(&content);
291    let line_index = LineIndex::new(&content);
292    let offset = position_to_offset(&line_index, line, col)?;
293    let actions = squawk_ide::code_actions::code_actions(parse.tree(), offset);
294
295    let converted = actions.map(|actions| {
296        actions
297            .into_iter()
298            .map(|action| {
299                let edits = action
300                    .edits
301                    .into_iter()
302                    .map(|edit| {
303                        let start_pos = line_index.line_col(edit.text_range.start());
304                        let end_pos = line_index.line_col(edit.text_range.end());
305                        let start_wide = line_index
306                            .to_wide(line_index::WideEncoding::Utf16, start_pos)
307                            .unwrap();
308                        let end_wide = line_index
309                            .to_wide(line_index::WideEncoding::Utf16, end_pos)
310                            .unwrap();
311
312                        TextEdit {
313                            start_line_number: start_wide.line,
314                            start_column: start_wide.col,
315                            end_line_number: end_wide.line,
316                            end_column: end_wide.col,
317                            text: edit.text.unwrap_or_default(),
318                        }
319                    })
320                    .collect();
321
322                WasmCodeAction {
323                    title: action.title,
324                    edits,
325                    kind: match action.kind {
326                        squawk_ide::code_actions::ActionKind::QuickFix => "quickfix",
327                        squawk_ide::code_actions::ActionKind::RefactorRewrite => "refactor.rewrite",
328                    }
329                    .to_string(),
330                }
331            })
332            .collect::<Vec<_>>()
333    });
334
335    serde_wasm_bindgen::to_value(&converted).map_err(into_error)
336}
337
338fn position_to_offset(
339    line_index: &LineIndex,
340    line: u32,
341    col: u32,
342) -> Result<rowan::TextSize, Error> {
343    let wide_pos = line_index::WideLineCol { line, col };
344
345    let pos = line_index
346        .to_utf8(line_index::WideEncoding::Utf16, wide_pos)
347        .ok_or_else(|| Error::new("Invalid position"))?;
348
349    line_index
350        .offset(pos)
351        .ok_or_else(|| Error::new("Invalid position offset"))
352}
353
354#[derive(Serialize)]
355struct LocationRange {
356    file: String,
357    start_line: u32,
358    start_column: u32,
359    end_line: u32,
360    end_column: u32,
361}
362
363#[derive(Serialize)]
364struct WasmCodeAction {
365    title: String,
366    edits: Vec<TextEdit>,
367    kind: String,
368}
369
370#[derive(Serialize)]
371struct WasmDocumentSymbol {
372    name: String,
373    detail: Option<String>,
374    kind: String,
375    start_line: u32,
376    start_column: u32,
377    end_line: u32,
378    end_column: u32,
379    selection_start_line: u32,
380    selection_start_column: u32,
381    selection_end_line: u32,
382    selection_end_column: u32,
383    children: Vec<WasmDocumentSymbol>,
384}
385
386fn convert_document_symbol(
387    line_index: &LineIndex,
388    symbol: squawk_ide::document_symbols::DocumentSymbol,
389) -> WasmDocumentSymbol {
390    let full_start = line_index.line_col(symbol.full_range.start());
391    let full_end = line_index.line_col(symbol.full_range.end());
392    let full_start_wide = line_index
393        .to_wide(line_index::WideEncoding::Utf16, full_start)
394        .unwrap();
395    let full_end_wide = line_index
396        .to_wide(line_index::WideEncoding::Utf16, full_end)
397        .unwrap();
398
399    let focus_start = line_index.line_col(symbol.focus_range.start());
400    let focus_end = line_index.line_col(symbol.focus_range.end());
401    let focus_start_wide = line_index
402        .to_wide(line_index::WideEncoding::Utf16, focus_start)
403        .unwrap();
404    let focus_end_wide = line_index
405        .to_wide(line_index::WideEncoding::Utf16, focus_end)
406        .unwrap();
407
408    WasmDocumentSymbol {
409        name: symbol.name,
410        detail: symbol.detail,
411        kind: match symbol.kind {
412            squawk_ide::document_symbols::DocumentSymbolKind::Schema => "schema",
413            squawk_ide::document_symbols::DocumentSymbolKind::Table => "table",
414            squawk_ide::document_symbols::DocumentSymbolKind::View => "view",
415            squawk_ide::document_symbols::DocumentSymbolKind::MaterializedView => {
416                "materialized_view"
417            }
418            squawk_ide::document_symbols::DocumentSymbolKind::Function => "function",
419            squawk_ide::document_symbols::DocumentSymbolKind::Aggregate => "aggregate",
420            squawk_ide::document_symbols::DocumentSymbolKind::Procedure => "procedure",
421            squawk_ide::document_symbols::DocumentSymbolKind::EventTrigger => "event_trigger",
422            squawk_ide::document_symbols::DocumentSymbolKind::Role => "role",
423            squawk_ide::document_symbols::DocumentSymbolKind::Policy => "policy",
424            squawk_ide::document_symbols::DocumentSymbolKind::Type => "type",
425            squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum",
426            squawk_ide::document_symbols::DocumentSymbolKind::Index => "index",
427            squawk_ide::document_symbols::DocumentSymbolKind::Domain => "domain",
428            squawk_ide::document_symbols::DocumentSymbolKind::Sequence => "sequence",
429            squawk_ide::document_symbols::DocumentSymbolKind::Trigger => "trigger",
430            squawk_ide::document_symbols::DocumentSymbolKind::Tablespace => "tablespace",
431            squawk_ide::document_symbols::DocumentSymbolKind::Database => "database",
432            squawk_ide::document_symbols::DocumentSymbolKind::Server => "server",
433            squawk_ide::document_symbols::DocumentSymbolKind::Extension => "extension",
434            squawk_ide::document_symbols::DocumentSymbolKind::Column => "column",
435            squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant",
436            squawk_ide::document_symbols::DocumentSymbolKind::Cursor => "cursor",
437            squawk_ide::document_symbols::DocumentSymbolKind::PreparedStatement => {
438                "prepared_statement"
439            }
440            squawk_ide::document_symbols::DocumentSymbolKind::Channel => "channel",
441        }
442        .to_string(),
443        start_line: full_start_wide.line,
444        start_column: full_start_wide.col,
445        end_line: full_end_wide.line,
446        end_column: full_end_wide.col,
447        selection_start_line: focus_start_wide.line,
448        selection_start_column: focus_start_wide.col,
449        selection_end_line: focus_end_wide.line,
450        selection_end_column: focus_end_wide.col,
451        children: symbol
452            .children
453            .into_iter()
454            .map(|child| convert_document_symbol(line_index, child))
455            .collect(),
456    }
457}
458
459#[wasm_bindgen]
460pub fn inlay_hints(content: String) -> Result<JsValue, Error> {
461    let parse = squawk_syntax::SourceFile::parse(&content);
462    let line_index = LineIndex::new(&content);
463    let hints = squawk_ide::inlay_hints::inlay_hints(&parse.tree());
464
465    let converted: Vec<WasmInlayHint> = hints
466        .into_iter()
467        .map(|hint| {
468            let position = line_index.line_col(hint.position);
469            let position_wide = line_index
470                .to_wide(line_index::WideEncoding::Utf16, position)
471                .unwrap();
472
473            WasmInlayHint {
474                line: position_wide.line,
475                column: position_wide.col,
476                label: hint.label,
477                kind: match hint.kind {
478                    squawk_ide::inlay_hints::InlayHintKind::Type => "type",
479                    squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter",
480                }
481                .to_string(),
482            }
483        })
484        .collect();
485
486    serde_wasm_bindgen::to_value(&converted).map_err(into_error)
487}
488
489#[derive(Deserialize)]
490struct Position {
491    line: u32,
492    column: u32,
493}
494
495#[wasm_bindgen]
496pub fn selection_ranges(content: String, positions: Vec<JsValue>) -> Result<JsValue, Error> {
497    let parse = squawk_syntax::SourceFile::parse(&content);
498    let line_index = LineIndex::new(&content);
499    let tree = parse.tree();
500    let root = tree.syntax();
501
502    let mut results: Vec<Vec<WasmSelectionRange>> = vec![];
503
504    for pos in positions {
505        let pos: Position = serde_wasm_bindgen::from_value(pos).map_err(into_error)?;
506        let offset = position_to_offset(&line_index, pos.line, pos.column)?;
507
508        let mut ranges = vec![];
509        let mut range = TextRange::new(offset, offset);
510
511        for _ in 0..20 {
512            let next = squawk_ide::expand_selection::extend_selection(root, range);
513            if next == range {
514                break;
515            }
516
517            let start = line_index.line_col(next.start());
518            let end = line_index.line_col(next.end());
519            let start_wide = line_index
520                .to_wide(line_index::WideEncoding::Utf16, start)
521                .unwrap();
522            let end_wide = line_index
523                .to_wide(line_index::WideEncoding::Utf16, end)
524                .unwrap();
525
526            ranges.push(WasmSelectionRange {
527                start_line: start_wide.line,
528                start_column: start_wide.col,
529                end_line: end_wide.line,
530                end_column: end_wide.col,
531            });
532
533            range = next;
534        }
535
536        results.push(ranges);
537    }
538
539    serde_wasm_bindgen::to_value(&results).map_err(into_error)
540}
541
542#[derive(Serialize)]
543struct WasmInlayHint {
544    line: u32,
545    column: u32,
546    label: String,
547    kind: String,
548}
549
550#[derive(Serialize)]
551struct WasmSelectionRange {
552    start_line: u32,
553    start_column: u32,
554    end_line: u32,
555    end_column: u32,
556}
557
558#[wasm_bindgen]
559pub fn completion(content: String, line: u32, col: u32) -> Result<JsValue, Error> {
560    let parse = squawk_syntax::SourceFile::parse(&content);
561    let line_index = LineIndex::new(&content);
562    let offset = position_to_offset(&line_index, line, col)?;
563    let items = squawk_ide::completion::completion(&parse.tree(), offset);
564
565    let converted: Vec<WasmCompletionItem> = items
566        .into_iter()
567        .map(|item| WasmCompletionItem {
568            label: item.label,
569            kind: match item.kind {
570                squawk_ide::completion::CompletionItemKind::Keyword => "keyword",
571                squawk_ide::completion::CompletionItemKind::Table => "table",
572                squawk_ide::completion::CompletionItemKind::Column => "column",
573                squawk_ide::completion::CompletionItemKind::Function => "function",
574                squawk_ide::completion::CompletionItemKind::Schema => "schema",
575                squawk_ide::completion::CompletionItemKind::Type => "type",
576                squawk_ide::completion::CompletionItemKind::Snippet => "snippet",
577                squawk_ide::completion::CompletionItemKind::Operator => "operator",
578            }
579            .to_string(),
580            detail: item.detail,
581            insert_text: item.insert_text,
582            insert_text_format: item.insert_text_format.map(|fmt| {
583                match fmt {
584                    squawk_ide::completion::CompletionInsertTextFormat::PlainText => "plainText",
585                    squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet",
586                }
587                .to_string()
588            }),
589            trigger_completion_after_insert: item.trigger_completion_after_insert,
590        })
591        .collect();
592
593    serde_wasm_bindgen::to_value(&converted).map_err(into_error)
594}
595
596#[derive(Serialize)]
597struct WasmCompletionItem {
598    label: String,
599    kind: String,
600    detail: Option<String>,
601    insert_text: Option<String>,
602    insert_text_format: Option<String>,
603    trigger_completion_after_insert: bool,
604}