Skip to main content

squawk_wasm/
lib.rs

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