squawk_wasm/
lib.rs

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    // When the `console_error_panic_hook` feature is enabled, we can call the
12    // `set_panic_hook` function at least once during initialization, and then
13    // we will get better error messages if our code ever panics.
14    //
15    // For more details see
16    // https://github.com/rustwasm/console_error_panic_hook#readme
17    #[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    // used for the linter tab
62    range_start: usize,
63    // used for the linter tab
64    range_end: usize,
65    // used for the linter tab
66    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    // TODO: chain these with other stuff
94    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            // parser errors should be error
170            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}