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