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::file::InFile;
9use squawk_ide::folding_ranges::{FoldKind, folding_ranges};
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::PropertyGraph | 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 position = position_to_offset(&self.db, file, line, col)?;
277        let result = squawk_ide::goto_definition::goto_definition(&self.db, position);
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 position = position_to_offset(&self.db, file, line, col)?;
310        let result = squawk_ide::hover::hover(&self.db, position);
311
312        let converted = result.map(|hover| WasmHover {
313            snippet: hover.snippet,
314            comment: hover.comment,
315        });
316
317        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
318    }
319
320    pub fn find_references(&self, line: u32, col: u32) -> Result<JsValue, Error> {
321        let file = self.file()?;
322        let position = position_to_offset(&self.db, file, line, col)?;
323        let references = squawk_ide::find_references::find_references(&self.db, position);
324        let locations: Vec<LocationRange> = references
325            .iter()
326            .map(|loc| {
327                let file = file_string(&self.db, loc.file);
328                let line_index = db::line_index(&self.db, loc.file);
329                let start = line_index.line_col(loc.range.start());
330                let end = line_index.line_col(loc.range.end());
331                let start_wide = line_index
332                    .to_wide(line_index::WideEncoding::Utf16, start)
333                    .unwrap();
334                let end_wide = line_index
335                    .to_wide(line_index::WideEncoding::Utf16, end)
336                    .unwrap();
337
338                LocationRange {
339                    file: file.to_string(),
340                    start_line: start_wide.line,
341                    start_column: start_wide.col,
342                    end_line: end_wide.line,
343                    end_column: end_wide.col,
344                }
345            })
346            .collect();
347
348        serde_wasm_bindgen::to_value(&locations).map_err(into_error)
349    }
350
351    pub fn document_symbols(&self) -> Result<JsValue, Error> {
352        let file = self.file()?;
353        let line_index = db::line_index(&self.db, file);
354        let symbols = squawk_ide::document_symbols::document_symbols(&self.db, file);
355
356        let converted: Vec<WasmDocumentSymbol> = symbols
357            .into_iter()
358            .map(|s| convert_document_symbol(&line_index, s))
359            .collect();
360
361        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
362    }
363
364    pub fn code_actions(&self, line: u32, col: u32) -> Result<JsValue, Error> {
365        let file = self.file()?;
366        let line_index = db::line_index(&self.db, file);
367        let position = position_to_offset(&self.db, file, line, col)?;
368        let actions = squawk_ide::code_actions::code_actions(&self.db, position);
369
370        let converted = actions.map(|actions| {
371            actions
372                .into_iter()
373                .map(|action| {
374                    let edits = action
375                        .edits
376                        .into_iter()
377                        .map(|edit| {
378                            let start_pos = line_index.line_col(edit.text_range.start());
379                            let end_pos = line_index.line_col(edit.text_range.end());
380                            let start_wide = line_index
381                                .to_wide(line_index::WideEncoding::Utf16, start_pos)
382                                .unwrap();
383                            let end_wide = line_index
384                                .to_wide(line_index::WideEncoding::Utf16, end_pos)
385                                .unwrap();
386
387                            TextEdit {
388                                start_line_number: start_wide.line,
389                                start_column: start_wide.col,
390                                end_line_number: end_wide.line,
391                                end_column: end_wide.col,
392                                text: edit.text.unwrap_or_default(),
393                            }
394                        })
395                        .collect();
396
397                    WasmCodeAction {
398                        title: action.title,
399                        edits,
400                        kind: match action.kind {
401                            squawk_ide::code_actions::ActionKind::QuickFix => "quickfix",
402                            squawk_ide::code_actions::ActionKind::RefactorRewrite => {
403                                "refactor.rewrite"
404                            }
405                        }
406                        .to_string(),
407                    }
408                })
409                .collect::<Vec<_>>()
410        });
411
412        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
413    }
414
415    pub fn inlay_hints(&self) -> Result<JsValue, Error> {
416        let file = self.file()?;
417        let line_index = db::line_index(&self.db, file);
418        let hints = squawk_ide::inlay_hints::inlay_hints(&self.db, file);
419
420        let converted: Vec<WasmInlayHint> = hints
421            .into_iter()
422            .map(|hint| {
423                let position = line_index.line_col(hint.position);
424                let position_wide = line_index
425                    .to_wide(line_index::WideEncoding::Utf16, position)
426                    .unwrap();
427
428                WasmInlayHint {
429                    line: position_wide.line,
430                    column: position_wide.col,
431                    label: hint.label,
432                    kind: match hint.kind {
433                        squawk_ide::inlay_hints::InlayHintKind::Type => "type",
434                        squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter",
435                    }
436                    .to_string(),
437                }
438            })
439            .collect();
440
441        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
442    }
443
444    pub fn folding_ranges(&self) -> Result<JsValue, Error> {
445        let file = self.file()?;
446        let line_index = db::line_index(&self.db, file);
447        let folds = folding_ranges(&self.db, file);
448
449        let converted: Vec<WasmFoldingRange> = folds
450            .into_iter()
451            .map(|fold| {
452                let start = line_index.line_col(fold.range.start());
453                let end = line_index.line_col(fold.range.end());
454                let start_wide = line_index
455                    .to_wide(line_index::WideEncoding::Utf16, start)
456                    .unwrap();
457                let end_wide = line_index
458                    .to_wide(line_index::WideEncoding::Utf16, end)
459                    .unwrap();
460
461                WasmFoldingRange {
462                    start_line: start_wide.line,
463                    end_line: end_wide.line,
464                    kind: match fold.kind {
465                        FoldKind::Comment => "comment",
466                        _ => "region",
467                    }
468                    .to_string(),
469                }
470            })
471            .collect();
472
473        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
474    }
475
476    pub fn selection_ranges(&self, positions: Vec<JsValue>) -> Result<JsValue, Error> {
477        let file = self.file()?;
478        let parse = db::parse(&self.db, file);
479        let line_index = db::line_index(&self.db, file);
480        let tree = parse.tree();
481        let root = tree.syntax();
482
483        let mut results: Vec<Vec<WasmSelectionRange>> = vec![];
484
485        for pos in positions {
486            let pos: Position = serde_wasm_bindgen::from_value(pos).map_err(into_error)?;
487            let offset = position_to_offset(&self.db, file, pos.line, pos.column)?.value;
488
489            let mut ranges = vec![];
490            let mut range = TextRange::new(offset, offset);
491
492            for _ in 0..20 {
493                let next = squawk_ide::expand_selection::extend_selection(root, range);
494                if next == range {
495                    break;
496                }
497
498                let start = line_index.line_col(next.start());
499                let end = line_index.line_col(next.end());
500                let start_wide = line_index
501                    .to_wide(line_index::WideEncoding::Utf16, start)
502                    .unwrap();
503                let end_wide = line_index
504                    .to_wide(line_index::WideEncoding::Utf16, end)
505                    .unwrap();
506
507                ranges.push(WasmSelectionRange {
508                    start_line: start_wide.line,
509                    start_column: start_wide.col,
510                    end_line: end_wide.line,
511                    end_column: end_wide.col,
512                });
513
514                range = next;
515            }
516
517            results.push(ranges);
518        }
519
520        serde_wasm_bindgen::to_value(&results).map_err(into_error)
521    }
522
523    pub fn semantic_tokens(&self) -> Result<Vec<u32>, Error> {
524        let file = self.file()?;
525        let line_index = db::line_index(&self.db, file);
526        let content = file.content(&self.db);
527        let tokens = semantic_tokens(&self.db, file, None);
528
529        let mut encoder = SemanticTokenEncoder::with_capacity(tokens.len());
530
531        // Duplicated from squawk-server, fyi
532        for token in &tokens {
533            // Taken from rust-analyzer, this solves the case where we have a
534            // multi line semantic token which isn't supported by the LSP spec.
535            // see: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/rust-analyzer/src/lsp/to_proto.rs#L781C28-L781C28
536            for mut text_range in line_index.lines(token.range) {
537                if content[text_range].ends_with('\n') {
538                    text_range =
539                        TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n'));
540                }
541                let start_lc = line_index.line_col(text_range.start());
542                let end_lc = line_index.line_col(text_range.end());
543                let start_wide = line_index
544                    .to_wide(line_index::WideEncoding::Utf16, start_lc)
545                    .unwrap();
546                let end_wide = line_index
547                    .to_wide(line_index::WideEncoding::Utf16, end_lc)
548                    .unwrap();
549
550                encoder.push(EncodedSemanticToken {
551                    line: start_wide.line,
552                    start: start_wide.col,
553                    length: end_wide.col - start_wide.col,
554                    token_type: token.token_type,
555                    // TODO: once we get modifiers going, we'll need to update this
556                    modifiers: 0,
557                });
558            }
559        }
560
561        Ok(encoder.finish())
562    }
563
564    pub fn semantic_tokens_legend() -> Result<JsValue, Error> {
565        let legend = SemanticTokensLegend {
566            token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
567            token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
568        };
569        serde_wasm_bindgen::to_value(&legend).map_err(into_error)
570    }
571
572    pub fn completion(&self, line: u32, col: u32) -> Result<JsValue, Error> {
573        let file = self.file()?;
574        let position = position_to_offset(&self.db, file, line, col)?;
575        let items = squawk_ide::completion::completion(&self.db, position);
576
577        let converted: Vec<WasmCompletionItem> = items
578            .into_iter()
579            .map(|item| WasmCompletionItem {
580                label: item.label,
581                kind: match item.kind {
582                    squawk_ide::completion::CompletionItemKind::Keyword => "keyword",
583                    squawk_ide::completion::CompletionItemKind::Table => "table",
584                    squawk_ide::completion::CompletionItemKind::Column => "column",
585                    squawk_ide::completion::CompletionItemKind::Function => "function",
586                    squawk_ide::completion::CompletionItemKind::Schema => "schema",
587                    squawk_ide::completion::CompletionItemKind::Type => "type",
588                    squawk_ide::completion::CompletionItemKind::Snippet => "snippet",
589                    squawk_ide::completion::CompletionItemKind::Operator => "operator",
590                }
591                .to_string(),
592                detail: item.detail,
593                insert_text: item.insert_text,
594                insert_text_format: item.insert_text_format.map(|fmt| {
595                    match fmt {
596                        squawk_ide::completion::CompletionInsertTextFormat::PlainText => {
597                            "plainText"
598                        }
599                        squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet",
600                    }
601                    .to_string()
602                }),
603                trigger_completion_after_insert: item.trigger_completion_after_insert,
604            })
605            .collect();
606
607        serde_wasm_bindgen::to_value(&converted).map_err(into_error)
608    }
609}
610
611fn file_string(db: &Database, file: File) -> &'static str {
612    // TODO: this will need to change when we add extension support
613    if file == builtins_file(db) {
614        "builtins"
615    } else {
616        "current"
617    }
618}
619
620fn into_error<E: std::fmt::Display>(err: E) -> Error {
621    Error::new(&err.to_string())
622}
623
624fn position_to_offset(
625    db: &Database,
626    file: File,
627    line: u32,
628    col: u32,
629) -> Result<InFile<rowan::TextSize>, Error> {
630    let line_index = db::line_index(db, file);
631    let wide_pos = line_index::WideLineCol { line, col };
632
633    let pos = line_index
634        .to_utf8(line_index::WideEncoding::Utf16, wide_pos)
635        .ok_or_else(|| Error::new("Invalid position"))?;
636
637    let offset = line_index
638        .offset(pos)
639        .ok_or_else(|| Error::new("Invalid position offset"))?;
640    Ok(InFile::new(file, 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::Rule => "rule",
681            squawk_ide::document_symbols::DocumentSymbolKind::Policy => "policy",
682            squawk_ide::document_symbols::DocumentSymbolKind::PropertyGraph => "property_graph",
683            squawk_ide::document_symbols::DocumentSymbolKind::Type => "type",
684            squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum",
685            squawk_ide::document_symbols::DocumentSymbolKind::Index => "index",
686            squawk_ide::document_symbols::DocumentSymbolKind::Domain => "domain",
687            squawk_ide::document_symbols::DocumentSymbolKind::Sequence => "sequence",
688            squawk_ide::document_symbols::DocumentSymbolKind::Trigger => "trigger",
689            squawk_ide::document_symbols::DocumentSymbolKind::Tablespace => "tablespace",
690            squawk_ide::document_symbols::DocumentSymbolKind::Database => "database",
691            squawk_ide::document_symbols::DocumentSymbolKind::Server => "server",
692            squawk_ide::document_symbols::DocumentSymbolKind::Extension => "extension",
693            squawk_ide::document_symbols::DocumentSymbolKind::Column => "column",
694            squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant",
695            squawk_ide::document_symbols::DocumentSymbolKind::Cursor => "cursor",
696            squawk_ide::document_symbols::DocumentSymbolKind::PreparedStatement => {
697                "prepared_statement"
698            }
699            squawk_ide::document_symbols::DocumentSymbolKind::Channel => "channel",
700        }
701        .to_string(),
702        start_line: full_start_wide.line,
703        start_column: full_start_wide.col,
704        end_line: full_end_wide.line,
705        end_column: full_end_wide.col,
706        selection_start_line: focus_start_wide.line,
707        selection_start_column: focus_start_wide.col,
708        selection_end_line: focus_end_wide.line,
709        selection_end_column: focus_end_wide.col,
710        children: symbol
711            .children
712            .into_iter()
713            .map(|child| convert_document_symbol(line_index, child))
714            .collect(),
715    }
716}
717
718#[expect(unused)]
719#[derive(Serialize)]
720enum Severity {
721    Hint,
722    Info,
723    Warning,
724    Error,
725}
726
727#[derive(Serialize)]
728struct LintError {
729    severity: Severity,
730    code: String,
731    message: String,
732    start_line_number: u32,
733    start_column: u32,
734    end_line_number: u32,
735    end_column: u32,
736    range_start: usize,
737    range_end: usize,
738    messages: Vec<String>,
739    fix: Option<Fix>,
740}
741
742#[derive(Serialize)]
743struct Fix {
744    title: String,
745    edits: Vec<TextEdit>,
746}
747
748#[derive(Serialize)]
749struct TextEdit {
750    start_line_number: u32,
751    start_column: u32,
752    end_line_number: u32,
753    end_column: u32,
754    text: String,
755}
756
757#[derive(Serialize)]
758struct LocationRange {
759    file: String,
760    start_line: u32,
761    start_column: u32,
762    end_line: u32,
763    end_column: u32,
764}
765
766#[derive(Serialize)]
767struct WasmCodeAction {
768    title: String,
769    edits: Vec<TextEdit>,
770    kind: String,
771}
772
773#[derive(Serialize)]
774struct WasmHover {
775    snippet: String,
776    comment: Option<String>,
777}
778
779#[derive(Serialize)]
780struct WasmDocumentSymbol {
781    name: String,
782    detail: Option<String>,
783    kind: String,
784    start_line: u32,
785    start_column: u32,
786    end_line: u32,
787    end_column: u32,
788    selection_start_line: u32,
789    selection_start_column: u32,
790    selection_end_line: u32,
791    selection_end_column: u32,
792    children: Vec<WasmDocumentSymbol>,
793}
794
795#[derive(Serialize)]
796struct WasmInlayHint {
797    line: u32,
798    column: u32,
799    label: String,
800    kind: String,
801}
802
803#[derive(Serialize)]
804struct WasmFoldingRange {
805    start_line: u32,
806    end_line: u32,
807    kind: String,
808}
809
810#[derive(Serialize)]
811struct WasmSelectionRange {
812    start_line: u32,
813    start_column: u32,
814    end_line: u32,
815    end_column: u32,
816}
817
818#[derive(Serialize)]
819struct SemanticTokensLegend {
820    #[serde(rename = "tokenTypes")]
821    token_types: Vec<&'static str>,
822    #[serde(rename = "tokenModifiers")]
823    token_modifiers: Vec<&'static str>,
824}
825
826#[derive(Serialize)]
827struct WasmCompletionItem {
828    label: String,
829    kind: String,
830    detail: Option<String>,
831    insert_text: Option<String>,
832    insert_text_format: Option<String>,
833    trigger_completion_after_insert: bool,
834}
835
836#[derive(Deserialize)]
837struct Position {
838    line: u32,
839    column: u32,
840}