Skip to main content

squawk_wasm/
lib.rs

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