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