Skip to main content

squawk_wasm/
lib.rs

1use line_index::LineIndex;
2use log::info;
3use rowan::{TextRange, TextSize};
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_ide::semantic_tokens::{SemanticTokenType, semantic_tokens};
11use squawk_syntax::ast::AstNode;
12use wasm_bindgen::prelude::*;
13use web_sys::js_sys::Error;
14
15const SEMANTIC_TOKEN_TYPES: &[&str] = &[
16    "comment",
17    "function",
18    "keyword",
19    "namespace",
20    "number",
21    "operator",
22    "parameter",
23    "property",
24    "string",
25    "struct",
26    "type",
27    "variable",
28];
29
30const SEMANTIC_TOKEN_MODIFIERS: &[&str] = &["declaration", "definition", "readonly"];
31
32fn semantic_token_type_name(ty: SemanticTokenType) -> &'static str {
33    match ty {
34        SemanticTokenType::Bool | SemanticTokenType::Keyword => "keyword",
35        SemanticTokenType::Column => "variable",
36        SemanticTokenType::Comment => "comment",
37        SemanticTokenType::Function => "function",
38        SemanticTokenType::Name | SemanticTokenType::NameRef => "variable",
39        SemanticTokenType::Number => "number",
40        SemanticTokenType::Operator | SemanticTokenType::Punctuation => "operator",
41        SemanticTokenType::Parameter | SemanticTokenType::PositionalParam => "parameter",
42        SemanticTokenType::Schema => "namespace",
43        SemanticTokenType::String => "string",
44        SemanticTokenType::Table => "struct",
45        SemanticTokenType::Type => "type",
46    }
47}
48
49fn semantic_token_type_index(ty: SemanticTokenType) -> u32 {
50    let name = semantic_token_type_name(ty);
51    SEMANTIC_TOKEN_TYPES
52        .iter()
53        .position(|it| *it == name)
54        .unwrap() as u32
55}
56
57struct EncodedSemanticToken {
58    line: u32,
59    start: u32,
60    length: u32,
61    token_type: SemanticTokenType,
62    modifiers: u32,
63}
64
65struct SemanticTokenEncoder {
66    data: Vec<u32>,
67    prev_line: u32,
68    prev_start: u32,
69}
70
71impl SemanticTokenEncoder {
72    fn with_capacity(token_count: usize) -> Self {
73        Self {
74            data: Vec::with_capacity(token_count * 5),
75            prev_line: 0,
76            prev_start: 0,
77        }
78    }
79
80    fn push(&mut self, token: EncodedSemanticToken) {
81        let delta_line = token.line - self.prev_line;
82        let delta_start = if delta_line == 0 {
83            token.start - self.prev_start
84        } else {
85            token.start
86        };
87
88        self.data.extend_from_slice(&[
89            delta_line,
90            delta_start,
91            token.length,
92            semantic_token_type_index(token.token_type),
93            token.modifiers,
94        ]);
95
96        self.prev_line = token.line;
97        self.prev_start = token.start;
98    }
99
100    fn finish(self) -> Vec<u32> {
101        self.data
102    }
103}
104
105#[wasm_bindgen(start)]
106pub fn run() {
107    use log::Level;
108
109    // When the `console_error_panic_hook` feature is enabled, we can call the
110    // `set_panic_hook` function at least once during initialization, and then
111    // we will get better error messages if our code ever panics.
112    //
113    // For more details see
114    // https://github.com/rustwasm/console_error_panic_hook#readme
115    #[cfg(feature = "console_error_panic_hook")]
116    console_error_panic_hook::set_once();
117    console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
118    info!("init!");
119}
120
121#[wasm_bindgen]
122pub struct SquawkDatabase {
123    db: Database,
124    file: Option<File>,
125}
126
127#[wasm_bindgen]
128#[allow(clippy::new_without_default)]
129impl SquawkDatabase {
130    #[wasm_bindgen(constructor)]
131    pub fn new() -> SquawkDatabase {
132        SquawkDatabase {
133            db: Database::default(),
134            file: None,
135        }
136    }
137
138    pub fn open_file(&mut self, content: String) {
139        let file = File::new(&self.db, content.into());
140        self.file = Some(file);
141    }
142
143    pub fn update_file(&mut self, content: String) {
144        if let Some(file) = self.file {
145            file.set_content(&mut self.db).to(content.into());
146        }
147    }
148
149    fn file(&self) -> Result<File, Error> {
150        self.file
151            .ok_or_else(|| Error::new("No file open. Call open_file first."))
152    }
153
154    pub fn dump_cst(&self) -> Result<String, Error> {
155        let file = self.file()?;
156        let parse = db::parse(&self.db, file);
157        Ok(format!("{:#?}", parse.syntax_node()))
158    }
159
160    pub fn dump_tokens(&self) -> Result<String, Error> {
161        let file = self.file()?;
162        let content = file.content(&self.db);
163        let tokens = squawk_lexer::tokenize(content);
164        let mut start = 0;
165        let mut out = String::new();
166        for token in tokens {
167            let end = start + token.len;
168            let text = &content[start as usize..(end) as usize];
169            out += &format!("{:?}@{start}..{end} {:?}\n", token.kind, text);
170            start += token.len;
171        }
172        Ok(out)
173    }
174
175    pub fn lint(&self) -> Result<JsValue, Error> {
176        let file = self.file()?;
177        let content = file.content(&self.db);
178        let mut linter = squawk_linter::Linter::with_default_rules();
179        let parse = db::parse(&self.db, file);
180        let parse_errors = parse.errors();
181
182        let line_index = db::line_index(&self.db, file);
183
184        let parse_errors = parse_errors.iter().map(|x| {
185            let range_start = x.range().start();
186            let range_end = x.range().end();
187            let start = line_index.line_col(range_start);
188            let end = line_index.line_col(range_end);
189            let start = line_index
190                .to_wide(line_index::WideEncoding::Utf16, start)
191                .unwrap();
192            let end = line_index
193                .to_wide(line_index::WideEncoding::Utf16, end)
194                .unwrap();
195            LintError {
196                severity: Severity::Error,
197                code: "syntax-error".to_string(),
198                message: x.message().to_string(),
199                start_line_number: start.line,
200                start_column: start.col,
201                end_line_number: end.line,
202                end_column: end.col,
203                range_start: range_start.into(),
204                range_end: range_end.into(),
205                messages: vec![],
206                fix: None,
207            }
208        });
209
210        let lint_errors = linter.lint(&parse, content);
211        let errors = lint_errors.into_iter().map(|x| {
212            let start = line_index.line_col(x.text_range.start());
213            let end = line_index.line_col(x.text_range.end());
214            let start = line_index
215                .to_wide(line_index::WideEncoding::Utf16, start)
216                .unwrap();
217            let end = line_index
218                .to_wide(line_index::WideEncoding::Utf16, end)
219                .unwrap();
220
221            let messages = x.help.into_iter().collect();
222
223            let fix = x.fix.map(|fix| {
224                let edits = fix
225                    .edits
226                    .into_iter()
227                    .map(|edit| {
228                        let start_pos = line_index.line_col(edit.text_range.start());
229                        let end_pos = line_index.line_col(edit.text_range.end());
230                        let start_wide = line_index
231                            .to_wide(line_index::WideEncoding::Utf16, start_pos)
232                            .unwrap();
233                        let end_wide = line_index
234                            .to_wide(line_index::WideEncoding::Utf16, end_pos)
235                            .unwrap();
236
237                        TextEdit {
238                            start_line_number: start_wide.line,
239                            start_column: start_wide.col,
240                            end_line_number: end_wide.line,
241                            end_column: end_wide.col,
242                            text: edit.text.unwrap_or_default(),
243                        }
244                    })
245                    .collect();
246
247                Fix {
248                    title: fix.title,
249                    edits,
250                }
251            });
252
253            LintError {
254                code: x.code.to_string(),
255                range_start: x.text_range.start().into(),
256                range_end: x.text_range.end().into(),
257                message: x.message.clone(),
258                messages,
259                severity: Severity::Warning,
260                start_line_number: start.line,
261                start_column: start.col,
262                end_line_number: end.line,
263                end_column: end.col,
264                fix,
265            }
266        });
267
268        let mut errors_to_dump = errors.chain(parse_errors).collect::<Vec<_>>();
269        errors_to_dump.sort_by_key(|k| (k.start_line_number, k.start_column));
270
271        serde_wasm_bindgen::to_value(&errors_to_dump).map_err(into_error)
272    }
273
274    pub fn goto_definition(&self, line: u32, col: u32) -> Result<JsValue, Error> {
275        let file = self.file()?;
276        let current_line_index = db::line_index(&self.db, file);
277        let offset = position_to_offset(&current_line_index, line, col)?;
278        let builtins_li = builtins_line_index(&self.db);
279        let result = squawk_ide::goto_definition::goto_definition(&self.db, file, offset);
280
281        let response: Vec<LocationRange> = result
282            .into_iter()
283            .map(|location| {
284                let range = location.range;
285                let (file, line_index) = match location.file {
286                    FileId::Current => ("current", &current_line_index),
287                    FileId::Builtins => ("builtins", &builtins_li),
288                };
289                let start = line_index.line_col(range.start());
290                let end = line_index.line_col(range.end());
291                let start_wide = line_index
292                    .to_wide(line_index::WideEncoding::Utf16, start)
293                    .unwrap();
294                let end_wide = line_index
295                    .to_wide(line_index::WideEncoding::Utf16, end)
296                    .unwrap();
297
298                LocationRange {
299                    file: file.to_string(),
300                    start_line: start_wide.line,
301                    start_column: start_wide.col,
302                    end_line: end_wide.line,
303                    end_column: end_wide.col,
304                }
305            })
306            .collect();
307
308        serde_wasm_bindgen::to_value(&response).map_err(into_error)
309    }
310
311    pub fn hover(&self, line: u32, col: u32) -> Result<JsValue, Error> {
312        let file = self.file()?;
313        let line_index = db::line_index(&self.db, file);
314        let offset = position_to_offset(&line_index, line, col)?;
315        let result = squawk_ide::hover::hover(&self.db, file, offset);
316
317        let converted = result.map(|hover| WasmHover {
318            snippet: hover.snippet,
319            comment: hover.comment,
320        });
321
322        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
323    }
324
325    pub fn find_references(&self, line: u32, col: u32) -> Result<JsValue, Error> {
326        let file = self.file()?;
327        let line_index = db::line_index(&self.db, file);
328        let offset = position_to_offset(&line_index, line, col)?;
329        let references = squawk_ide::find_references::find_references(&self.db, file, offset);
330        let builtins_li = builtins_line_index(&self.db);
331        let locations: Vec<LocationRange> = references
332            .iter()
333            .map(|loc| {
334                let (li, file) = match loc.file {
335                    FileId::Current => (&line_index, "current"),
336                    FileId::Builtins => (&builtins_li, "builtins"),
337                };
338                let start = li.line_col(loc.range.start());
339                let end = li.line_col(loc.range.end());
340                let start_wide = li.to_wide(line_index::WideEncoding::Utf16, start).unwrap();
341                let end_wide = li.to_wide(line_index::WideEncoding::Utf16, end).unwrap();
342
343                LocationRange {
344                    file: file.to_string(),
345                    start_line: start_wide.line,
346                    start_column: start_wide.col,
347                    end_line: end_wide.line,
348                    end_column: end_wide.col,
349                }
350            })
351            .collect();
352
353        serde_wasm_bindgen::to_value(&locations).map_err(into_error)
354    }
355
356    pub fn document_symbols(&self) -> Result<JsValue, Error> {
357        let file = self.file()?;
358        let line_index = db::line_index(&self.db, file);
359        let symbols = squawk_ide::document_symbols::document_symbols(&self.db, file);
360
361        let converted: Vec<WasmDocumentSymbol> = symbols
362            .into_iter()
363            .map(|s| convert_document_symbol(&line_index, s))
364            .collect();
365
366        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
367    }
368
369    pub fn code_actions(&self, line: u32, col: u32) -> Result<JsValue, Error> {
370        let file = self.file()?;
371        let line_index = db::line_index(&self.db, file);
372        let offset = position_to_offset(&line_index, line, col)?;
373        let actions = squawk_ide::code_actions::code_actions(&self.db, file, offset);
374
375        let converted = actions.map(|actions| {
376            actions
377                .into_iter()
378                .map(|action| {
379                    let edits = action
380                        .edits
381                        .into_iter()
382                        .map(|edit| {
383                            let start_pos = line_index.line_col(edit.text_range.start());
384                            let end_pos = line_index.line_col(edit.text_range.end());
385                            let start_wide = line_index
386                                .to_wide(line_index::WideEncoding::Utf16, start_pos)
387                                .unwrap();
388                            let end_wide = line_index
389                                .to_wide(line_index::WideEncoding::Utf16, end_pos)
390                                .unwrap();
391
392                            TextEdit {
393                                start_line_number: start_wide.line,
394                                start_column: start_wide.col,
395                                end_line_number: end_wide.line,
396                                end_column: end_wide.col,
397                                text: edit.text.unwrap_or_default(),
398                            }
399                        })
400                        .collect();
401
402                    WasmCodeAction {
403                        title: action.title,
404                        edits,
405                        kind: match action.kind {
406                            squawk_ide::code_actions::ActionKind::QuickFix => "quickfix",
407                            squawk_ide::code_actions::ActionKind::RefactorRewrite => {
408                                "refactor.rewrite"
409                            }
410                        }
411                        .to_string(),
412                    }
413                })
414                .collect::<Vec<_>>()
415        });
416
417        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
418    }
419
420    pub fn inlay_hints(&self) -> Result<JsValue, Error> {
421        let file = self.file()?;
422        let line_index = db::line_index(&self.db, file);
423        let hints = squawk_ide::inlay_hints::inlay_hints(&self.db, file);
424
425        let converted: Vec<WasmInlayHint> = hints
426            .into_iter()
427            .map(|hint| {
428                let position = line_index.line_col(hint.position);
429                let position_wide = line_index
430                    .to_wide(line_index::WideEncoding::Utf16, position)
431                    .unwrap();
432
433                WasmInlayHint {
434                    line: position_wide.line,
435                    column: position_wide.col,
436                    label: hint.label,
437                    kind: match hint.kind {
438                        squawk_ide::inlay_hints::InlayHintKind::Type => "type",
439                        squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter",
440                    }
441                    .to_string(),
442                }
443            })
444            .collect();
445
446        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
447    }
448
449    pub fn folding_ranges(&self) -> Result<JsValue, Error> {
450        let file = self.file()?;
451        let line_index = db::line_index(&self.db, file);
452        let folds = folding_ranges(&self.db, file);
453
454        let converted: Vec<WasmFoldingRange> = folds
455            .into_iter()
456            .map(|fold| {
457                let start = line_index.line_col(fold.range.start());
458                let end = line_index.line_col(fold.range.end());
459                let start_wide = line_index
460                    .to_wide(line_index::WideEncoding::Utf16, start)
461                    .unwrap();
462                let end_wide = line_index
463                    .to_wide(line_index::WideEncoding::Utf16, end)
464                    .unwrap();
465
466                WasmFoldingRange {
467                    start_line: start_wide.line,
468                    end_line: end_wide.line,
469                    kind: match fold.kind {
470                        FoldKind::Comment => "comment",
471                        _ => "region",
472                    }
473                    .to_string(),
474                }
475            })
476            .collect();
477
478        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
479    }
480
481    pub fn selection_ranges(&self, positions: Vec<JsValue>) -> Result<JsValue, Error> {
482        let file = self.file()?;
483        let parse = db::parse(&self.db, file);
484        let line_index = db::line_index(&self.db, file);
485        let tree = parse.tree();
486        let root = tree.syntax();
487
488        let mut results: Vec<Vec<WasmSelectionRange>> = vec![];
489
490        for pos in positions {
491            let pos: Position = serde_wasm_bindgen::from_value(pos).map_err(into_error)?;
492            let offset = position_to_offset(&line_index, pos.line, pos.column)?;
493
494            let mut ranges = vec![];
495            let mut range = TextRange::new(offset, offset);
496
497            for _ in 0..20 {
498                let next = squawk_ide::expand_selection::extend_selection(root, range);
499                if next == range {
500                    break;
501                }
502
503                let start = line_index.line_col(next.start());
504                let end = line_index.line_col(next.end());
505                let start_wide = line_index
506                    .to_wide(line_index::WideEncoding::Utf16, start)
507                    .unwrap();
508                let end_wide = line_index
509                    .to_wide(line_index::WideEncoding::Utf16, end)
510                    .unwrap();
511
512                ranges.push(WasmSelectionRange {
513                    start_line: start_wide.line,
514                    start_column: start_wide.col,
515                    end_line: end_wide.line,
516                    end_column: end_wide.col,
517                });
518
519                range = next;
520            }
521
522            results.push(ranges);
523        }
524
525        serde_wasm_bindgen::to_value(&results).map_err(into_error)
526    }
527
528    pub fn semantic_tokens(&self) -> Result<Vec<u32>, Error> {
529        let file = self.file()?;
530        let line_index = db::line_index(&self.db, file);
531        let content = file.content(&self.db);
532        let tokens = semantic_tokens(&self.db, file, None);
533
534        let mut encoder = SemanticTokenEncoder::with_capacity(tokens.len());
535
536        // Duplicated from squawk-server, fyi
537        for token in &tokens {
538            // Taken from rust-analyzer, this solves the case where we have a
539            // multi line semantic token which isn't supported by the LSP spec.
540            // see: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/rust-analyzer/src/lsp/to_proto.rs#L781C28-L781C28
541            for mut text_range in line_index.lines(token.range) {
542                if content[text_range].ends_with('\n') {
543                    text_range =
544                        TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n'));
545                }
546                let start_lc = line_index.line_col(text_range.start());
547                let end_lc = line_index.line_col(text_range.end());
548                let start_wide = line_index
549                    .to_wide(line_index::WideEncoding::Utf16, start_lc)
550                    .unwrap();
551                let end_wide = line_index
552                    .to_wide(line_index::WideEncoding::Utf16, end_lc)
553                    .unwrap();
554
555                encoder.push(EncodedSemanticToken {
556                    line: start_wide.line,
557                    start: start_wide.col,
558                    length: end_wide.col - start_wide.col,
559                    token_type: token.token_type,
560                    // TODO: once we get modifiers going, we'll need to update this
561                    modifiers: 0,
562                });
563            }
564        }
565
566        Ok(encoder.finish())
567    }
568
569    pub fn semantic_tokens_legend() -> Result<JsValue, Error> {
570        let legend = SemanticTokensLegend {
571            token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
572            token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
573        };
574        serde_wasm_bindgen::to_value(&legend).map_err(into_error)
575    }
576
577    pub fn completion(&self, line: u32, col: u32) -> Result<JsValue, Error> {
578        let file = self.file()?;
579        let line_index = db::line_index(&self.db, file);
580        let offset = position_to_offset(&line_index, line, col)?;
581        let items = squawk_ide::completion::completion(&self.db, file, offset);
582
583        let converted: Vec<WasmCompletionItem> = items
584            .into_iter()
585            .map(|item| WasmCompletionItem {
586                label: item.label,
587                kind: match item.kind {
588                    squawk_ide::completion::CompletionItemKind::Keyword => "keyword",
589                    squawk_ide::completion::CompletionItemKind::Table => "table",
590                    squawk_ide::completion::CompletionItemKind::Column => "column",
591                    squawk_ide::completion::CompletionItemKind::Function => "function",
592                    squawk_ide::completion::CompletionItemKind::Schema => "schema",
593                    squawk_ide::completion::CompletionItemKind::Type => "type",
594                    squawk_ide::completion::CompletionItemKind::Snippet => "snippet",
595                    squawk_ide::completion::CompletionItemKind::Operator => "operator",
596                }
597                .to_string(),
598                detail: item.detail,
599                insert_text: item.insert_text,
600                insert_text_format: item.insert_text_format.map(|fmt| {
601                    match fmt {
602                        squawk_ide::completion::CompletionInsertTextFormat::PlainText => {
603                            "plainText"
604                        }
605                        squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet",
606                    }
607                    .to_string()
608                }),
609                trigger_completion_after_insert: item.trigger_completion_after_insert,
610            })
611            .collect();
612
613        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
614    }
615}
616
617fn into_error<E: std::fmt::Display>(err: E) -> Error {
618    Error::new(&err.to_string())
619}
620
621fn position_to_offset(
622    line_index: &LineIndex,
623    line: u32,
624    col: u32,
625) -> Result<rowan::TextSize, Error> {
626    let wide_pos = line_index::WideLineCol { line, col };
627
628    let pos = line_index
629        .to_utf8(line_index::WideEncoding::Utf16, wide_pos)
630        .ok_or_else(|| Error::new("Invalid position"))?;
631
632    line_index
633        .offset(pos)
634        .ok_or_else(|| Error::new("Invalid position offset"))
635}
636
637fn convert_document_symbol(
638    line_index: &LineIndex,
639    symbol: squawk_ide::document_symbols::DocumentSymbol,
640) -> WasmDocumentSymbol {
641    let full_start = line_index.line_col(symbol.full_range.start());
642    let full_end = line_index.line_col(symbol.full_range.end());
643    let full_start_wide = line_index
644        .to_wide(line_index::WideEncoding::Utf16, full_start)
645        .unwrap();
646    let full_end_wide = line_index
647        .to_wide(line_index::WideEncoding::Utf16, full_end)
648        .unwrap();
649
650    let focus_start = line_index.line_col(symbol.focus_range.start());
651    let focus_end = line_index.line_col(symbol.focus_range.end());
652    let focus_start_wide = line_index
653        .to_wide(line_index::WideEncoding::Utf16, focus_start)
654        .unwrap();
655    let focus_end_wide = line_index
656        .to_wide(line_index::WideEncoding::Utf16, focus_end)
657        .unwrap();
658
659    WasmDocumentSymbol {
660        name: symbol.name,
661        detail: symbol.detail,
662        kind: match symbol.kind {
663            squawk_ide::document_symbols::DocumentSymbolKind::Schema => "schema",
664            squawk_ide::document_symbols::DocumentSymbolKind::Table => "table",
665            squawk_ide::document_symbols::DocumentSymbolKind::View => "view",
666            squawk_ide::document_symbols::DocumentSymbolKind::MaterializedView => {
667                "materialized_view"
668            }
669            squawk_ide::document_symbols::DocumentSymbolKind::Function => "function",
670            squawk_ide::document_symbols::DocumentSymbolKind::Aggregate => "aggregate",
671            squawk_ide::document_symbols::DocumentSymbolKind::Procedure => "procedure",
672            squawk_ide::document_symbols::DocumentSymbolKind::EventTrigger => "event_trigger",
673            squawk_ide::document_symbols::DocumentSymbolKind::Role => "role",
674            squawk_ide::document_symbols::DocumentSymbolKind::Policy => "policy",
675            squawk_ide::document_symbols::DocumentSymbolKind::PropertyGraph => "property_graph",
676            squawk_ide::document_symbols::DocumentSymbolKind::Type => "type",
677            squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum",
678            squawk_ide::document_symbols::DocumentSymbolKind::Index => "index",
679            squawk_ide::document_symbols::DocumentSymbolKind::Domain => "domain",
680            squawk_ide::document_symbols::DocumentSymbolKind::Sequence => "sequence",
681            squawk_ide::document_symbols::DocumentSymbolKind::Trigger => "trigger",
682            squawk_ide::document_symbols::DocumentSymbolKind::Tablespace => "tablespace",
683            squawk_ide::document_symbols::DocumentSymbolKind::Database => "database",
684            squawk_ide::document_symbols::DocumentSymbolKind::Server => "server",
685            squawk_ide::document_symbols::DocumentSymbolKind::Extension => "extension",
686            squawk_ide::document_symbols::DocumentSymbolKind::Column => "column",
687            squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant",
688            squawk_ide::document_symbols::DocumentSymbolKind::Cursor => "cursor",
689            squawk_ide::document_symbols::DocumentSymbolKind::PreparedStatement => {
690                "prepared_statement"
691            }
692            squawk_ide::document_symbols::DocumentSymbolKind::Channel => "channel",
693        }
694        .to_string(),
695        start_line: full_start_wide.line,
696        start_column: full_start_wide.col,
697        end_line: full_end_wide.line,
698        end_column: full_end_wide.col,
699        selection_start_line: focus_start_wide.line,
700        selection_start_column: focus_start_wide.col,
701        selection_end_line: focus_end_wide.line,
702        selection_end_column: focus_end_wide.col,
703        children: symbol
704            .children
705            .into_iter()
706            .map(|child| convert_document_symbol(line_index, child))
707            .collect(),
708    }
709}
710
711#[expect(unused)]
712#[derive(Serialize)]
713enum Severity {
714    Hint,
715    Info,
716    Warning,
717    Error,
718}
719
720#[derive(Serialize)]
721struct LintError {
722    severity: Severity,
723    code: String,
724    message: String,
725    start_line_number: u32,
726    start_column: u32,
727    end_line_number: u32,
728    end_column: u32,
729    range_start: usize,
730    range_end: usize,
731    messages: Vec<String>,
732    fix: Option<Fix>,
733}
734
735#[derive(Serialize)]
736struct Fix {
737    title: String,
738    edits: Vec<TextEdit>,
739}
740
741#[derive(Serialize)]
742struct TextEdit {
743    start_line_number: u32,
744    start_column: u32,
745    end_line_number: u32,
746    end_column: u32,
747    text: String,
748}
749
750#[derive(Serialize)]
751struct LocationRange {
752    file: String,
753    start_line: u32,
754    start_column: u32,
755    end_line: u32,
756    end_column: u32,
757}
758
759#[derive(Serialize)]
760struct WasmCodeAction {
761    title: String,
762    edits: Vec<TextEdit>,
763    kind: String,
764}
765
766#[derive(Serialize)]
767struct WasmHover {
768    snippet: String,
769    comment: Option<String>,
770}
771
772#[derive(Serialize)]
773struct WasmDocumentSymbol {
774    name: String,
775    detail: Option<String>,
776    kind: String,
777    start_line: u32,
778    start_column: u32,
779    end_line: u32,
780    end_column: u32,
781    selection_start_line: u32,
782    selection_start_column: u32,
783    selection_end_line: u32,
784    selection_end_column: u32,
785    children: Vec<WasmDocumentSymbol>,
786}
787
788#[derive(Serialize)]
789struct WasmInlayHint {
790    line: u32,
791    column: u32,
792    label: String,
793    kind: String,
794}
795
796#[derive(Serialize)]
797struct WasmFoldingRange {
798    start_line: u32,
799    end_line: u32,
800    kind: String,
801}
802
803#[derive(Serialize)]
804struct WasmSelectionRange {
805    start_line: u32,
806    start_column: u32,
807    end_line: u32,
808    end_column: u32,
809}
810
811#[derive(Serialize)]
812struct SemanticTokensLegend {
813    #[serde(rename = "tokenTypes")]
814    token_types: Vec<&'static str>,
815    #[serde(rename = "tokenModifiers")]
816    token_modifiers: Vec<&'static str>,
817}
818
819#[derive(Serialize)]
820struct WasmCompletionItem {
821    label: String,
822    kind: String,
823    detail: Option<String>,
824    insert_text: Option<String>,
825    insert_text_format: Option<String>,
826    trigger_completion_after_insert: bool,
827}
828
829#[derive(Deserialize)]
830struct Position {
831    line: u32,
832    column: u32,
833}