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