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