From aeebb089fdb531811c3f25d4d1bce3089ca6b902 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Tue, 3 Mar 2026 20:45:47 -0500 Subject: [PATCH] ide/playground: salsa most of the server functions --- Cargo.lock | 107 ++- Cargo.toml | 7 +- crates/squawk_ide/Cargo.toml | 3 + crates/squawk_ide/src/binder.rs | 2 + crates/squawk_ide/src/builtins.rs | 40 +- crates/squawk_ide/src/code_actions.rs | 48 +- crates/squawk_ide/src/completion.rs | 44 +- .../{squawk_server => squawk_ide}/src/db.rs | 10 + crates/squawk_ide/src/document_symbols.rs | 29 +- crates/squawk_ide/src/find_references.rs | 37 +- crates/squawk_ide/src/goto_definition.rs | 40 +- crates/squawk_ide/src/hover.rs | 102 ++- crates/squawk_ide/src/inlay_hints.rs | 46 +- crates/squawk_ide/src/lib.rs | 1 + crates/squawk_ide/src/scope.rs | 2 +- crates/squawk_ide/src/symbols.rs | 2 +- crates/squawk_server/src/ignore.rs | 11 +- crates/squawk_server/src/lib.rs | 79 +- crates/squawk_server/src/lint.rs | 13 +- crates/squawk_wasm/Cargo.toml | 1 + crates/squawk_wasm/src/lib.rs | 838 +++++++++--------- playground/src/App.tsx | 30 +- playground/src/providers.tsx | 25 +- playground/src/squawk.tsx | 85 +- 24 files changed, 908 insertions(+), 694 deletions(-) rename crates/{squawk_server => squawk_ide}/src/db.rs (72%) diff --git a/Cargo.lock b/Cargo.lock index 3910b837..aeab295f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,17 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -254,6 +265,12 @@ version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.34" @@ -1264,9 +1281,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1430,9 +1447,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minicov" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" dependencies = [ "cc", "walkdir", @@ -1509,6 +1526,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1601,6 +1627,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl" version = "0.10.73" @@ -2605,17 +2637,20 @@ name = "squawk-ide" version = "2.43.0" dependencies = [ "annotate-snippets", + "etcetera", "insta", "itertools 0.14.0", "la-arena", "line-index", "log", "rowan", + "salsa", "smallvec", "smol_str", "squawk-linter", "squawk-syntax", "tabled", + "url", ] [[package]] @@ -2698,6 +2733,7 @@ dependencies = [ "line-index", "log", "rowan", + "salsa", "serde", "serde-wasm-bindgen", "squawk-ide", @@ -3155,37 +3191,25 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3194,9 +3218,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3204,55 +3228,70 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.106", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.50" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" dependencies = [ + "async-trait", + "cast", "js-sys", + "libm", "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.50" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 023a1547..585d3592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,9 +46,9 @@ line-index = "0.1.2" lsp-server = "0.7.8" lsp-types = "0.95" serde-wasm-bindgen = "0.6.5" -wasm-bindgen = "0.2.100" -wasm-bindgen-test = "0.3.34" -web-sys = "0.3.77" +wasm-bindgen = "0.2.114" +wasm-bindgen-test = "0.3.50" +web-sys = "0.3.91" console_error_panic_hook = "0.1.7" console_log = "1.0.0" annotate-snippets = "0.12.4" @@ -63,6 +63,7 @@ snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd"] } smallvec = "1.13.2" tabled = "0.17.0" etcetera = "0.11.0" +url = "2.5.4" # local # we have to make the versions explicit otherwise `cargo publish` won't work diff --git a/crates/squawk_ide/Cargo.toml b/crates/squawk_ide/Cargo.toml index ed6badf4..4532afb9 100644 --- a/crates/squawk_ide/Cargo.toml +++ b/crates/squawk_ide/Cargo.toml @@ -20,11 +20,14 @@ squawk-linter.workspace = true rowan.workspace = true line-index.workspace = true annotate-snippets.workspace = true +url.workspace = true +salsa.workspace = true log.workspace = true smol_str.workspace = true la-arena.workspace = true smallvec.workspace = true itertools.workspace = true +etcetera.workspace = true [dev-dependencies] insta.workspace = true diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 5bd1e770..12d202ce 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -7,11 +7,13 @@ use squawk_syntax::{SyntaxNodePtr, ast, ast::AstNode}; use crate::scope::{Scope, ScopeId}; use crate::symbols::{Name, Schema, Symbol, SymbolKind}; +#[derive(Clone, PartialEq)] struct SearchPathChange { position: TextSize, search_path: Vec, } +#[derive(Clone, PartialEq)] pub(crate) struct Binder { // TODO: doesn't seem like we need this with our resolve setup scopes: Arena, diff --git a/crates/squawk_ide/src/builtins.rs b/crates/squawk_ide/src/builtins.rs index df52c53a..69f59cd4 100644 --- a/crates/squawk_ide/src/builtins.rs +++ b/crates/squawk_ide/src/builtins.rs @@ -1,4 +1,42 @@ -pub const BUILTINS_SQL: &str = include_str!("builtins.sql"); +#[cfg(not(target_arch = "wasm32"))] +use etcetera::BaseStrategy; +use line_index::LineIndex; +use salsa::Database as Db; +use squawk_syntax::{Parse, SourceFile}; +#[cfg(not(target_arch = "wasm32"))] +use url::Url; + +use crate::binder::{self, Binder}; + +pub(crate) const BUILTINS_SQL: &str = include_str!("builtins.sql"); + +#[salsa::tracked] +pub fn parse_builtins(_db: &dyn Db) -> Parse { + SourceFile::parse(BUILTINS_SQL) +} + +#[salsa::tracked] +pub fn builtins_line_index(_db: &dyn Db) -> LineIndex { + LineIndex::new(BUILTINS_SQL) +} + +#[salsa::tracked] +pub fn builtins_binder(db: &dyn Db) -> Binder { + let builtins_tree = parse_builtins(db).tree(); + binder::bind(&builtins_tree) +} + +#[cfg(not(target_arch = "wasm32"))] +#[salsa::tracked] +pub fn builtins_url(_db: &dyn Db) -> Option { + let strategy = etcetera::base_strategy::choose_base_strategy().ok()?; + let config_dir = strategy.config_dir(); + let cache_dir = config_dir.join("squawk/stubs"); + let path = cache_dir.join("builtins.sql"); + std::fs::create_dir_all(&cache_dir).ok()?; + std::fs::write(&path, BUILTINS_SQL).ok()?; + Url::from_file_path(path).ok() +} #[cfg(test)] mod test { diff --git a/crates/squawk_ide/src/code_actions.rs b/crates/squawk_ide/src/code_actions.rs index ccc7f5cc..1f0965ee 100644 --- a/crates/squawk_ide/src/code_actions.rs +++ b/crates/squawk_ide/src/code_actions.rs @@ -1,5 +1,6 @@ use itertools::Itertools; use rowan::{TextRange, TextSize}; +use salsa::Database as Db; use squawk_linter::Edit; use squawk_syntax::{ SyntaxKind, SyntaxToken, @@ -10,44 +11,49 @@ use std::iter; use crate::{ binder, column_name::ColumnName, + db::{File, parse}, offsets::token_from_offset, quote::{quote_column_alias, unquote_ident}, symbols::Name, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ActionKind { QuickFix, RefactorRewrite, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CodeAction { pub title: String, pub edits: Vec, pub kind: ActionKind, } -pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option> { +#[salsa::tracked] +pub fn code_actions(db: &dyn Db, file: File, offset: TextSize) -> Option> { + let parse = parse(db, file); + let source_file = parse.tree(); + let mut actions = vec![]; - rewrite_as_regular_string(&mut actions, &file, offset); - rewrite_as_dollar_quoted_string(&mut actions, &file, offset); - remove_else_clause(&mut actions, &file, offset); - rewrite_table_as_select(&mut actions, &file, offset); - rewrite_select_as_table(&mut actions, &file, offset); - rewrite_from(&mut actions, &file, offset); - rewrite_leading_from(&mut actions, &file, offset); - rewrite_values_as_select(&mut actions, &file, offset); - rewrite_select_as_values(&mut actions, &file, offset); - add_schema(&mut actions, &file, offset); - quote_identifier(&mut actions, &file, offset); - unquote_identifier(&mut actions, &file, offset); - add_explicit_alias(&mut actions, &file, offset); - remove_redundant_alias(&mut actions, &file, offset); - rewrite_cast_to_double_colon(&mut actions, &file, offset); - rewrite_double_colon_to_cast(&mut actions, &file, offset); - rewrite_between_as_binary_expression(&mut actions, &file, offset); - rewrite_timestamp_type(&mut actions, &file, offset); + rewrite_as_regular_string(&mut actions, &source_file, offset); + rewrite_as_dollar_quoted_string(&mut actions, &source_file, offset); + remove_else_clause(&mut actions, &source_file, offset); + rewrite_table_as_select(&mut actions, &source_file, offset); + rewrite_select_as_table(&mut actions, &source_file, offset); + rewrite_from(&mut actions, &source_file, offset); + rewrite_leading_from(&mut actions, &source_file, offset); + rewrite_values_as_select(&mut actions, &source_file, offset); + rewrite_select_as_values(&mut actions, &source_file, offset); + add_schema(&mut actions, &source_file, offset); + quote_identifier(&mut actions, &source_file, offset); + unquote_identifier(&mut actions, &source_file, offset); + add_explicit_alias(&mut actions, &source_file, offset); + remove_redundant_alias(&mut actions, &source_file, offset); + rewrite_cast_to_double_colon(&mut actions, &source_file, offset); + rewrite_double_colon_to_cast(&mut actions, &source_file, offset); + rewrite_between_as_binary_expression(&mut actions, &source_file, offset); + rewrite_timestamp_type(&mut actions, &source_file, offset); Some(actions) } diff --git a/crates/squawk_ide/src/completion.rs b/crates/squawk_ide/src/completion.rs index 078eb8de..c50aecb1 100644 --- a/crates/squawk_ide/src/completion.rs +++ b/crates/squawk_ide/src/completion.rs @@ -1,17 +1,23 @@ use rowan::TextSize; +use salsa::Database as Db; use squawk_syntax::ast::{self, AstNode}; use squawk_syntax::{SyntaxKind, SyntaxToken}; use crate::binder; +use crate::db::{File, parse}; use crate::resolve; use crate::symbols::{Name, Schema, SymbolKind}; use crate::tokens::is_string_or_comment; const COMPLETION_MARKER: &str = "squawkCompletionMarker"; -pub fn completion(file: &ast::SourceFile, offset: TextSize) -> Vec { - let file = file_with_completion_marker(file, offset); - let Some(token) = token_at_offset(&file, offset) else { +#[salsa::tracked] +pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec { + let parse = parse(db, file); + let source_file = parse.tree(); + + let marker_file = file_with_completion_marker(&source_file, offset); + let Some(token) = token_at_offset(&marker_file, offset) else { // empty file return default_completions(); }; @@ -24,19 +30,23 @@ pub fn completion(file: &ast::SourceFile, offset: TextSize) -> Vec table_completions(&file, &token), + CompletionContext::TableOnly => table_completions(&marker_file, &token), CompletionContext::Default => default_completions(), CompletionContext::SelectClause(select_clause) => { - select_completions(&file, select_clause, &token) + select_completions(&marker_file, select_clause, &token) } CompletionContext::SelectClauses(select) => select_clauses_completions(&select), - CompletionContext::SelectExpr(select) => select_expr_completions(&file, &select, &token), - CompletionContext::LimitClause => limit_completions(&file, &token), - CompletionContext::OffsetClause => offset_completions(&file, &token), + CompletionContext::SelectExpr(select) => { + select_expr_completions(&marker_file, &select, &token) + } + CompletionContext::LimitClause => limit_completions(&marker_file, &token), + CompletionContext::OffsetClause => offset_completions(&marker_file, &token), CompletionContext::DeleteClauses(delete) => { - delete_clauses_completions(&file, &delete, &token) + delete_clauses_completions(&marker_file, &delete, &token) + } + CompletionContext::DeleteExpr(delete) => { + delete_expr_completions(&marker_file, &delete, &token) } - CompletionContext::DeleteExpr(delete) => delete_expr_completions(&file, &delete, &token), } } @@ -944,17 +954,17 @@ pub struct CompletionItem { #[cfg(test)] mod tests { use super::completion; + use crate::db::{Database, File}; use crate::test_utils::fixture; use insta::assert_snapshot; - use squawk_syntax::ast; use tabled::builder::Builder; use tabled::settings::Style; fn completions(sql: &str) -> String { let (offset, sql) = fixture(sql); - let parse = ast::SourceFile::parse(&sql); - let file = parse.tree(); - let items = completion(&file, offset); + let db = Database::default(); + let file = File::new(&db, sql, 0); + let items = completion(&db, file, offset); assert!( !items.is_empty(), "No completions found. If this was intended, use `completions_not_found` instead." @@ -964,9 +974,9 @@ mod tests { fn completions_not_found(sql: &str) { let (offset, sql) = fixture(sql); - let parse = ast::SourceFile::parse(&sql); - let file = parse.tree(); - let items = completion(&file, offset); + let db = Database::default(); + let file = File::new(&db, sql, 0); + let items = completion(&db, file, offset); assert_eq!( items, vec![], diff --git a/crates/squawk_server/src/db.rs b/crates/squawk_ide/src/db.rs similarity index 72% rename from crates/squawk_server/src/db.rs rename to crates/squawk_ide/src/db.rs index e9b1ffd4..a5fb8d0f 100644 --- a/crates/squawk_server/src/db.rs +++ b/crates/squawk_ide/src/db.rs @@ -3,6 +3,9 @@ use salsa::Database as Db; use salsa::Storage; use squawk_syntax::{Parse, SourceFile}; +use crate::binder; +use crate::binder::Binder; + #[salsa::input] pub struct File { #[returns(ref)] @@ -20,6 +23,13 @@ pub fn line_index(db: &dyn Db, file: File) -> LineIndex { LineIndex::new(file.content(db)) } +#[salsa::tracked] +pub fn bind(db: &dyn Db, file: File) -> Binder { + let result = parse(db, file); + let source_file = result.tree(); + binder::bind(&source_file) +} + #[salsa::db] #[derive(Default)] pub struct Database { diff --git a/crates/squawk_ide/src/document_symbols.rs b/crates/squawk_ide/src/document_symbols.rs index e3f49561..fd09880a 100644 --- a/crates/squawk_ide/src/document_symbols.rs +++ b/crates/squawk_ide/src/document_symbols.rs @@ -1,13 +1,15 @@ use rowan::TextRange; +use salsa::Database as Db; use squawk_syntax::ast::{self, AstNode}; use crate::binder::{self, extract_string_literal}; +use crate::db::{File, parse}; use crate::resolve::{ resolve_aggregate_info, resolve_function_info, resolve_procedure_info, resolve_sequence_info, resolve_table_info, resolve_type_info, resolve_view_info, }; -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DocumentSymbolKind { Schema, Table, @@ -36,7 +38,7 @@ pub enum DocumentSymbolKind { Channel, } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DocumentSymbol { pub name: String, pub detail: Option, @@ -49,12 +51,16 @@ pub struct DocumentSymbol { pub children: Vec, } -pub fn document_symbols(file: &ast::SourceFile) -> Vec { +#[salsa::tracked] +pub fn document_symbols(db: &dyn Db, file: File) -> Vec { + let parse = parse(db, file); + let source_file = parse.tree(); + // TODO: we should salsa this - let binder = binder::bind(file); + let binder = binder::bind(&source_file); let mut symbols = vec![]; - for stmt in file.stmts() { + for stmt in source_file.stmts() { match stmt { ast::Stmt::CreateSchema(create_schema) => { if let Some(symbol) = create_schema_symbol(create_schema) { @@ -840,24 +846,25 @@ fn create_unlisten_symbol(unlisten: ast::Unlisten) -> Option { #[cfg(test)] mod tests { use super::*; + use crate::db::{Database, File}; use annotate_snippets::{ AnnotationKind, Group, Level, Renderer, Snippet, renderer::DecorStyle, }; use insta::assert_snapshot; fn symbols_not_found(sql: &str) { - let parse = ast::SourceFile::parse(sql); - let file = parse.tree(); - let symbols = document_symbols(&file); + let db = Database::default(); + let file = File::new(&db, sql.to_string(), 0); + let symbols = document_symbols(&db, file); if !symbols.is_empty() { panic!("Symbols found. If this is expected, use `symbols` instead.") } } fn symbols(sql: &str) -> String { - let parse = ast::SourceFile::parse(sql); - let file = parse.tree(); - let symbols = document_symbols(&file); + let db = Database::default(); + let file = File::new(&db, sql.to_string(), 0); + let symbols = document_symbols(&db, file); if symbols.is_empty() { panic!("No symbols found. If this is expected, use `symbols_not_found` instead.") } diff --git a/crates/squawk_ide/src/find_references.rs b/crates/squawk_ide/src/find_references.rs index e0398cde..8847a938 100644 --- a/crates/squawk_ide/src/find_references.rs +++ b/crates/squawk_ide/src/find_references.rs @@ -1,9 +1,11 @@ -use crate::binder::{self, Binder}; -use crate::builtins::BUILTINS_SQL; +use crate::binder::Binder; +use crate::builtins::{builtins_binder, parse_builtins}; +use crate::db::{File, bind, parse}; use crate::goto_definition::{FileId, Location}; use crate::offsets::token_from_offset; use crate::resolve; use rowan::TextSize; +use salsa::Database as Db; use smallvec::{SmallVec, smallvec}; use squawk_syntax::{ SyntaxNodePtr, @@ -11,17 +13,18 @@ use squawk_syntax::{ match_ast, }; -pub fn find_references(file: &ast::SourceFile, offset: TextSize) -> Vec { - // TODO: we should salsa this - let current_binder = binder::bind(file); +#[salsa::tracked] +pub fn find_references(db: &dyn Db, file: File, offset: TextSize) -> Vec { + let parse = parse(db, file); + let source_file = parse.tree(); - // TODO: we should salsa this - let builtins_tree = ast::SourceFile::parse(BUILTINS_SQL).tree(); - // TODO: we should salsa this - let builtins_binder = binder::bind(&builtins_tree); + let current_binder = bind(db, file); + + let builtins_tree = parse_builtins(db).tree(); + let builtins_binder = builtins_binder(db); let Some((target_file, target_defs)) = find_target_defs( - file, + &source_file, offset, ¤t_binder, &builtins_tree, @@ -31,7 +34,7 @@ pub fn find_references(file: &ast::SourceFile, offset: TextSize) -> Vec (¤t_binder, file.syntax()), + FileId::Current => (¤t_binder, source_file.syntax()), FileId::Builtins => (&builtins_binder, builtins_tree.syntax()), }; @@ -46,7 +49,7 @@ pub fn find_references(file: &ast::SourceFile, offset: TextSize) -> Vec { @@ -114,23 +117,23 @@ fn find_target_defs( #[cfg(test)] mod test { use crate::builtins::BUILTINS_SQL; + use crate::db::{Database, File}; use crate::find_references::find_references; use crate::goto_definition::FileId; use crate::test_utils::fixture; use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle}; use insta::assert_snapshot; use rowan::TextRange; - use squawk_syntax::ast; #[track_caller] fn find_refs(sql: &str) -> String { let (mut offset, sql) = fixture(sql); offset = offset.checked_sub(1.into()).unwrap_or_default(); - let parse = ast::SourceFile::parse(&sql); - assert_eq!(parse.errors(), vec![]); - let file: ast::SourceFile = parse.tree(); + let db = Database::default(); + let file = File::new(&db, sql.clone(), 0); + assert_eq!(crate::db::parse(&db, file).errors(), vec![]); - let references = find_references(&file, offset); + let references = find_references(&db, file, offset); let offset_usize: usize = offset.into(); diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 11acd519..47b75a37 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -1,15 +1,21 @@ +use crate::binder; +use crate::builtins::parse_builtins; +use crate::db::{File, parse}; use crate::offsets::token_from_offset; use crate::resolve; -use crate::{binder, builtins::BUILTINS_SQL}; use rowan::{TextRange, TextSize}; +use salsa::Database as Db; use smallvec::{SmallVec, smallvec}; use squawk_syntax::{ SyntaxKind, ast::{self, AstNode}, }; -pub fn goto_definition(file: &ast::SourceFile, offset: TextSize) -> SmallVec<[Location; 1]> { - let Some(token) = token_from_offset(file, offset) else { +#[salsa::tracked] +pub fn goto_definition(db: &dyn Db, file: File, offset: TextSize) -> SmallVec<[Location; 1]> { + let parse = parse(db, file); + let source_file = &parse.tree(); + let Some(token) = token_from_offset(source_file, offset) else { return smallvec![]; }; let Some(parent) = token.parent() else { @@ -32,21 +38,22 @@ pub fn goto_definition(file: &ast::SourceFile, offset: TextSize) -> SmallVec<[Lo // goto def on COMMIT -> BEGIN/START TRANSACTION if ast::Commit::can_cast(parent.kind()) - && let Some(begin_range) = find_preceding_begin(file, token.text_range().start()) + && let Some(begin_range) = find_preceding_begin(source_file, token.text_range().start()) { return smallvec![Location::range(begin_range)]; } // goto def on ROLLBACK -> BEGIN/START TRANSACTION if ast::Rollback::can_cast(parent.kind()) - && let Some(begin_range) = find_preceding_begin(file, token.text_range().start()) + && let Some(begin_range) = find_preceding_begin(source_file, token.text_range().start()) { return smallvec![Location::range(begin_range)]; } // goto def on BEGIN/START TRANSACTION -> COMMIT or ROLLBACK if ast::Begin::can_cast(parent.kind()) - && let Some(end_range) = find_following_commit_or_rollback(file, token.text_range().end()) + && let Some(end_range) = + find_following_commit_or_rollback(source_file, token.text_range().end()) { return smallvec![Location::range(end_range)]; } @@ -58,9 +65,9 @@ pub fn goto_definition(file: &ast::SourceFile, offset: TextSize) -> SmallVec<[Lo if let Some(name_ref) = ast::NameRef::cast(parent.clone()) { for file_id in [FileId::Current, FileId::Builtins] { let file = match file_id { - FileId::Current => file, + FileId::Current => source_file, // TODO: we should salsa this - FileId::Builtins => &ast::SourceFile::parse(BUILTINS_SQL).tree(), + FileId::Builtins => &parse_builtins(db).tree(), }; // TODO: we should salsa this let binder_output = binder::bind(file); @@ -90,9 +97,9 @@ pub fn goto_definition(file: &ast::SourceFile, offset: TextSize) -> SmallVec<[Lo if let Some(ty) = type_node { for file_id in [FileId::Current, FileId::Builtins] { let file = match file_id { - FileId::Current => file, + FileId::Current => source_file, // TODO: we should salsa this - FileId::Builtins => &ast::SourceFile::parse(BUILTINS_SQL).tree(), + FileId::Builtins => &parse_builtins(db).tree(), }; // TODO: we should salsa this let binder_output = binder::bind(file); @@ -115,7 +122,7 @@ pub enum FileId { Builtins, } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Location { pub file: FileId, pub range: TextRange, @@ -160,6 +167,7 @@ fn find_following_commit_or_rollback(file: &ast::SourceFile, after: TextSize) -> #[cfg(test)] mod test { use crate::builtins::BUILTINS_SQL; + use crate::db::{Database, File}; use crate::goto_definition::{FileId, goto_definition}; use crate::test_utils::fixture; use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle}; @@ -167,8 +175,6 @@ mod test { use log::info; use rowan::TextRange; - use squawk_syntax::ast; - #[track_caller] fn goto(sql: &str) -> String { goto_(sql).expect("should always find a definition") @@ -181,10 +187,10 @@ mod test { // For go to def we want the previous character since we usually put the // marker after the item we're trying to go to def on. offset = offset.checked_sub(1.into()).unwrap_or_default(); - let parse = ast::SourceFile::parse(&sql); - assert_eq!(parse.errors(), vec![]); - let file: ast::SourceFile = parse.tree(); - let results = goto_definition(&file, offset); + let db = Database::default(); + let file = File::new(&db, sql.clone(), 0); + assert_eq!(crate::db::parse(&db, file).errors(), vec![]); + let results = goto_definition(&db, file, offset); if !results.is_empty() { let offset: usize = offset.into(); let mut current_dests = vec![]; diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 9a746536..68bd6440 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -1,6 +1,7 @@ -use crate::builtins::BUILTINS_SQL; +use crate::builtins::parse_builtins; use crate::classify::{NameClass, NameRefClass, classify_def_node, classify_name}; use crate::column_name::ColumnName; +use crate::db::{File, parse}; use crate::offsets::token_from_offset; use crate::{ binder, @@ -8,52 +9,55 @@ use crate::{ }; use crate::{goto_definition, resolve}; use rowan::TextSize; +use salsa::Database as Db; use squawk_syntax::SyntaxNode; use squawk_syntax::{ SyntaxKind, ast::{self, AstNode}, }; -pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { - let token = token_from_offset(file, offset)?; +#[salsa::tracked] +pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option { + let parse = parse(db, file); + let source_file = parse.tree(); + + let token = token_from_offset(&source_file, offset)?; let parent = token.parent()?; - let root = file.syntax(); + let root = source_file.syntax(); // TODO: we should salsa this - let binder = binder::bind(file); + let binder = binder::bind(&source_file); if token.kind() == SyntaxKind::STAR { if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) && field_expr.star_token().is_some() - && let Some(result) = hover_qualified_star(root, &field_expr, &binder) + && let Some(result) = hover_qualified_star(db, root, &field_expr, &binder) { return Some(result); } if let Some(arg_list) = ast::ArgList::cast(parent.clone()) - && let Some(result) = hover_unqualified_star_in_arg_list(root, &arg_list, &binder) + && let Some(result) = hover_unqualified_star_in_arg_list(db, root, &arg_list, &binder) { return Some(result); } if let Some(target) = ast::Target::cast(parent.clone()) && target.star_token().is_some() - && let Some(result) = hover_unqualified_star(root, &target, &binder) + && let Some(result) = hover_unqualified_star(db, root, &target, &binder) { return Some(result); } } if let Some(name_ref) = ast::NameRef::cast(parent.clone()) { - let definition = goto_definition::goto_definition(file, offset); + let definition = goto_definition::goto_definition(db, file, offset); let def = definition.first()?; let (binder, root) = match def.file { goto_definition::FileId::Current => (binder, root.clone()), goto_definition::FileId::Builtins => { - let builtins_tree = ast::SourceFile::parse(BUILTINS_SQL); - let builtins_tree = builtins_tree.tree(); - // TODO: we should salsa this + let builtins_tree = parse_builtins(db).tree(); let binder = binder::bind(&builtins_tree); let tree = builtins_tree.syntax().clone(); (binder, tree) @@ -67,7 +71,7 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { let context = classify_def_node(&def_node)?; - return hover_name_ref(&root, &name_ref, &binder, context, &def_node); + return hover_name_ref(db, &root, &name_ref, &binder, context, &def_node); } if let Some(name) = ast::Name::cast(parent) { @@ -145,6 +149,7 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { } fn hover_name_ref( + db: &dyn Db, root: &SyntaxNode, name_ref: &ast::NameRef, // TODO: we should pass in the file id along with the def_node and then use @@ -178,7 +183,7 @@ fn hover_name_ref( return Some(result); } // Finally try as table (handles case like `select t from t;` where t is the table) - hover_table(binder, def_node) + hover_table(db, binder, def_node) } NameRefClass::DeleteQualifiedColumnTable | NameRefClass::ForeignKeyTable @@ -190,7 +195,7 @@ fn hover_name_ref( | NameRefClass::SelectQualifiedColumnTable | NameRefClass::Table | NameRefClass::UpdateQualifiedColumnTable - | NameRefClass::View => hover_table(binder, def_node), + | NameRefClass::View => hover_table(db, binder, def_node), NameRefClass::Sequence => hover_sequence(root, name_ref, binder), NameRefClass::Trigger => hover_trigger(root, name_ref, binder), NameRefClass::Policy => hover_policy(root, name_ref, binder), @@ -439,12 +444,15 @@ fn hover_column_definition( // TODO: we should pass in the file id along with the def_node and then use // salsa to lookup the the correct binder. fn format_table_source( + db: &dyn Db, root: &SyntaxNode, source: resolve::TableSource, binder: &binder::Binder, ) -> Option { match source { - resolve::TableSource::Alias(alias) => format_alias_with_column_list(root, &alias, binder), + resolve::TableSource::Alias(alias) => { + format_alias_with_column_list(db, root, &alias, binder) + } resolve::TableSource::WithTable(with_table) => format_with_table(&with_table), resolve::TableSource::CreateView(create_view) => format_create_view(&create_view, binder), resolve::TableSource::CreateMaterializedView(create_materialized_view) => { @@ -457,7 +465,7 @@ fn format_table_source( } } -fn hover_table(binder: &binder::Binder, def_node: &SyntaxNode) -> Option { +fn hover_table(db: &dyn Db, binder: &binder::Binder, def_node: &SyntaxNode) -> Option { if let Some(result) = hover_subquery_table(def_node) { return Some(result); } @@ -465,13 +473,14 @@ fn hover_table(binder: &binder::Binder, def_node: &SyntaxNode) -> Option if let Some(source) = resolve::find_table_source(def_node) && let Some(root) = def_node.ancestors().last() { - return format_table_source(&root, source, binder); + return format_table_source(db, &root, source, binder); } None } fn format_alias_with_column_list( + db: &dyn Db, root: &SyntaxNode, alias: &ast::Alias, binder: &binder::Binder, @@ -492,7 +501,7 @@ fn format_alias_with_column_list( if let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) && let Some(table_ptr) = resolve::table_ptr_from_from_item(binder, &from_item) { - let base_columns = collect_star_column_names(root, &table_ptr, binder); + let base_columns = collect_star_column_names(db, root, &table_ptr, binder); for column in base_columns.iter().skip(columns.len()) { columns.push(column.clone()); } @@ -507,26 +516,32 @@ fn format_alias_with_column_list( } fn hover_qualified_star( + db: &dyn Db, root: &SyntaxNode, field_expr: &ast::FieldExpr, binder: &binder::Binder, ) -> Option { let table_ptr = resolve::resolve_qualified_star_table_ptr(binder, field_expr)?; - hover_qualified_star_columns(root, &table_ptr, binder) + hover_qualified_star_columns(db, root, &table_ptr, binder) } fn hover_unqualified_star( + db: &dyn Db, root: &SyntaxNode, target: &ast::Target, binder: &binder::Binder, ) -> Option { - let mut results = hover_unqualified_star_with_binder(root, target, binder); + let mut results = hover_unqualified_star_with_binder(db, root, target, binder); if results.is_empty() && target_has_schema_qualified_from_item(target) { - let builtins_tree = ast::SourceFile::parse(BUILTINS_SQL).tree(); + let builtins_tree = parse_builtins(db).tree(); let builtins_binder = binder::bind(&builtins_tree); - results = - hover_unqualified_star_with_binder(builtins_tree.syntax(), target, &builtins_binder); + results = hover_unqualified_star_with_binder( + db, + builtins_tree.syntax(), + target, + &builtins_binder, + ); } if results.is_empty() { @@ -537,6 +552,7 @@ fn hover_unqualified_star( } fn hover_unqualified_star_with_binder( + db: &dyn Db, root: &SyntaxNode, target: &ast::Target, binder: &binder::Binder, @@ -545,7 +561,7 @@ fn hover_unqualified_star_with_binder( if let Some(table_ptrs) = resolve::resolve_unqualified_star_table_ptrs(binder, target) { for table_ptr in table_ptrs { - if let Some(columns) = hover_qualified_star_columns(root, &table_ptr, binder) { + if let Some(columns) = hover_qualified_star_columns(db, root, &table_ptr, binder) { results.push(columns); } } @@ -572,6 +588,7 @@ fn target_has_schema_qualified_from_item(target: &ast::Target) -> bool { } fn hover_unqualified_star_in_arg_list( + db: &dyn Db, root: &SyntaxNode, arg_list: &ast::ArgList, binder: &binder::Binder, @@ -579,7 +596,7 @@ fn hover_unqualified_star_in_arg_list( let table_ptrs = resolve::resolve_unqualified_star_in_arg_list_ptrs(binder, arg_list)?; let mut results = vec![]; for table_ptr in table_ptrs { - if let Some(columns) = hover_qualified_star_columns(root, &table_ptr, binder) { + if let Some(columns) = hover_qualified_star_columns(db, root, &table_ptr, binder) { results.push(columns); } } @@ -609,6 +626,7 @@ fn format_subquery_table(name: &Name, paren_select: &ast::ParenSelect) -> Option } fn hover_qualified_star_columns( + db: &dyn Db, root: &SyntaxNode, table_ptr: &squawk_syntax::SyntaxNodePtr, binder: &binder::Binder, @@ -616,12 +634,12 @@ fn hover_qualified_star_columns( let table_name_node = table_ptr.to_node(root); if let Some(paren_select) = ast::ParenSelect::cast(table_name_node.clone()) { - return hover_qualified_star_columns_from_subquery(root, &paren_select, binder); + return hover_qualified_star_columns_from_subquery(db, root, &paren_select, binder); } match resolve::find_table_source(&table_name_node)? { resolve::TableSource::Alias(alias) => { - hover_qualified_star_columns_from_alias(root, &alias, binder) + hover_qualified_star_columns_from_alias(db, root, &alias, binder) } resolve::TableSource::WithTable(with_table) => { hover_qualified_star_columns_from_cte(root, &with_table, binder) @@ -636,12 +654,13 @@ fn hover_qualified_star_columns( hover_qualified_star_columns_from_materialized_view(&create_materialized_view, binder) } resolve::TableSource::ParenSelect(paren_select) => { - hover_qualified_star_columns_from_subquery(root, &paren_select, binder) + hover_qualified_star_columns_from_subquery(db, root, &paren_select, binder) } } } fn hover_qualified_star_columns_from_alias( + db: &dyn Db, root: &SyntaxNode, alias: &ast::Alias, binder: &binder::Binder, @@ -666,7 +685,7 @@ fn hover_qualified_star_columns_from_alias( let from_item = alias.syntax().ancestors().find_map(ast::FromItem::cast)?; let table_ptr = resolve::table_ptr_from_from_item(binder, &from_item)?; - let base_column_names = collect_star_column_names(root, &table_ptr, binder); + let base_column_names = collect_star_column_names(db, root, &table_ptr, binder); for column_name in base_column_names.iter().skip(alias_columns.len()) { results.push(ColumnHover::table_column( @@ -679,6 +698,7 @@ fn hover_qualified_star_columns_from_alias( } fn collect_star_column_names( + db: &dyn Db, root: &SyntaxNode, table_ptr: &squawk_syntax::SyntaxNodePtr, binder: &binder::Binder, @@ -694,7 +714,7 @@ fn collect_star_column_names( if !columns.is_empty() { return columns; } - return collect_star_column_names_from_paren_select(root, &paren_select, binder); + return collect_star_column_names_from_paren_select(db, root, &paren_select, binder); } match resolve::find_table_source(&table_name_node) { @@ -710,7 +730,7 @@ fn collect_star_column_names( return columns; } - let builtins_tree = ast::SourceFile::parse(BUILTINS_SQL).tree(); + let builtins_tree = parse_builtins(db).tree(); let builtins_binder = binder::bind(&builtins_tree); resolve::collect_with_table_column_names( &builtins_binder, @@ -741,6 +761,7 @@ fn collect_star_column_names( } fn collect_star_column_names_from_paren_select( + db: &dyn Db, root: &SyntaxNode, paren_select: &ast::ParenSelect, binder: &binder::Binder, @@ -757,7 +778,7 @@ fn collect_star_column_names_from_paren_select( let mut columns = vec![]; for from_item in from_clause.from_items() { if let Some(table_ptr) = resolve::table_ptr_from_from_item(binder, &from_item) { - columns.extend(collect_star_column_names(root, &table_ptr, binder)); + columns.extend(collect_star_column_names(db, root, &table_ptr, binder)); } } columns @@ -861,6 +882,7 @@ fn hover_qualified_star_columns_from_materialized_view( } fn hover_qualified_star_columns_from_subquery( + db: &dyn Db, root: &SyntaxNode, paren_select: &ast::ParenSelect, binder: &binder::Binder, @@ -878,7 +900,9 @@ fn hover_qualified_star_columns_from_subquery( if target.star_token().is_some() { let table_ptrs = resolve::resolve_unqualified_star_table_ptrs(binder, &target)?; for table_ptr in table_ptrs { - if let Some(columns) = hover_qualified_star_columns(root, &table_ptr, binder) { + if let Some(columns) = + hover_qualified_star_columns(db, root, &table_ptr, binder) + { results.push(columns) } } @@ -1630,11 +1654,11 @@ fn hover_routine( #[cfg(test)] mod test { + use crate::db::{Database, File}; use crate::hover::hover; use crate::test_utils::fixture; use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle}; use insta::assert_snapshot; - use squawk_syntax::ast; #[track_caller] fn check_hover(sql: &str) -> String { @@ -1643,13 +1667,13 @@ mod test { #[track_caller] fn check_hover_(sql: &str) -> Option { + let db = Database::default(); let (mut offset, sql) = fixture(sql); offset = offset.checked_sub(1.into()).unwrap_or_default(); - let parse = ast::SourceFile::parse(&sql); - assert_eq!(parse.errors(), vec![]); - let file: ast::SourceFile = parse.tree(); + let file = File::new(&db, sql.clone(), 0); + assert_eq!(crate::db::parse(&db, file).errors(), vec![]); - if let Some(type_info) = hover(&file, offset) { + if let Some(type_info) = hover(&db, file, offset) { let offset_usize: usize = offset.into(); let title = format!("hover: {}", type_info); let group = Level::INFO.primary_title(&title).element( diff --git a/crates/squawk_ide/src/inlay_hints.rs b/crates/squawk_ide/src/inlay_hints.rs index 482aecd7..8c7004fd 100644 --- a/crates/squawk_ide/src/inlay_hints.rs +++ b/crates/squawk_ide/src/inlay_hints.rs @@ -1,9 +1,11 @@ -use crate::builtins::BUILTINS_SQL; +use crate::builtins::parse_builtins; +use crate::db::{File, parse}; use crate::goto_definition::FileId; use crate::resolve; use crate::symbols::Name; use crate::{binder, goto_definition}; use rowan::{TextRange, TextSize}; +use salsa::Database as Db; use squawk_syntax::ast::{self, AstNode}; /// `VSCode` has some theming options based on these types. @@ -26,20 +28,26 @@ pub struct InlayHint { pub file: Option, } -pub fn inlay_hints(file: &ast::SourceFile) -> Vec { +#[salsa::tracked] +pub fn inlay_hints(db: &dyn Db, file: File) -> Vec { + let parse = parse(db, file); + let source_file = parse.tree(); + let mut hints = vec![]; - for node in file.syntax().descendants() { + for node in source_file.syntax().descendants() { if let Some(call_expr) = ast::CallExpr::cast(node.clone()) { - inlay_hint_call_expr(&mut hints, file, call_expr); + inlay_hint_call_expr(db, &mut hints, file, &source_file, call_expr); } else if let Some(insert) = ast::Insert::cast(node) { - inlay_hint_insert(&mut hints, file, insert); + inlay_hint_insert(db, &mut hints, file, &source_file, insert); } } hints } fn inlay_hint_call_expr( + db: &dyn Db, hints: &mut Vec, + file_id: File, file: &ast::SourceFile, call_expr: ast::CallExpr, ) -> Option<()> { @@ -52,14 +60,14 @@ fn inlay_hint_call_expr( ast::FieldExpr::cast(expr.syntax().clone())?.field()? }; - let location = goto_definition::goto_definition(file, name_ref.syntax().text_range().start()) - .into_iter() - .next()?; + let location = + goto_definition::goto_definition(db, file_id, name_ref.syntax().text_range().start()) + .into_iter() + .next()?; let file = match location.file { goto_definition::FileId::Current => file, - // TODO: we should salsa this - goto_definition::FileId::Builtins => &ast::SourceFile::parse(BUILTINS_SQL).tree(), + goto_definition::FileId::Builtins => &parse_builtins(db).tree(), }; let function_name_node = file.syntax().covering_element(location.range); @@ -88,7 +96,9 @@ fn inlay_hint_call_expr( } fn inlay_hint_insert( + db: &dyn Db, hints: &mut Vec, + file_id: File, file: &ast::SourceFile, insert: ast::Insert, ) -> Option<()> { @@ -101,14 +111,13 @@ fn inlay_hint_insert( .start(); // We need to support the table definition not being found since we can // still provide inlay hints when a column list is provided - let location = goto_definition::goto_definition(file, name_start) + let location = goto_definition::goto_definition(db, file_id, name_start) .into_iter() .next(); let file = match location.as_ref().map(|x| x.file) { Some(goto_definition::FileId::Current) | None => file, - // TODO: we should salsa this - Some(goto_definition::FileId::Builtins) => &ast::SourceFile::parse(BUILTINS_SQL).tree(), + Some(goto_definition::FileId::Builtins) => &parse_builtins(db).tree(), }; let create_table = { @@ -222,18 +231,19 @@ fn target_list_from_select_variant(select: ast::SelectVariant) -> Option String { - let parse = ast::SourceFile::parse(sql); - assert_eq!(parse.errors(), vec![]); - let file: ast::SourceFile = parse.tree(); + let db = Database::default(); + let file = File::new(&db, sql.to_string(), 0); + + assert_eq!(crate::db::parse(&db, file).errors(), vec![]); - let hints = inlay_hints(&file); + let hints = inlay_hints(&db, file); if hints.is_empty() { return String::new(); diff --git a/crates/squawk_ide/src/lib.rs b/crates/squawk_ide/src/lib.rs index 9c2a1df8..27216c33 100644 --- a/crates/squawk_ide/src/lib.rs +++ b/crates/squawk_ide/src/lib.rs @@ -4,6 +4,7 @@ mod classify; pub mod code_actions; pub mod column_name; pub mod completion; +pub mod db; pub mod document_symbols; pub mod expand_selection; pub mod find_references; diff --git a/crates/squawk_ide/src/scope.rs b/crates/squawk_ide/src/scope.rs index bb27b469..e04674a6 100644 --- a/crates/squawk_ide/src/scope.rs +++ b/crates/squawk_ide/src/scope.rs @@ -5,7 +5,7 @@ use crate::symbols::{Name, SymbolId}; pub(crate) type ScopeId = Idx; -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, PartialEq)] pub(crate) struct Scope { #[allow(dead_code)] pub(crate) parent: Option, diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index b32885d0..ba8305e0 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -66,7 +66,7 @@ pub(crate) enum SymbolKind { Policy, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct Symbol { pub(crate) kind: SymbolKind, pub(crate) ptr: SyntaxNodePtr, diff --git a/crates/squawk_server/src/ignore.rs b/crates/squawk_server/src/ignore.rs index b52c74ac..e4651a70 100644 --- a/crates/squawk_server/src/ignore.rs +++ b/crates/squawk_server/src/ignore.rs @@ -67,6 +67,7 @@ fn is_ignore_comment(token: &SyntaxToken) -> bool { #[cfg(test)] mod test { use crate::{diagnostic::AssociatedDiagnosticData, lint::lint}; + use squawk_ide::db::{Database, File}; #[test] fn ignore_line() { @@ -85,7 +86,7 @@ create table c ( b int ); "; - let ignore_line_edits = lint(sql) + let ignore_line_edits = lint_sql(sql) .into_iter() .flat_map(|x| { let data = x.data?; @@ -132,7 +133,7 @@ create table c ( b int ); "; - let ignore_line_edits = lint(sql) + let ignore_line_edits = lint_sql(sql) .into_iter() .flat_map(|x| { let data = x.data?; @@ -198,4 +199,10 @@ create table c ( result } + + fn lint_sql(sql: &str) -> Vec { + let db = Database::default(); + let file = File::new(&db, sql.to_owned(), 0); + lint(&db, file) + } } diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 9fc44a10..32742a2e 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -1,6 +1,5 @@ use ::line_index::LineIndex; use anyhow::{Context, Result}; -use etcetera::BaseStrategy; use log::info; use lsp_server::{Connection, Message, Notification, Response}; use lsp_types::{ @@ -25,41 +24,25 @@ use lsp_types::{ }; use rowan::TextRange; use salsa::Setter; +use squawk_ide::builtins::{builtins_line_index, builtins_url}; +use squawk_ide::code_actions::code_actions; use squawk_ide::completion::completion; +use squawk_ide::db::{Database, File, line_index, parse}; use squawk_ide::document_symbols::{DocumentSymbolKind, document_symbols}; use squawk_ide::find_references::find_references; use squawk_ide::goto_definition::goto_definition; use squawk_ide::hover::hover; use squawk_ide::inlay_hints::inlay_hints; -use squawk_ide::{builtins::BUILTINS_SQL, code_actions::code_actions}; -use std::{collections::HashMap, fs, sync::OnceLock}; +use std::collections::HashMap; use diagnostic::DIAGNOSTIC_NAME; -use crate::db::{Database, File, line_index, parse}; use crate::diagnostic::AssociatedDiagnosticData; -mod db; mod diagnostic; mod ignore; mod lint; mod lsp_utils; -fn builtins_url() -> Option { - // TODO: once we get salsa setup, we can migrate this over - static BUILTINS_URL: OnceLock> = OnceLock::new(); - BUILTINS_URL - .get_or_init(|| { - let strategy = etcetera::base_strategy::choose_base_strategy().ok()?; - let config_dir = strategy.config_dir(); - let cache_dir = config_dir.join("squawk/stubs"); - let path = cache_dir.join("builtins.sql"); - fs::create_dir_all(cache_dir).ok()?; - fs::write(&path, BUILTINS_SQL).ok()?; - Url::from_file_path(&path).ok() - }) - .clone() -} - struct DocumentState { content: String, #[allow(dead_code)] @@ -253,11 +236,10 @@ fn handle_goto_definition( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); let offset = lsp_utils::offset(&line_index, position).unwrap(); - let ranges = goto_definition(&parse.tree(), offset) + let ranges = goto_definition(db, file, offset) .into_iter() .filter_map(|location| { debug_assert!( @@ -267,12 +249,12 @@ fn handle_goto_definition( let uri = match location.file { squawk_ide::goto_definition::FileId::Current => uri.clone(), - squawk_ide::goto_definition::FileId::Builtins => builtins_url()?, + squawk_ide::goto_definition::FileId::Builtins => builtins_url(db)?, }; let line_index = match location.file { squawk_ide::goto_definition::FileId::Current => &line_index, - squawk_ide::goto_definition::FileId::Builtins => &LineIndex::new(BUILTINS_SQL), + squawk_ide::goto_definition::FileId::Builtins => &builtins_line_index(db), }; let range = lsp_utils::range(line_index, location.range); @@ -305,11 +287,10 @@ fn handle_hover( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); let offset = lsp_utils::offset(&line_index, position).unwrap(); - let type_info = hover(&parse.tree(), offset); + let type_info = hover(db, file, offset); let result = type_info.map(|type_str| Hover { contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString { @@ -339,11 +320,9 @@ fn handle_inlay_hints( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); - // TODO: move this to a tracked function - let hints = inlay_hints(&parse.tree()); + let hints = inlay_hints(db, file); let lsp_hints: Vec = hints .into_iter() @@ -353,14 +332,12 @@ fn handle_inlay_hints( let uri = match hint.file { Some(squawk_ide::goto_definition::FileId::Current) | None => uri.clone(), - Some(squawk_ide::goto_definition::FileId::Builtins) => builtins_url()?, + Some(squawk_ide::goto_definition::FileId::Builtins) => builtins_url(db)?, }; let line_index = match hint.file { Some(squawk_ide::goto_definition::FileId::Current) | None => &line_index, - Some(squawk_ide::goto_definition::FileId::Builtins) => { - &LineIndex::new(BUILTINS_SQL) - } + Some(squawk_ide::goto_definition::FileId::Builtins) => &builtins_line_index(db), }; let kind: InlayHintKind = match hint.kind { @@ -415,10 +392,9 @@ fn handle_document_symbol( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); - let symbols = document_symbols(&parse.tree()); + let symbols = document_symbols(db, file); fn convert_symbol( sym: squawk_ide::document_symbols::DocumentSymbol, @@ -558,11 +534,10 @@ fn handle_references( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); let offset = lsp_utils::offset(&line_index, position).unwrap(); - let refs = find_references(&parse.tree(), offset); + let refs = find_references(db, file, offset); let include_declaration = params.context.include_declaration; let locations: Vec = refs @@ -571,11 +546,11 @@ fn handle_references( .filter_map(|loc| { let uri = match loc.file { squawk_ide::goto_definition::FileId::Current => uri.clone(), - squawk_ide::goto_definition::FileId::Builtins => builtins_url()?, + squawk_ide::goto_definition::FileId::Builtins => builtins_url(db)?, }; let line_index = match loc.file { squawk_ide::goto_definition::FileId::Current => &line_index, - squawk_ide::goto_definition::FileId::Builtins => &LineIndex::new(BUILTINS_SQL), + squawk_ide::goto_definition::FileId::Builtins => &builtins_line_index(db), }; Some(Location { uri, @@ -605,7 +580,6 @@ fn handle_completion( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); let Some(offset) = lsp_utils::offset(&line_index, position) else { @@ -618,8 +592,7 @@ fn handle_completion( return Ok(()); }; - // TODO: move this to a tracked function - let completion_items = completion(&parse.tree(), offset) + let completion_items = completion(db, file, offset) .into_iter() .map(lsp_utils::completion_item) .collect(); @@ -648,12 +621,10 @@ fn handle_code_action( let db = file_system.db(); let file = file_system.file(&uri).unwrap(); - let parse = parse(db, file); let line_index = line_index(db, file); let offset = lsp_utils::offset(&line_index, params.range.start).unwrap(); - // TODO: move this to a tracked function - let ide_actions = code_actions(parse.tree(), offset).unwrap_or_default(); + let ide_actions = code_actions(db, file, offset).unwrap_or_default(); for action in ide_actions { let lsp_action = lsp_utils::code_action(&line_index, uri.clone(), action); @@ -805,10 +776,10 @@ fn handle_did_open( let content = params.text_document.text; let version = params.text_document.version; - // TODO: move this to a tracked function - let diagnostics = lint::lint(&content); - file_system.set(uri.clone(), DocumentState { content, version }); + let db = file_system.db(); + let file = file_system.file(&uri).unwrap(); + let diagnostics = lint::lint(db, file); // TODO: we need a better setup for "run func when input changed" publish_diagnostics(connection, uri, version, diagnostics)?; @@ -831,17 +802,17 @@ fn handle_did_change( let updated_content = lsp_utils::apply_incremental_changes(content, params.content_changes); - // TODO: move this to a tracked function - let diagnostics = lint::lint(&updated_content); - publish_diagnostics(connection, uri.clone(), version, diagnostics)?; - file_system.set( - uri, + uri.clone(), DocumentState { content: updated_content, version, }, ); + let db = file_system.db(); + let file = file_system.file(&uri).unwrap(); + let diagnostics = lint::lint(db, file); + publish_diagnostics(connection, uri, version, diagnostics)?; Ok(()) } diff --git a/crates/squawk_server/src/lint.rs b/crates/squawk_server/src/lint.rs index eb229980..71f1d2b9 100644 --- a/crates/squawk_server/src/lint.rs +++ b/crates/squawk_server/src/lint.rs @@ -1,7 +1,8 @@ -use line_index::LineIndex; +use ::line_index::LineIndex; use lsp_types::{CodeDescription, Diagnostic, DiagnosticSeverity, Position, Range, TextEdit, Url}; +use salsa::Database as Db; +use squawk_ide::db::{File, line_index as file_line_index, parse}; use squawk_linter::{Edit, Linter}; -use squawk_syntax::SourceFile; use crate::{ DIAGNOSTIC_NAME, @@ -19,12 +20,14 @@ fn to_text_edit(edit: Edit, line_index: &LineIndex) -> Option { Some(TextEdit::new(range, edit.text.unwrap_or_default())) } -pub(crate) fn lint(content: &str) -> Vec { - let parse = SourceFile::parse(content); +#[salsa::tracked] +pub(crate) fn lint(db: &dyn Db, file: File) -> Vec { + let parse = parse(db, file); + let content = file.content(db); let parse_errors = parse.errors(); let mut linter = Linter::with_all_rules(); let violations = linter.lint(&parse, content); - let line_index = LineIndex::new(content); + let line_index = file_line_index(db, file); let mut diagnostics = Vec::with_capacity(violations.len() + parse_errors.len()); diff --git a/crates/squawk_wasm/Cargo.toml b/crates/squawk_wasm/Cargo.toml index b36aafb6..2118946a 100644 --- a/crates/squawk_wasm/Cargo.toml +++ b/crates/squawk_wasm/Cargo.toml @@ -24,6 +24,7 @@ squawk-linter.workspace = true squawk-lexer.workspace = true squawk-ide.workspace = true rowan.workspace = true +salsa.workspace = true wasm-bindgen.workspace = true serde-wasm-bindgen.workspace = true diff --git a/crates/squawk_wasm/src/lib.rs b/crates/squawk_wasm/src/lib.rs index 25827e64..d722b622 100644 --- a/crates/squawk_wasm/src/lib.rs +++ b/crates/squawk_wasm/src/lib.rs @@ -1,8 +1,10 @@ use line_index::LineIndex; use log::info; use rowan::TextRange; +use salsa::Setter; use serde::{Deserialize, Serialize}; -use squawk_ide::builtins::BUILTINS_SQL; +use squawk_ide::builtins::builtins_line_index; +use squawk_ide::db::{self, Database, File}; use squawk_ide::goto_definition::FileId; use squawk_syntax::ast::AstNode; use wasm_bindgen::prelude::*; @@ -25,278 +27,110 @@ pub fn run() { } #[wasm_bindgen] -pub fn dump_cst(text: String) -> String { - let parse = squawk_syntax::SourceFile::parse(&text); - format!("{:#?}", parse.syntax_node()) +pub struct SquawkDatabase { + db: Database, + file: Option, } #[wasm_bindgen] -pub fn dump_tokens(text: String) -> String { - let tokens = squawk_lexer::tokenize(&text); - let mut start = 0; - let mut out = String::new(); - for token in tokens { - let end = start + token.len; - let content = &text[start as usize..(end) as usize]; - out += &format!("{:?}@{start}..{end} {:?}\n", token.kind, content); - start += token.len; +#[allow(clippy::new_without_default)] +impl SquawkDatabase { + #[wasm_bindgen(constructor)] + pub fn new() -> SquawkDatabase { + SquawkDatabase { + db: Database::default(), + file: None, + } } - out -} -#[expect(unused)] -#[derive(Serialize)] -enum Severity { - Hint, - Info, - Warning, - Error, -} + pub fn open_file(&mut self, content: String) { + let file = File::new(&self.db, content, 0); + self.file = Some(file); + } -#[derive(Serialize)] -struct LintError { - severity: Severity, - code: String, - message: String, - start_line_number: u32, - start_column: u32, - end_line_number: u32, - end_column: u32, - // used for the linter tab - range_start: usize, - // used for the linter tab - range_end: usize, - // used for the linter tab - messages: Vec, - fix: Option, -} + pub fn update_file(&mut self, content: String, version: i32) { + if let Some(file) = self.file { + file.set_content(&mut self.db).to(content); + file.set_version(&mut self.db).to(version); + } + } -#[derive(Serialize)] -struct Fix { - title: String, - edits: Vec, -} + fn file(&self) -> Result { + self.file + .ok_or_else(|| Error::new("No file open. Call open_file first.")) + } -#[derive(Serialize)] -struct TextEdit { - start_line_number: u32, - start_column: u32, - end_line_number: u32, - end_column: u32, - text: String, -} + pub fn dump_cst(&self) -> Result { + let file = self.file()?; + let parse = db::parse(&self.db, file); + Ok(format!("{:#?}", parse.syntax_node())) + } -#[wasm_bindgen] -pub fn lint(text: String) -> Result { - let mut linter = squawk_linter::Linter::with_all_rules(); - let parse = squawk_syntax::SourceFile::parse(&text); - let parse_errors = parse.errors(); - - let line_index = LineIndex::new(&text); - - // TODO: chain these with other stuff - let parse_errors = parse_errors.iter().map(|x| { - let range_start = x.range().start(); - let range_end = x.range().end(); - let start = line_index.line_col(range_start); - let end = line_index.line_col(range_end); - let start = line_index - .to_wide(line_index::WideEncoding::Utf16, start) - .unwrap(); - let end = line_index - .to_wide(line_index::WideEncoding::Utf16, end) - .unwrap(); - LintError { - severity: Severity::Error, - code: "syntax-error".to_string(), - message: x.message().to_string(), - start_line_number: start.line, - start_column: start.col, - end_line_number: end.line, - end_column: end.col, - range_start: range_start.into(), - range_end: range_end.into(), - messages: vec![], - fix: None, + pub fn dump_tokens(&self) -> Result { + let file = self.file()?; + let content = file.content(&self.db); + let tokens = squawk_lexer::tokenize(content); + let mut start = 0; + let mut out = String::new(); + for token in tokens { + let end = start + token.len; + let text = &content[start as usize..(end) as usize]; + out += &format!("{:?}@{start}..{end} {:?}\n", token.kind, text); + start += token.len; } - }); - - let lint_errors = linter.lint(&parse, &text); - let errors = lint_errors.into_iter().map(|x| { - let start = line_index.line_col(x.text_range.start()); - let end = line_index.line_col(x.text_range.end()); - let start = line_index - .to_wide(line_index::WideEncoding::Utf16, start) - .unwrap(); - let end = line_index - .to_wide(line_index::WideEncoding::Utf16, end) - .unwrap(); - - let messages = x.help.into_iter().collect(); - - let fix = x.fix.map(|fix| { - let edits = fix - .edits - .into_iter() - .map(|edit| { - let start_pos = line_index.line_col(edit.text_range.start()); - let end_pos = line_index.line_col(edit.text_range.end()); - let start_wide = line_index - .to_wide(line_index::WideEncoding::Utf16, start_pos) - .unwrap(); - let end_wide = line_index - .to_wide(line_index::WideEncoding::Utf16, end_pos) - .unwrap(); - - TextEdit { - start_line_number: start_wide.line, - start_column: start_wide.col, - end_line_number: end_wide.line, - end_column: end_wide.col, - text: edit.text.unwrap_or_default(), - } - }) - .collect(); + Ok(out) + } - Fix { - title: fix.title, - edits, + pub fn lint(&self) -> Result { + let file = self.file()?; + let content = file.content(&self.db); + let mut linter = squawk_linter::Linter::with_all_rules(); + let parse = db::parse(&self.db, file); + let parse_errors = parse.errors(); + + let line_index = db::line_index(&self.db, file); + + let parse_errors = parse_errors.iter().map(|x| { + let range_start = x.range().start(); + let range_end = x.range().end(); + let start = line_index.line_col(range_start); + let end = line_index.line_col(range_end); + let start = line_index + .to_wide(line_index::WideEncoding::Utf16, start) + .unwrap(); + let end = line_index + .to_wide(line_index::WideEncoding::Utf16, end) + .unwrap(); + LintError { + severity: Severity::Error, + code: "syntax-error".to_string(), + message: x.message().to_string(), + start_line_number: start.line, + start_column: start.col, + end_line_number: end.line, + end_column: end.col, + range_start: range_start.into(), + range_end: range_end.into(), + messages: vec![], + fix: None, } }); - LintError { - code: x.code.to_string(), - range_start: x.text_range.start().into(), - range_end: x.text_range.end().into(), - message: x.message.clone(), - messages, - // parser errors should be error - severity: Severity::Warning, - start_line_number: start.line, - start_column: start.col, - end_line_number: end.line, - end_column: end.col, - fix, - } - }); - - let mut errors_to_dump = errors.chain(parse_errors).collect::>(); - errors_to_dump.sort_by_key(|k| (k.start_line_number, k.start_column)); - - serde_wasm_bindgen::to_value(&errors_to_dump).map_err(into_error) -} - -fn into_error(err: E) -> Error { - Error::new(&err.to_string()) -} - -#[wasm_bindgen] -pub fn goto_definition(content: String, line: u32, col: u32) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let current_line_index = LineIndex::new(&content); - let builtins_line_index = LineIndex::new(BUILTINS_SQL); - let offset = position_to_offset(¤t_line_index, line, col)?; - let result = squawk_ide::goto_definition::goto_definition(&parse.tree(), offset); - - let response: Vec = result - .into_iter() - .map(|location| { - let range = location.range; - let (file, line_index) = match location.file { - FileId::Current => ("current", ¤t_line_index), - FileId::Builtins => ("builtins", &builtins_line_index), - }; - let start = line_index.line_col(range.start()); - let end = line_index.line_col(range.end()); - let start_wide = line_index + let lint_errors = linter.lint(&parse, content); + let errors = lint_errors.into_iter().map(|x| { + let start = line_index.line_col(x.text_range.start()); + let end = line_index.line_col(x.text_range.end()); + let start = line_index .to_wide(line_index::WideEncoding::Utf16, start) .unwrap(); - let end_wide = line_index + let end = line_index .to_wide(line_index::WideEncoding::Utf16, end) .unwrap(); - LocationRange { - file: file.to_string(), - start_line: start_wide.line, - start_column: start_wide.col, - end_line: end_wide.line, - end_column: end_wide.col, - } - }) - .collect(); + let messages = x.help.into_iter().collect(); - serde_wasm_bindgen::to_value(&response).map_err(into_error) -} - -#[wasm_bindgen] -pub fn hover(content: String, line: u32, col: u32) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let offset = position_to_offset(&line_index, line, col)?; - let result = squawk_ide::hover::hover(&parse.tree(), offset); - - serde_wasm_bindgen::to_value(&result).map_err(into_error) -} - -#[wasm_bindgen] -pub fn find_references(content: String, line: u32, col: u32) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let offset = position_to_offset(&line_index, line, col)?; - let references = squawk_ide::find_references::find_references(&parse.tree(), offset); - - let builtins_line_index = LineIndex::new(BUILTINS_SQL); - let locations: Vec = references - .iter() - .map(|loc| { - let (li, file) = match loc.file { - FileId::Current => (&line_index, "current"), - FileId::Builtins => (&builtins_line_index, "builtin"), - }; - let start = li.line_col(loc.range.start()); - let end = li.line_col(loc.range.end()); - let start_wide = li.to_wide(line_index::WideEncoding::Utf16, start).unwrap(); - let end_wide = li.to_wide(line_index::WideEncoding::Utf16, end).unwrap(); - - LocationRange { - file: file.to_string(), - start_line: start_wide.line, - start_column: start_wide.col, - end_line: end_wide.line, - end_column: end_wide.col, - } - }) - .collect(); - - serde_wasm_bindgen::to_value(&locations).map_err(into_error) -} - -#[wasm_bindgen] -pub fn document_symbols(content: String) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let symbols = squawk_ide::document_symbols::document_symbols(&parse.tree()); - - let converted: Vec = symbols - .into_iter() - .map(|s| convert_document_symbol(&line_index, s)) - .collect(); - - serde_wasm_bindgen::to_value(&converted).map_err(into_error) -} - -#[wasm_bindgen] -pub fn code_actions(content: String, line: u32, col: u32) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let offset = position_to_offset(&line_index, line, col)?; - let actions = squawk_ide::code_actions::code_actions(parse.tree(), offset); - - let converted = actions.map(|actions| { - actions - .into_iter() - .map(|action| { - let edits = action + let fix = x.fix.map(|fix| { + let edits = fix .edits .into_iter() .map(|edit| { @@ -319,20 +153,292 @@ pub fn code_actions(content: String, line: u32, col: u32) -> Result "quickfix", - squawk_ide::code_actions::ActionKind::RefactorRewrite => "refactor.rewrite", + } + }); + + LintError { + code: x.code.to_string(), + range_start: x.text_range.start().into(), + range_end: x.text_range.end().into(), + message: x.message.clone(), + messages, + severity: Severity::Warning, + start_line_number: start.line, + start_column: start.col, + end_line_number: end.line, + end_column: end.col, + fix, + } + }); + + let mut errors_to_dump = errors.chain(parse_errors).collect::>(); + errors_to_dump.sort_by_key(|k| (k.start_line_number, k.start_column)); + + serde_wasm_bindgen::to_value(&errors_to_dump).map_err(into_error) + } + + pub fn goto_definition(&self, line: u32, col: u32) -> Result { + let file = self.file()?; + let current_line_index = db::line_index(&self.db, file); + let offset = position_to_offset(¤t_line_index, line, col)?; + let builtins_li = builtins_line_index(&self.db); + let result = squawk_ide::goto_definition::goto_definition(&self.db, file, offset); + + let response: Vec = result + .into_iter() + .map(|location| { + let range = location.range; + let (file, line_index) = match location.file { + FileId::Current => ("current", ¤t_line_index), + FileId::Builtins => ("builtins", &builtins_li), + }; + let start = line_index.line_col(range.start()); + let end = line_index.line_col(range.end()); + let start_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, start) + .unwrap(); + let end_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, end) + .unwrap(); + + LocationRange { + file: file.to_string(), + start_line: start_wide.line, + start_column: start_wide.col, + end_line: end_wide.line, + end_column: end_wide.col, + } + }) + .collect(); + + serde_wasm_bindgen::to_value(&response).map_err(into_error) + } + + pub fn hover(&self, line: u32, col: u32) -> Result { + let file = self.file()?; + let line_index = db::line_index(&self.db, file); + let offset = position_to_offset(&line_index, line, col)?; + let result = squawk_ide::hover::hover(&self.db, file, offset); + + serde_wasm_bindgen::to_value(&result).map_err(into_error) + } + + pub fn find_references(&self, line: u32, col: u32) -> Result { + let file = self.file()?; + let line_index = db::line_index(&self.db, file); + let offset = position_to_offset(&line_index, line, col)?; + let references = squawk_ide::find_references::find_references(&self.db, file, offset); + let builtins_li = builtins_line_index(&self.db); + let locations: Vec = references + .iter() + .map(|loc| { + let (li, file) = match loc.file { + FileId::Current => (&line_index, "current"), + FileId::Builtins => (&builtins_li, "builtins"), + }; + let start = li.line_col(loc.range.start()); + let end = li.line_col(loc.range.end()); + let start_wide = li.to_wide(line_index::WideEncoding::Utf16, start).unwrap(); + let end_wide = li.to_wide(line_index::WideEncoding::Utf16, end).unwrap(); + + LocationRange { + file: file.to_string(), + start_line: start_wide.line, + start_column: start_wide.col, + end_line: end_wide.line, + end_column: end_wide.col, + } + }) + .collect(); + + serde_wasm_bindgen::to_value(&locations).map_err(into_error) + } + + pub fn document_symbols(&self) -> Result { + let file = self.file()?; + let line_index = db::line_index(&self.db, file); + let symbols = squawk_ide::document_symbols::document_symbols(&self.db, file); + + let converted: Vec = symbols + .into_iter() + .map(|s| convert_document_symbol(&line_index, s)) + .collect(); + + serde_wasm_bindgen::to_value(&converted).map_err(into_error) + } + + pub fn code_actions(&self, line: u32, col: u32) -> Result { + let file = self.file()?; + let line_index = db::line_index(&self.db, file); + let offset = position_to_offset(&line_index, line, col)?; + let actions = squawk_ide::code_actions::code_actions(&self.db, file, offset); + + let converted = actions.map(|actions| { + actions + .into_iter() + .map(|action| { + let edits = action + .edits + .into_iter() + .map(|edit| { + let start_pos = line_index.line_col(edit.text_range.start()); + let end_pos = line_index.line_col(edit.text_range.end()); + let start_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, start_pos) + .unwrap(); + let end_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, end_pos) + .unwrap(); + + TextEdit { + start_line_number: start_wide.line, + start_column: start_wide.col, + end_line_number: end_wide.line, + end_column: end_wide.col, + text: edit.text.unwrap_or_default(), + } + }) + .collect(); + + WasmCodeAction { + title: action.title, + edits, + kind: match action.kind { + squawk_ide::code_actions::ActionKind::QuickFix => "quickfix", + squawk_ide::code_actions::ActionKind::RefactorRewrite => { + "refactor.rewrite" + } + } + .to_string(), + } + }) + .collect::>() + }); + + serde_wasm_bindgen::to_value(&converted).map_err(into_error) + } + + pub fn inlay_hints(&self) -> Result { + let file = self.file()?; + let line_index = db::line_index(&self.db, file); + let hints = squawk_ide::inlay_hints::inlay_hints(&self.db, file); + + let converted: Vec = hints + .into_iter() + .map(|hint| { + let position = line_index.line_col(hint.position); + let position_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, position) + .unwrap(); + + WasmInlayHint { + line: position_wide.line, + column: position_wide.col, + label: hint.label, + kind: match hint.kind { + squawk_ide::inlay_hints::InlayHintKind::Type => "type", + squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter", } .to_string(), } }) - .collect::>() - }); + .collect(); + + serde_wasm_bindgen::to_value(&converted).map_err(into_error) + } - serde_wasm_bindgen::to_value(&converted).map_err(into_error) + pub fn selection_ranges(&self, positions: Vec) -> Result { + let file = self.file()?; + let parse = db::parse(&self.db, file); + let line_index = db::line_index(&self.db, file); + let tree = parse.tree(); + let root = tree.syntax(); + + let mut results: Vec> = vec![]; + + for pos in positions { + let pos: Position = serde_wasm_bindgen::from_value(pos).map_err(into_error)?; + let offset = position_to_offset(&line_index, pos.line, pos.column)?; + + let mut ranges = vec![]; + let mut range = TextRange::new(offset, offset); + + for _ in 0..20 { + let next = squawk_ide::expand_selection::extend_selection(root, range); + if next == range { + break; + } + + let start = line_index.line_col(next.start()); + let end = line_index.line_col(next.end()); + let start_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, start) + .unwrap(); + let end_wide = line_index + .to_wide(line_index::WideEncoding::Utf16, end) + .unwrap(); + + ranges.push(WasmSelectionRange { + start_line: start_wide.line, + start_column: start_wide.col, + end_line: end_wide.line, + end_column: end_wide.col, + }); + + range = next; + } + + results.push(ranges); + } + + serde_wasm_bindgen::to_value(&results).map_err(into_error) + } + + pub fn completion(&self, line: u32, col: u32) -> Result { + let file = self.file()?; + let line_index = db::line_index(&self.db, file); + let offset = position_to_offset(&line_index, line, col)?; + let items = squawk_ide::completion::completion(&self.db, file, offset); + + let converted: Vec = items + .into_iter() + .map(|item| WasmCompletionItem { + label: item.label, + kind: match item.kind { + squawk_ide::completion::CompletionItemKind::Keyword => "keyword", + squawk_ide::completion::CompletionItemKind::Table => "table", + squawk_ide::completion::CompletionItemKind::Column => "column", + squawk_ide::completion::CompletionItemKind::Function => "function", + squawk_ide::completion::CompletionItemKind::Schema => "schema", + squawk_ide::completion::CompletionItemKind::Type => "type", + squawk_ide::completion::CompletionItemKind::Snippet => "snippet", + squawk_ide::completion::CompletionItemKind::Operator => "operator", + } + .to_string(), + detail: item.detail, + insert_text: item.insert_text, + insert_text_format: item.insert_text_format.map(|fmt| { + match fmt { + squawk_ide::completion::CompletionInsertTextFormat::PlainText => { + "plainText" + } + squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet", + } + .to_string() + }), + trigger_completion_after_insert: item.trigger_completion_after_insert, + }) + .collect(); + + serde_wasm_bindgen::to_value(&converted).map_err(into_error) + } +} + +fn into_error(err: E) -> Error { + Error::new(&err.to_string()) } fn position_to_offset( @@ -351,38 +457,6 @@ fn position_to_offset( .ok_or_else(|| Error::new("Invalid position offset")) } -#[derive(Serialize)] -struct LocationRange { - file: String, - start_line: u32, - start_column: u32, - end_line: u32, - end_column: u32, -} - -#[derive(Serialize)] -struct WasmCodeAction { - title: String, - edits: Vec, - kind: String, -} - -#[derive(Serialize)] -struct WasmDocumentSymbol { - name: String, - detail: Option, - kind: String, - start_line: u32, - start_column: u32, - end_line: u32, - end_column: u32, - selection_start_line: u32, - selection_start_column: u32, - selection_end_line: u32, - selection_end_column: u32, - children: Vec, -} - fn convert_document_symbol( line_index: &LineIndex, symbol: squawk_ide::document_symbols::DocumentSymbol, @@ -456,87 +530,75 @@ fn convert_document_symbol( } } -#[wasm_bindgen] -pub fn inlay_hints(content: String) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let hints = squawk_ide::inlay_hints::inlay_hints(&parse.tree()); - - let converted: Vec = hints - .into_iter() - .map(|hint| { - let position = line_index.line_col(hint.position); - let position_wide = line_index - .to_wide(line_index::WideEncoding::Utf16, position) - .unwrap(); - - WasmInlayHint { - line: position_wide.line, - column: position_wide.col, - label: hint.label, - kind: match hint.kind { - squawk_ide::inlay_hints::InlayHintKind::Type => "type", - squawk_ide::inlay_hints::InlayHintKind::Parameter => "parameter", - } - .to_string(), - } - }) - .collect(); - - serde_wasm_bindgen::to_value(&converted).map_err(into_error) +#[expect(unused)] +#[derive(Serialize)] +enum Severity { + Hint, + Info, + Warning, + Error, } -#[derive(Deserialize)] -struct Position { - line: u32, - column: u32, +#[derive(Serialize)] +struct LintError { + severity: Severity, + code: String, + message: String, + start_line_number: u32, + start_column: u32, + end_line_number: u32, + end_column: u32, + range_start: usize, + range_end: usize, + messages: Vec, + fix: Option, } -#[wasm_bindgen] -pub fn selection_ranges(content: String, positions: Vec) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let tree = parse.tree(); - let root = tree.syntax(); - - let mut results: Vec> = vec![]; - - for pos in positions { - let pos: Position = serde_wasm_bindgen::from_value(pos).map_err(into_error)?; - let offset = position_to_offset(&line_index, pos.line, pos.column)?; - - let mut ranges = vec![]; - let mut range = TextRange::new(offset, offset); - - for _ in 0..20 { - let next = squawk_ide::expand_selection::extend_selection(root, range); - if next == range { - break; - } - - let start = line_index.line_col(next.start()); - let end = line_index.line_col(next.end()); - let start_wide = line_index - .to_wide(line_index::WideEncoding::Utf16, start) - .unwrap(); - let end_wide = line_index - .to_wide(line_index::WideEncoding::Utf16, end) - .unwrap(); +#[derive(Serialize)] +struct Fix { + title: String, + edits: Vec, +} - ranges.push(WasmSelectionRange { - start_line: start_wide.line, - start_column: start_wide.col, - end_line: end_wide.line, - end_column: end_wide.col, - }); +#[derive(Serialize)] +struct TextEdit { + start_line_number: u32, + start_column: u32, + end_line_number: u32, + end_column: u32, + text: String, +} - range = next; - } +#[derive(Serialize)] +struct LocationRange { + file: String, + start_line: u32, + start_column: u32, + end_line: u32, + end_column: u32, +} - results.push(ranges); - } +#[derive(Serialize)] +struct WasmCodeAction { + title: String, + edits: Vec, + kind: String, +} - serde_wasm_bindgen::to_value(&results).map_err(into_error) +#[derive(Serialize)] +struct WasmDocumentSymbol { + name: String, + detail: Option, + kind: String, + start_line: u32, + start_column: u32, + end_line: u32, + end_column: u32, + selection_start_line: u32, + selection_start_column: u32, + selection_end_line: u32, + selection_end_column: u32, + children: Vec, } #[derive(Serialize)] @@ -555,44 +617,6 @@ struct WasmSelectionRange { end_column: u32, } -#[wasm_bindgen] -pub fn completion(content: String, line: u32, col: u32) -> Result { - let parse = squawk_syntax::SourceFile::parse(&content); - let line_index = LineIndex::new(&content); - let offset = position_to_offset(&line_index, line, col)?; - let items = squawk_ide::completion::completion(&parse.tree(), offset); - - let converted: Vec = items - .into_iter() - .map(|item| WasmCompletionItem { - label: item.label, - kind: match item.kind { - squawk_ide::completion::CompletionItemKind::Keyword => "keyword", - squawk_ide::completion::CompletionItemKind::Table => "table", - squawk_ide::completion::CompletionItemKind::Column => "column", - squawk_ide::completion::CompletionItemKind::Function => "function", - squawk_ide::completion::CompletionItemKind::Schema => "schema", - squawk_ide::completion::CompletionItemKind::Type => "type", - squawk_ide::completion::CompletionItemKind::Snippet => "snippet", - squawk_ide::completion::CompletionItemKind::Operator => "operator", - } - .to_string(), - detail: item.detail, - insert_text: item.insert_text, - insert_text_format: item.insert_text_format.map(|fmt| { - match fmt { - squawk_ide::completion::CompletionInsertTextFormat::PlainText => "plainText", - squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet", - } - .to_string() - }), - trigger_completion_after_insert: item.trigger_completion_after_insert, - }) - .collect(); - - serde_wasm_bindgen::to_value(&converted).map_err(into_error) -} - #[derive(Serialize)] struct WasmCompletionItem { label: String, @@ -602,3 +626,9 @@ struct WasmCompletionItem { insert_text_format: Option, trigger_completion_after_insert: bool, } + +#[derive(Deserialize)] +struct Position { + line: u32, + column: u32, +} diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 614a2ff3..30691a99 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -128,10 +128,11 @@ function initialValue(): string | null { export function App() { const [mode, setActiveMode] = useMode() const [text, setState] = useState(() => initialValue() ?? SETTINGS.value) + const [version, setVersion] = useState(0) const [file, setFile] = useState<"current" | "builtins">("current") const editorRef = useRef(null) - const markers = useMarkers(text) + const markers = useMarkers(text, version) return (
@@ -177,8 +178,9 @@ export function App() { /> ) : null} { + onChange={(text, version) => { setState(text) + setVersion(version) }} autoFocus markers={markers} @@ -196,9 +198,9 @@ export function App() {
{mode === "Syntax Tree" ? ( // TODO: it might be better to have an editor and switch the underlying monaco models - + ) : mode === "Tokens" ? ( - + ) : mode === "Lint" ? ( ) : mode == null ? null : ( @@ -225,8 +227,8 @@ function BuiltinsBanner({ onBack }: { onBack: () => void }) { ) } -function TokenPanel({ text }: { text: string }) { - const value = useDumpTokens(text) +function TokenPanel({ text, version }: { text: string; version: number }) { + const value = useDumpTokens(text, version) return ( void + onChange?: (_: string, version: number) => void onSave?: (_: string) => void settings: monaco.editor.IStandaloneEditorConstructionOptions markers?: Marker[] onModelChange?: (uri: monaco.Uri | null) => void ref?: React.RefObject }) { - const onChangeText = useEffectEvent((text: string) => { - onChange?.(text) + const onChangeText = useEffectEvent((text: string, version: number) => { + onChange?.(text, version) }) const onSaveText = useEffectEvent((text: string) => { onSave?.(text) @@ -616,7 +618,7 @@ function Editor({ }) editor.onDidChangeModelContent(() => { - onChangeText(editor.getValue()) + onChangeText(editor.getValue(), editor.getModel()?.getVersionId() ?? 0) }) if (autoFocusRef.current) { editor.focus() @@ -677,8 +679,8 @@ function createMarkerKey(marker: { return `${marker.startLineNumber}:${marker.startColumn}:${marker.endLineNumber}:${marker.endColumn}:${marker.message}` } -function SyntaxTreePanel({ text }: { text: string }) { - const value = useDumpCst(text) +function SyntaxTreePanel({ text, version }: { text: string; version: number }) { + const value = useDumpCst(text, version) return ( { - const errors = useErrors(text) +function useMarkers(text: string, version: number): Array { + const errors = useErrors(text, version) return errors.map((x): Marker => { const startLineNumber = x.start_line_number + 1 const startColumn = x.start_column + 1 diff --git a/playground/src/providers.tsx b/playground/src/providers.tsx index f846f120..0eb27bcb 100644 --- a/playground/src/providers.tsx +++ b/playground/src/providers.tsx @@ -15,10 +15,11 @@ export async function provideInlayHints( model: monaco.editor.ITextModel, ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return { hints: [], dispose: () => {} } try { - const wasmHints = inlay_hints(content) + const wasmHints = inlay_hints(content, version) const hints = wasmHints.map((hint) => ({ label: hint.label, @@ -43,10 +44,11 @@ export async function provideDocumentSymbols( model: monaco.editor.ITextModel, ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return [] try { - const symbols = document_symbols(content) + const symbols = document_symbols(content, version) return symbols.map((symbol) => convertDocumentSymbol(symbol)) } catch (e) { @@ -97,11 +99,13 @@ export async function provideCodeActions( position: monaco.Position, ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return [] try { const actions = code_actions( content, + version, position.lineNumber - 1, position.column - 1, ) @@ -138,10 +142,16 @@ export async function provideHover( position: monaco.Position, ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return null try { - const result = hover(content, position.lineNumber - 1, position.column - 1) + const result = hover( + content, + version, + position.lineNumber - 1, + position.column - 1, + ) if (!result) return null @@ -162,11 +172,13 @@ export async function provideDefinition( position: monaco.Position, ): Promise | null> { const content = model.getValue() + const version = model.getVersionId() if (!content) return null try { const results = goto_definition( content, + version, position.lineNumber - 1, position.column - 1, ) @@ -197,11 +209,13 @@ export async function provideReferences( position: monaco.Position, ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return [] try { const results = find_references( content, + version, position.lineNumber - 1, position.column - 1, ) @@ -226,6 +240,7 @@ export async function provideSelectionRanges( positions: monaco.Position[], ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return [] try { @@ -234,7 +249,7 @@ export async function provideSelectionRanges( column: pos.column - 1, })) - const results = selection_ranges(content, wasmPositions) + const results = selection_ranges(content, version, wasmPositions) return results.map((ranges) => ranges.map((range) => ({ @@ -280,11 +295,13 @@ export async function provideCompletionItems( position: monaco.Position, ): Promise { const content = model.getValue() + const version = model.getVersionId() if (!content) return { suggestions: [] } try { const items = completion( content, + version, position.lineNumber - 1, position.column - 1, ) diff --git a/playground/src/squawk.tsx b/playground/src/squawk.tsx index d38c6a70..f5dec7e2 100644 --- a/playground/src/squawk.tsx +++ b/playground/src/squawk.tsx @@ -1,17 +1,5 @@ import { useEffect, useState } from "react" -import initWasm, { - dump_cst, - dump_tokens, - lint as lint_, - goto_definition as goto_definition_, - hover as hover_, - find_references as find_references_, - document_symbols as document_symbols_, - code_actions as code_actions_, - inlay_hints as inlay_hints_, - selection_ranges as selection_ranges_, - completion as completion_, -} from "./pkg/squawk_wasm" +import initWasm, { SquawkDatabase } from "./pkg/squawk_wasm" export type TextEdit = { start_line_number: number @@ -40,78 +28,113 @@ export type LintError = { fix?: Fix } -function lint(text: string): Array { - return lint_(text) +let db: SquawkDatabase | null = null + +// We pass in content and version here so that we: +// 1. update the database +// 2. so the react compiler doesn't just cache the functions at their initial +// results. We need them to be dependent on their input. +// +// We can probably do better than this. +function getDb(content: string, version: number): SquawkDatabase { + if (db == null) { + db = new SquawkDatabase() + db.open_file(content) + } + db.update_file(content, version) + + return db } -export function inlay_hints(content: string): InlayHint[] { - return inlay_hints_(content) +function lint(content: string, version: number): Array { + return getDb(content, version).lint() +} + +export function inlay_hints(content: string, version: number): InlayHint[] { + return getDb(content, version).inlay_hints() } export function code_actions( content: string, + version: number, line: number, column: number, ): CodeAction[] | null { - return code_actions_(content, line, column) + return getDb(content, version).code_actions(line, column) } -export function document_symbols(content: string): DocumentSymbol[] { - return document_symbols_(content) +export function document_symbols( + content: string, + version: number, +): DocumentSymbol[] { + return getDb(content, version).document_symbols() } export function hover( content: string, + version: number, line: number, column: number, ): string | null { - return hover_(content, line, column) + return getDb(content, version).hover(line, column) } export function goto_definition( content: string, + version: number, line: number, column: number, ): Array { - return goto_definition_(content, line, column) + return getDb(content, version).goto_definition(line, column) } export function find_references( content: string, + version: number, line: number, column: number, ): LocationRange[] { - return find_references_(content, line, column) + return getDb(content, version).find_references(line, column) } export function selection_ranges( content: string, + version: number, positions: Array<{ line: number; column: number }>, ): SelectionRange[][] { - return selection_ranges_(content, positions) + return getDb(content, version).selection_ranges(positions) } export function completion( content: string, + version: number, line: number, column: number, ): CompletionItem[] { - return completion_(content, line, column) + return getDb(content, version).completion(line, column) +} + +export function dump_cst(content: string, version: number): string { + return getDb(content, version).dump_cst() +} + +export function dump_tokens(content: string, version: number): string { + return getDb(content, version).dump_tokens() } -export function useErrors(text: string) { +export function useErrors(text: string, version: number) { const isReady = useWasmStatus() - return isReady ? lint(text) : [] + return isReady ? lint(text, version) : [] } -export function useDumpCst(text: string): string { +export function useDumpCst(text: string, version: number): string { const isReady = useWasmStatus() - return isReady ? dump_cst(text) : "" + return isReady ? dump_cst(text, version) : "" } -export function useDumpTokens(text: string): string { +export function useDumpTokens(text: string, version: number): string { const isReady = useWasmStatus() - return isReady ? dump_tokens(text) : "" + return isReady ? dump_tokens(text, version) : "" } let isStartingAlready: { promise: Promise; start: number } | null =