1use line_index::LineIndex;
2use log::info;
3use rowan::TextRange;
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_syntax::ast::AstNode;
11use wasm_bindgen::prelude::*;
12use web_sys::js_sys::Error;
13
14#[wasm_bindgen(start)]
15pub fn run() {
16 use log::Level;
17
18 #[cfg(feature = "console_error_panic_hook")]
25 console_error_panic_hook::set_once();
26 console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
27 info!("init!");
28}
29
30#[wasm_bindgen]
31pub struct SquawkDatabase {
32 db: Database,
33 file: Option<File>,
34}
35
36#[wasm_bindgen]
37#[allow(clippy::new_without_default)]
38impl SquawkDatabase {
39 #[wasm_bindgen(constructor)]
40 pub fn new() -> SquawkDatabase {
41 SquawkDatabase {
42 db: Database::default(),
43 file: None,
44 }
45 }
46
47 pub fn open_file(&mut self, content: String) {
48 let file = File::new(&self.db, content.into());
49 self.file = Some(file);
50 }
51
52 pub fn update_file(&mut self, content: String) {
53 if let Some(file) = self.file {
54 file.set_content(&mut self.db).to(content.into());
55 }
56 }
57
58 fn file(&self) -> Result<File, Error> {
59 self.file
60 .ok_or_else(|| Error::new("No file open. Call open_file first."))
61 }
62
63 pub fn dump_cst(&self) -> Result<String, Error> {
64 let file = self.file()?;
65 let parse = db::parse(&self.db, file);
66 Ok(format!("{:#?}", parse.syntax_node()))
67 }
68
69 pub fn dump_tokens(&self) -> Result<String, Error> {
70 let file = self.file()?;
71 let content = file.content(&self.db);
72 let tokens = squawk_lexer::tokenize(content);
73 let mut start = 0;
74 let mut out = String::new();
75 for token in tokens {
76 let end = start + token.len;
77 let text = &content[start as usize..(end) as usize];
78 out += &format!("{:?}@{start}..{end} {:?}\n", token.kind, text);
79 start += token.len;
80 }
81 Ok(out)
82 }
83
84 pub fn lint(&self) -> Result<JsValue, Error> {
85 let file = self.file()?;
86 let content = file.content(&self.db);
87 let mut linter = squawk_linter::Linter::with_all_rules();
88 let parse = db::parse(&self.db, file);
89 let parse_errors = parse.errors();
90
91 let line_index = db::line_index(&self.db, file);
92
93 let parse_errors = parse_errors.iter().map(|x| {
94 let range_start = x.range().start();
95 let range_end = x.range().end();
96 let start = line_index.line_col(range_start);
97 let end = line_index.line_col(range_end);
98 let start = line_index
99 .to_wide(line_index::WideEncoding::Utf16, start)
100 .unwrap();
101 let end = line_index
102 .to_wide(line_index::WideEncoding::Utf16, end)
103 .unwrap();
104 LintError {
105 severity: Severity::Error,
106 code: "syntax-error".to_string(),
107 message: x.message().to_string(),
108 start_line_number: start.line,
109 start_column: start.col,
110 end_line_number: end.line,
111 end_column: end.col,
112 range_start: range_start.into(),
113 range_end: range_end.into(),
114 messages: vec![],
115 fix: None,
116 }
117 });
118
119 let lint_errors = linter.lint(&parse, content);
120 let errors = lint_errors.into_iter().map(|x| {
121 let start = line_index.line_col(x.text_range.start());
122 let end = line_index.line_col(x.text_range.end());
123 let start = line_index
124 .to_wide(line_index::WideEncoding::Utf16, start)
125 .unwrap();
126 let end = line_index
127 .to_wide(line_index::WideEncoding::Utf16, end)
128 .unwrap();
129
130 let messages = x.help.into_iter().collect();
131
132 let fix = x.fix.map(|fix| {
133 let edits = fix
134 .edits
135 .into_iter()
136 .map(|edit| {
137 let start_pos = line_index.line_col(edit.text_range.start());
138 let end_pos = line_index.line_col(edit.text_range.end());
139 let start_wide = line_index
140 .to_wide(line_index::WideEncoding::Utf16, start_pos)
141 .unwrap();
142 let end_wide = line_index
143 .to_wide(line_index::WideEncoding::Utf16, end_pos)
144 .unwrap();
145
146 TextEdit {
147 start_line_number: start_wide.line,
148 start_column: start_wide.col,
149 end_line_number: end_wide.line,
150 end_column: end_wide.col,
151 text: edit.text.unwrap_or_default(),
152 }
153 })
154 .collect();
155
156 Fix {
157 title: fix.title,
158 edits,
159 }
160 });
161
162 LintError {
163 code: x.code.to_string(),
164 range_start: x.text_range.start().into(),
165 range_end: x.text_range.end().into(),
166 message: x.message.clone(),
167 messages,
168 severity: Severity::Warning,
169 start_line_number: start.line,
170 start_column: start.col,
171 end_line_number: end.line,
172 end_column: end.col,
173 fix,
174 }
175 });
176
177 let mut errors_to_dump = errors.chain(parse_errors).collect::<Vec<_>>();
178 errors_to_dump.sort_by_key(|k| (k.start_line_number, k.start_column));
179
180 serde_wasm_bindgen::to_value(&errors_to_dump).map_err(into_error)
181 }
182
183 pub fn goto_definition(&self, line: u32, col: u32) -> Result<JsValue, Error> {
184 let file = self.file()?;
185 let current_line_index = db::line_index(&self.db, file);
186 let offset = position_to_offset(¤t_line_index, line, col)?;
187 let builtins_li = builtins_line_index(&self.db);
188 let result = squawk_ide::goto_definition::goto_definition(&self.db, file, offset);
189
190 let response: Vec<LocationRange> = result
191 .into_iter()
192 .map(|location| {
193 let range = location.range;
194 let (file, line_index) = match location.file {
195 FileId::Current => ("current", ¤t_line_index),
196 FileId::Builtins => ("builtins", &builtins_li),
197 };
198 let start = line_index.line_col(range.start());
199 let end = line_index.line_col(range.end());
200 let start_wide = line_index
201 .to_wide(line_index::WideEncoding::Utf16, start)
202 .unwrap();
203 let end_wide = line_index
204 .to_wide(line_index::WideEncoding::Utf16, end)
205 .unwrap();
206
207 LocationRange {
208 file: file.to_string(),
209 start_line: start_wide.line,
210 start_column: start_wide.col,
211 end_line: end_wide.line,
212 end_column: end_wide.col,
213 }
214 })
215 .collect();
216
217 serde_wasm_bindgen::to_value(&response).map_err(into_error)
218 }
219
220 pub fn hover(&self, line: u32, col: u32) -> Result<JsValue, Error> {
221 let file = self.file()?;
222 let line_index = db::line_index(&self.db, file);
223 let offset = position_to_offset(&line_index, line, col)?;
224 let result = squawk_ide::hover::hover(&self.db, file, offset);
225
226 serde_wasm_bindgen::to_value(&result).map_err(into_error)
227 }
228
229 pub fn find_references(&self, line: u32, col: u32) -> Result<JsValue, Error> {
230 let file = self.file()?;
231 let line_index = db::line_index(&self.db, file);
232 let offset = position_to_offset(&line_index, line, col)?;
233 let references = squawk_ide::find_references::find_references(&self.db, file, offset);
234 let builtins_li = builtins_line_index(&self.db);
235 let locations: Vec<LocationRange> = references
236 .iter()
237 .map(|loc| {
238 let (li, file) = match loc.file {
239 FileId::Current => (&line_index, "current"),
240 FileId::Builtins => (&builtins_li, "builtins"),
241 };
242 let start = li.line_col(loc.range.start());
243 let end = li.line_col(loc.range.end());
244 let start_wide = li.to_wide(line_index::WideEncoding::Utf16, start).unwrap();
245 let end_wide = li.to_wide(line_index::WideEncoding::Utf16, end).unwrap();
246
247 LocationRange {
248 file: file.to_string(),
249 start_line: start_wide.line,
250 start_column: start_wide.col,
251 end_line: end_wide.line,
252 end_column: end_wide.col,
253 }
254 })
255 .collect();
256
257 serde_wasm_bindgen::to_value(&locations).map_err(into_error)
258 }
259
260 pub fn document_symbols(&self) -> Result<JsValue, Error> {
261 let file = self.file()?;
262 let line_index = db::line_index(&self.db, file);
263 let symbols = squawk_ide::document_symbols::document_symbols(&self.db, file);
264
265 let converted: Vec<WasmDocumentSymbol> = symbols
266 .into_iter()
267 .map(|s| convert_document_symbol(&line_index, s))
268 .collect();
269
270 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
271 }
272
273 pub fn code_actions(&self, line: u32, col: u32) -> Result<JsValue, Error> {
274 let file = self.file()?;
275 let line_index = db::line_index(&self.db, file);
276 let offset = position_to_offset(&line_index, line, col)?;
277 let actions = squawk_ide::code_actions::code_actions(&self.db, file, offset);
278
279 let converted = actions.map(|actions| {
280 actions
281 .into_iter()
282 .map(|action| {
283 let edits = action
284 .edits
285 .into_iter()
286 .map(|edit| {
287 let start_pos = line_index.line_col(edit.text_range.start());
288 let end_pos = line_index.line_col(edit.text_range.end());
289 let start_wide = line_index
290 .to_wide(line_index::WideEncoding::Utf16, start_pos)
291 .unwrap();
292 let end_wide = line_index
293 .to_wide(line_index::WideEncoding::Utf16, end_pos)
294 .unwrap();
295
296 TextEdit {
297 start_line_number: start_wide.line,
298 start_column: start_wide.col,
299 end_line_number: end_wide.line,
300 end_column: end_wide.col,
301 text: edit.text.unwrap_or_default(),
302 }
303 })
304 .collect();
305
306 WasmCodeAction {
307 title: action.title,
308 edits,
309 kind: match action.kind {
310 squawk_ide::code_actions::ActionKind::QuickFix => "quickfix",
311 squawk_ide::code_actions::ActionKind::RefactorRewrite => {
312 "refactor.rewrite"
313 }
314 }
315 .to_string(),
316 }
317 })
318 .collect::<Vec<_>>()
319 });
320
321 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
322 }
323
324 pub fn inlay_hints(&self) -> Result<JsValue, Error> {
325 let file = self.file()?;
326 let line_index = db::line_index(&self.db, file);
327 let hints = squawk_ide::inlay_hints::inlay_hints(&self.db, file);
328
329 let converted: Vec<WasmInlayHint> = hints
330 .into_iter()
331 .map(|hint| {
332 let position = line_index.line_col(hint.position);
333 let position_wide = line_index
334 .to_wide(line_index::WideEncoding::Utf16, position)
335 .unwrap();
336
337 WasmInlayHint {
338 line: position_wide.line,
339 column: position_wide.col,
340 label: hint.label,
341 kind: match hint.kind {
342 squawk_ide::inlay_hints::InlayHintKind::Type => "type",
343 squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter",
344 }
345 .to_string(),
346 }
347 })
348 .collect();
349
350 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
351 }
352
353 pub fn folding_ranges(&self) -> Result<JsValue, Error> {
354 let file = self.file()?;
355 let line_index = db::line_index(&self.db, file);
356 let folds = folding_ranges(&self.db, file);
357
358 let converted: Vec<WasmFoldingRange> = folds
359 .into_iter()
360 .map(|fold| {
361 let start = line_index.line_col(fold.range.start());
362 let end = line_index.line_col(fold.range.end());
363 let start_wide = line_index
364 .to_wide(line_index::WideEncoding::Utf16, start)
365 .unwrap();
366 let end_wide = line_index
367 .to_wide(line_index::WideEncoding::Utf16, end)
368 .unwrap();
369
370 WasmFoldingRange {
371 start_line: start_wide.line,
372 end_line: end_wide.line,
373 kind: match fold.kind {
374 FoldKind::Comment => "comment",
375 _ => "region",
376 }
377 .to_string(),
378 }
379 })
380 .collect();
381
382 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
383 }
384
385 pub fn selection_ranges(&self, positions: Vec<JsValue>) -> Result<JsValue, Error> {
386 let file = self.file()?;
387 let parse = db::parse(&self.db, file);
388 let line_index = db::line_index(&self.db, file);
389 let tree = parse.tree();
390 let root = tree.syntax();
391
392 let mut results: Vec<Vec<WasmSelectionRange>> = vec![];
393
394 for pos in positions {
395 let pos: Position = serde_wasm_bindgen::from_value(pos).map_err(into_error)?;
396 let offset = position_to_offset(&line_index, pos.line, pos.column)?;
397
398 let mut ranges = vec![];
399 let mut range = TextRange::new(offset, offset);
400
401 for _ in 0..20 {
402 let next = squawk_ide::expand_selection::extend_selection(root, range);
403 if next == range {
404 break;
405 }
406
407 let start = line_index.line_col(next.start());
408 let end = line_index.line_col(next.end());
409 let start_wide = line_index
410 .to_wide(line_index::WideEncoding::Utf16, start)
411 .unwrap();
412 let end_wide = line_index
413 .to_wide(line_index::WideEncoding::Utf16, end)
414 .unwrap();
415
416 ranges.push(WasmSelectionRange {
417 start_line: start_wide.line,
418 start_column: start_wide.col,
419 end_line: end_wide.line,
420 end_column: end_wide.col,
421 });
422
423 range = next;
424 }
425
426 results.push(ranges);
427 }
428
429 serde_wasm_bindgen::to_value(&results).map_err(into_error)
430 }
431
432 pub fn completion(&self, line: u32, col: u32) -> Result<JsValue, Error> {
433 let file = self.file()?;
434 let line_index = db::line_index(&self.db, file);
435 let offset = position_to_offset(&line_index, line, col)?;
436 let items = squawk_ide::completion::completion(&self.db, file, offset);
437
438 let converted: Vec<WasmCompletionItem> = items
439 .into_iter()
440 .map(|item| WasmCompletionItem {
441 label: item.label,
442 kind: match item.kind {
443 squawk_ide::completion::CompletionItemKind::Keyword => "keyword",
444 squawk_ide::completion::CompletionItemKind::Table => "table",
445 squawk_ide::completion::CompletionItemKind::Column => "column",
446 squawk_ide::completion::CompletionItemKind::Function => "function",
447 squawk_ide::completion::CompletionItemKind::Schema => "schema",
448 squawk_ide::completion::CompletionItemKind::Type => "type",
449 squawk_ide::completion::CompletionItemKind::Snippet => "snippet",
450 squawk_ide::completion::CompletionItemKind::Operator => "operator",
451 }
452 .to_string(),
453 detail: item.detail,
454 insert_text: item.insert_text,
455 insert_text_format: item.insert_text_format.map(|fmt| {
456 match fmt {
457 squawk_ide::completion::CompletionInsertTextFormat::PlainText => {
458 "plainText"
459 }
460 squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet",
461 }
462 .to_string()
463 }),
464 trigger_completion_after_insert: item.trigger_completion_after_insert,
465 })
466 .collect();
467
468 serde_wasm_bindgen::to_value(&converted).map_err(into_error)
469 }
470}
471
472fn into_error<E: std::fmt::Display>(err: E) -> Error {
473 Error::new(&err.to_string())
474}
475
476fn position_to_offset(
477 line_index: &LineIndex,
478 line: u32,
479 col: u32,
480) -> Result<rowan::TextSize, Error> {
481 let wide_pos = line_index::WideLineCol { line, col };
482
483 let pos = line_index
484 .to_utf8(line_index::WideEncoding::Utf16, wide_pos)
485 .ok_or_else(|| Error::new("Invalid position"))?;
486
487 line_index
488 .offset(pos)
489 .ok_or_else(|| Error::new("Invalid position offset"))
490}
491
492fn convert_document_symbol(
493 line_index: &LineIndex,
494 symbol: squawk_ide::document_symbols::DocumentSymbol,
495) -> WasmDocumentSymbol {
496 let full_start = line_index.line_col(symbol.full_range.start());
497 let full_end = line_index.line_col(symbol.full_range.end());
498 let full_start_wide = line_index
499 .to_wide(line_index::WideEncoding::Utf16, full_start)
500 .unwrap();
501 let full_end_wide = line_index
502 .to_wide(line_index::WideEncoding::Utf16, full_end)
503 .unwrap();
504
505 let focus_start = line_index.line_col(symbol.focus_range.start());
506 let focus_end = line_index.line_col(symbol.focus_range.end());
507 let focus_start_wide = line_index
508 .to_wide(line_index::WideEncoding::Utf16, focus_start)
509 .unwrap();
510 let focus_end_wide = line_index
511 .to_wide(line_index::WideEncoding::Utf16, focus_end)
512 .unwrap();
513
514 WasmDocumentSymbol {
515 name: symbol.name,
516 detail: symbol.detail,
517 kind: match symbol.kind {
518 squawk_ide::document_symbols::DocumentSymbolKind::Schema => "schema",
519 squawk_ide::document_symbols::DocumentSymbolKind::Table => "table",
520 squawk_ide::document_symbols::DocumentSymbolKind::View => "view",
521 squawk_ide::document_symbols::DocumentSymbolKind::MaterializedView => {
522 "materialized_view"
523 }
524 squawk_ide::document_symbols::DocumentSymbolKind::Function => "function",
525 squawk_ide::document_symbols::DocumentSymbolKind::Aggregate => "aggregate",
526 squawk_ide::document_symbols::DocumentSymbolKind::Procedure => "procedure",
527 squawk_ide::document_symbols::DocumentSymbolKind::EventTrigger => "event_trigger",
528 squawk_ide::document_symbols::DocumentSymbolKind::Role => "role",
529 squawk_ide::document_symbols::DocumentSymbolKind::Policy => "policy",
530 squawk_ide::document_symbols::DocumentSymbolKind::Type => "type",
531 squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum",
532 squawk_ide::document_symbols::DocumentSymbolKind::Index => "index",
533 squawk_ide::document_symbols::DocumentSymbolKind::Domain => "domain",
534 squawk_ide::document_symbols::DocumentSymbolKind::Sequence => "sequence",
535 squawk_ide::document_symbols::DocumentSymbolKind::Trigger => "trigger",
536 squawk_ide::document_symbols::DocumentSymbolKind::Tablespace => "tablespace",
537 squawk_ide::document_symbols::DocumentSymbolKind::Database => "database",
538 squawk_ide::document_symbols::DocumentSymbolKind::Server => "server",
539 squawk_ide::document_symbols::DocumentSymbolKind::Extension => "extension",
540 squawk_ide::document_symbols::DocumentSymbolKind::Column => "column",
541 squawk_ide::document_symbols::DocumentSymbolKind::Variant => "variant",
542 squawk_ide::document_symbols::DocumentSymbolKind::Cursor => "cursor",
543 squawk_ide::document_symbols::DocumentSymbolKind::PreparedStatement => {
544 "prepared_statement"
545 }
546 squawk_ide::document_symbols::DocumentSymbolKind::Channel => "channel",
547 }
548 .to_string(),
549 start_line: full_start_wide.line,
550 start_column: full_start_wide.col,
551 end_line: full_end_wide.line,
552 end_column: full_end_wide.col,
553 selection_start_line: focus_start_wide.line,
554 selection_start_column: focus_start_wide.col,
555 selection_end_line: focus_end_wide.line,
556 selection_end_column: focus_end_wide.col,
557 children: symbol
558 .children
559 .into_iter()
560 .map(|child| convert_document_symbol(line_index, child))
561 .collect(),
562 }
563}
564
565#[expect(unused)]
566#[derive(Serialize)]
567enum Severity {
568 Hint,
569 Info,
570 Warning,
571 Error,
572}
573
574#[derive(Serialize)]
575struct LintError {
576 severity: Severity,
577 code: String,
578 message: String,
579 start_line_number: u32,
580 start_column: u32,
581 end_line_number: u32,
582 end_column: u32,
583 range_start: usize,
584 range_end: usize,
585 messages: Vec<String>,
586 fix: Option<Fix>,
587}
588
589#[derive(Serialize)]
590struct Fix {
591 title: String,
592 edits: Vec<TextEdit>,
593}
594
595#[derive(Serialize)]
596struct TextEdit {
597 start_line_number: u32,
598 start_column: u32,
599 end_line_number: u32,
600 end_column: u32,
601 text: String,
602}
603
604#[derive(Serialize)]
605struct LocationRange {
606 file: String,
607 start_line: u32,
608 start_column: u32,
609 end_line: u32,
610 end_column: u32,
611}
612
613#[derive(Serialize)]
614struct WasmCodeAction {
615 title: String,
616 edits: Vec<TextEdit>,
617 kind: String,
618}
619
620#[derive(Serialize)]
621struct WasmDocumentSymbol {
622 name: String,
623 detail: Option<String>,
624 kind: String,
625 start_line: u32,
626 start_column: u32,
627 end_line: u32,
628 end_column: u32,
629 selection_start_line: u32,
630 selection_start_column: u32,
631 selection_end_line: u32,
632 selection_end_column: u32,
633 children: Vec<WasmDocumentSymbol>,
634}
635
636#[derive(Serialize)]
637struct WasmInlayHint {
638 line: u32,
639 column: u32,
640 label: String,
641 kind: String,
642}
643
644#[derive(Serialize)]
645struct WasmFoldingRange {
646 start_line: u32,
647 end_line: u32,
648 kind: String,
649}
650
651#[derive(Serialize)]
652struct WasmSelectionRange {
653 start_line: u32,
654 start_column: u32,
655 end_line: u32,
656 end_column: u32,
657}
658
659#[derive(Serialize)]
660struct WasmCompletionItem {
661 label: String,
662 kind: String,
663 detail: Option<String>,
664 insert_text: Option<String>,
665 insert_text_format: Option<String>,
666 trigger_completion_after_insert: bool,
667}
668
669#[derive(Deserialize)]
670struct Position {
671 line: u32,
672 column: u32,
673}