From ca574651d99bb85be4507cf340f0b227e807314d Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 21 Feb 2025 01:45:45 -0300 Subject: [PATCH 1/5] wip --- Cargo.lock | 8 ++ Cargo.toml | 3 +- extensions/completion/Cargo.toml | 19 +++ extensions/completion/src/keywords.rs | 138 ++++++++++++++++++++++ extensions/completion/src/lib.rs | 164 ++++++++++++++++++++++++++ 5 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 extensions/completion/Cargo.toml create mode 100644 extensions/completion/src/keywords.rs create mode 100644 extensions/completion/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 28046068d..5ac0b6cb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1620,6 +1620,14 @@ dependencies = [ "rustyline", ] +[[package]] +name = "limbo_completion" +version = "0.0.15" +dependencies = [ + "limbo_ext", + "mimalloc", +] + [[package]] name = "limbo_core" version = "0.0.15" diff --git a/Cargo.toml b/Cargo.toml index f555f726f..6bfa52177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ members = [ "bindings/rust", "bindings/wasm", "cli", - "core", + "core", + "extensions/completion", "extensions/core", "extensions/crypto", "extensions/kvstore", diff --git a/extensions/completion/Cargo.toml b/extensions/completion/Cargo.toml new file mode 100644 index 000000000..32b350871 --- /dev/null +++ b/extensions/completion/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors.workspace = true +edition.workspace = true +license.workspace = true +name = "limbo_completion" +repository.workspace = true +version.workspace = true + +[dependencies] +limbo_ext = { path = "../core", features = ["static"] } + +[target.'cfg(not(target_family = "wasm"))'.dependencies] +mimalloc = { version = "0.1", default-features = false } + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +static = ["limbo_ext/static"] diff --git a/extensions/completion/src/keywords.rs b/extensions/completion/src/keywords.rs new file mode 100644 index 000000000..c92207464 --- /dev/null +++ b/extensions/completion/src/keywords.rs @@ -0,0 +1,138 @@ +pub(crate) static KEYWORDS: [&str; 136] = [ + "ABORT", + "ACTION", + "ADD", + "AFTER", + "ALL", + "ALTER", + "ANALYZE", + "AND", + "AS", + "ASC", + "ATTACH", + "AUTOINCREMENT", + "BEFORE", + "BEGIN", + "BETWEEN", + "BY", + "CASCADE", + "CASE", + "CAST", + "CHECK", + "COLLATE", + "COLUMN", + "COMMIT", + "CONFLICT", + "CONSTRAINT", + "CREATE", + "CROSS", + "CURRENT", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "DATABASE", + "DEFAULT", + "DEFERRABLE", + "DEFERRED", + "DELETE", + "DESC", + "DETACH", + "DISTINCT", + "DO", + "DROP", + "EACH", + "ELSE", + "END", + "ESCAPE", + "EXCEPT", + "EXCLUSIVE", + "EXISTS", + "EXPLAIN", + "FAIL", + "FILTER", + "FOLLOWING", + "FOR", + "FOREIGN", + "FROM", + "FULL", + "GLO", + "GROUP", + "HAVING", + "IF", + "IGNORE", + "IMMEDIATE", + "IN", + "INDEX", + "INDEXED", + "INITIALLY", + "INNER", + "INSERT", + "INSTEAD", + "INTERSECT", + "INTO", + "IS", + "ISNULL", + "JOIN", + "KEY", + "LEFT", + "LIKE", + "LIMIT", + "MATCH", + "NATURAL", + "NO", + "NOT", + "NOTHING", + "NOTNULL", + "NULL", + "OF", + "OFFSET", + "ON", + "OR", + "ORDER", + "OUTER", + "OVER", + "PARTITION", + "PLAN", + "PRAGMA", + "PRECEDING", + "PRIMARY", + "QUERY", + "RAISE", + "RANGE", + "RECURSIVE", + "REFERENCES", + "REGEXP", + "REINDEX", + "RELEASE", + "RENAME", + "REPLACE", + "RESTRICT", + "RIGHT", + "ROLLBACK", + "ROW", + "ROWS", + "SAVEPOINT", + "SELECT", + "SET", + "TABLE", + "TEMP", + "TEMPORARY", + "THEN", + "TO", + "TRANSACTION", + "TRIGGER", + "UNBOUNDED", + "UNION", + "UNIQUE", + "UPDATE", + "USING", + "VACUUM", + "VALUES", + "VIEW", + "VIRTUAL", + "WHEN", + "WHERE", + "WINDOW", + "WITH", + "WITHOUT", +]; diff --git a/extensions/completion/src/lib.rs b/extensions/completion/src/lib.rs new file mode 100644 index 000000000..df23c9bc1 --- /dev/null +++ b/extensions/completion/src/lib.rs @@ -0,0 +1,164 @@ +mod keywords; + +use keywords::KEYWORDS; +use limbo_ext::{ + register_extension, ExtensionApi, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, Value, +}; + +register_extension! { + vtabs: { CompletionVTab } +} + +macro_rules! try_option { + ($expr:expr, $err:expr) => { + match $expr { + Some(val) => val, + None => return $err, + } + }; +} + +#[derive(Debug, Default, PartialEq, Clone)] +enum CompletionPhase { + #[default] + FirstPhase = 0, + Keywords = 1, + Pragmas = 2, + Functions = 3, + Collations = 4, + Indexes = 5, + Triggers = 6, + Databases = 7, + Tables = 8, // Also VIEWs and TRIGGERs + Columns = 9, + Modules = 10, + Eof = 11, +} + +impl Into for CompletionPhase { + fn into(self) -> i64 { + use self::CompletionPhase::*; + match self { + FirstPhase => 0, + Keywords => 1, + Pragmas => 2, + Functions => 3, + Collations => 4, + Indexes => 5, + Triggers => 6, + Databases => 7, + Tables => 8, + Columns => 9, + Modules => 10, + Eof => 11, + } + } +} + +/// A virtual table that generates a sequence of integers +#[derive(Debug, VTabModuleDerive)] +struct CompletionVTab {} + +impl VTabModule for CompletionVTab { + type VCursor = CompletionCursor; + const NAME: &'static str = "completion"; + + fn connect(api: &ExtensionApi) -> ResultCode { + // Create table schema + let sql = "CREATE TABLE completion( + candidate TEXT, + prefix TEXT HIDDEN, + wholeline TEXT HIDDEN, + phase INT HIDDEN + )"; + api.declare_virtual_table(Self::NAME, sql) + } + + fn open() -> Self::VCursor { + CompletionCursor::default() + } + + fn column(cursor: &Self::VCursor, idx: u32) -> Value { + cursor.column(idx) + } + + fn next(cursor: &mut Self::VCursor) -> ResultCode { + cursor.next() + } + + fn eof(cursor: &Self::VCursor) -> bool { + cursor.eof() + } + + fn filter(cursor: &mut Self::VCursor, arg_count: i32, args: &[Value]) -> ResultCode { + todo!() + } +} + +/// The cursor for iterating over the generated sequence +#[derive(Debug, Default)] +struct CompletionCursor { + line: String, + prefix: String, + curr_row: String, + rowid: i64, + phase: CompletionPhase, + inter_phase_counter: usize, + // stmt: Statement + // conn: Connection +} + +impl CompletionCursor {} + +impl VTabCursor for CompletionCursor { + type Error = ResultCode; + + fn next(&mut self) -> ResultCode { + let mut curr_col = -1 as isize; + self.rowid += 1; + let mut next_phase = CompletionPhase::FirstPhase; + + while self.phase != CompletionPhase::Eof { + match self.phase { + CompletionPhase::Keywords => { + if self.inter_phase_counter >= KEYWORDS.len() { + self.curr_row.clear(); + self.phase = CompletionPhase::Databases; + } else { + self.inter_phase_counter += 1; + self.curr_row.push_str(KEYWORDS[self.inter_phase_counter]); + } + } + CompletionPhase::Databases => { + // TODO implement this when + // self.stmt = self.conn.prepare("PRAGMA database_list") + curr_col = 1; + next_phase = CompletionPhase::Tables; + } + _ => (), + } + } + ResultCode::OK + } + + fn eof(&self) -> bool { + self.phase == CompletionPhase::Eof + } + + fn column(&self, idx: u32) -> Value { + match idx { + 0 => Value::from_text(self.curr_row.clone()), // COMPLETION_COLUMN_CANDIDATE + 1 => Value::from_text(self.prefix.clone()), // COMPLETION_COLUMN_PREFIX + 2 => Value::from_text(self.line.clone()), // COMPLETION_COLUMN_WHOLELINE + 3 => Value::from_integer(self.phase.clone().into()), // COMPLETION_COLUMN_PHASE + _ => Value::null(), + } + } + + fn rowid(&self) -> i64 { + self.rowid + } +} + +#[cfg(test)] +mod tests {} From 99d979eb8041da3c674272eb209ff7a7ceb7466b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Sat, 22 Feb 2025 02:21:27 -0300 Subject: [PATCH 2/5] first version of vtable with keyword autocomplete --- Cargo.lock | 1 + Cargo.toml | 1 + cli/Cargo.toml | 2 +- core/Cargo.toml | 2 + core/ext/mod.rs | 4 ++ extensions/completion/Cargo.toml | 2 +- extensions/completion/src/keywords.rs | 15 ++++- extensions/completion/src/lib.rs | 80 ++++++++++++++++++++++----- 8 files changed, 89 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ac0b6cb8..c79c0f1d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1647,6 +1647,7 @@ dependencies = [ "julian_day_converter", "libc", "libloading", + "limbo_completion", "limbo_crypto", "limbo_ext", "limbo_ipaddr", diff --git a/Cargo.toml b/Cargo.toml index 6bfa52177..12e668754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ limbo_regexp = { path = "extensions/regexp", version = "0.0.15" } limbo_series = { path = "extensions/series", version = "0.0.15" } limbo_time = { path = "extensions/time", version = "0.0.15" } limbo_uuid = { path = "extensions/uuid", version = "0.0.15" } +limbo_completion = { path = "extensions/completion", version = "0.0.15" } limbo_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.0.15" } limbo_ipaddr = { path = "extensions/ipaddr", version = "0.0.15" } # Config for 'cargo dist' diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 784168f8c..6e9f4301a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -24,7 +24,7 @@ clap = { version = "4.5", features = ["derive"] } comfy-table = "7.1.4" dirs = "5.0.1" env_logger = "0.10.1" -limbo_core = { path = "../core" } +limbo_core = { path = "../core", default-features = true, features = ["completion"]} rustyline = "12.0.0" ctrlc = "3.4.4" csv = "1.3.1" diff --git a/core/Cargo.toml b/core/Cargo.toml index 3bc30f5a4..c72bbc5c8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,6 +25,7 @@ time = ["limbo_time/static"] crypto = ["limbo_crypto/static"] series = ["limbo_series/static"] ipaddr = ["limbo_ipaddr/static"] +completion = ["limbo_completion/static"] [target.'cfg(target_os = "linux")'.dependencies] io-uring = { version = "0.6.1", optional = true } @@ -64,6 +65,7 @@ limbo_time = { workspace = true, optional = true, features = ["static"] } limbo_crypto = { workspace = true, optional = true, features = ["static"] } limbo_series = { workspace = true, optional = true, features = ["static"] } limbo_ipaddr = { workspace = true, optional = true, features = ["static"] } +limbo_completion = { workspace = true, optional = true, features = ["static"] } miette = "7.4.0" strum = "0.26" parking_lot = "0.12.3" diff --git a/core/ext/mod.rs b/core/ext/mod.rs index a88468f3b..961cf5a85 100644 --- a/core/ext/mod.rs +++ b/core/ext/mod.rs @@ -154,6 +154,10 @@ impl Database { if unsafe { !limbo_ipaddr::register_extension_static(&ext_api).is_ok() } { return Err("Failed to register ipaddr extension".to_string()); } + #[cfg(feature = "completion")] + if unsafe { !limbo_completion::register_extension_static(&ext_api).is_ok() } { + return Err("Failed to register completion extension".to_string()); + } Ok(()) } } diff --git a/extensions/completion/Cargo.toml b/extensions/completion/Cargo.toml index 32b350871..dace3cc8a 100644 --- a/extensions/completion/Cargo.toml +++ b/extensions/completion/Cargo.toml @@ -7,7 +7,7 @@ repository.workspace = true version.workspace = true [dependencies] -limbo_ext = { path = "../core", features = ["static"] } +limbo_ext = { workspace = true, features = ["static"] } [target.'cfg(not(target_family = "wasm"))'.dependencies] mimalloc = { version = "0.1", default-features = false } diff --git a/extensions/completion/src/keywords.rs b/extensions/completion/src/keywords.rs index c92207464..f8eff0485 100644 --- a/extensions/completion/src/keywords.rs +++ b/extensions/completion/src/keywords.rs @@ -1,10 +1,11 @@ -pub(crate) static KEYWORDS: [&str; 136] = [ +pub(crate) static KEYWORDS: [&str; 147] = [ "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", + "ALWAYS", "ANALYZE", "AND", "AS", @@ -45,18 +46,22 @@ pub(crate) static KEYWORDS: [&str; 136] = [ "END", "ESCAPE", "EXCEPT", + "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FILTER", + "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", - "GLO", + "GENERATED", + "GLOB", "GROUP", + "GROUPS", "HAVING", "IF", "IGNORE", @@ -74,21 +79,25 @@ pub(crate) static KEYWORDS: [&str; 136] = [ "ISNULL", "JOIN", "KEY", + "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", + "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", + "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", + "OTHERS", "OUTER", "OVER", "PARTITION", @@ -107,6 +116,7 @@ pub(crate) static KEYWORDS: [&str; 136] = [ "RENAME", "REPLACE", "RESTRICT", + "RETURNING", "RIGHT", "ROLLBACK", "ROW", @@ -118,6 +128,7 @@ pub(crate) static KEYWORDS: [&str; 136] = [ "TEMP", "TEMPORARY", "THEN", + "TIES", "TO", "TRANSACTION", "TRIGGER", diff --git a/extensions/completion/src/lib.rs b/extensions/completion/src/lib.rs index df23c9bc1..66bf35499 100644 --- a/extensions/completion/src/lib.rs +++ b/extensions/completion/src/lib.rs @@ -1,3 +1,6 @@ +//! Reference for implementation +//! https://github.com/sqlite/sqlite/blob/a80089c5167856f0aadc9c878bd65843df724c06/ext/misc/completion.c + mod keywords; use keywords::KEYWORDS; @@ -21,7 +24,6 @@ macro_rules! try_option { #[derive(Debug, Default, PartialEq, Clone)] enum CompletionPhase { #[default] - FirstPhase = 0, Keywords = 1, Pragmas = 2, Functions = 3, @@ -39,7 +41,6 @@ impl Into for CompletionPhase { fn into(self) -> i64 { use self::CompletionPhase::*; match self { - FirstPhase => 0, Keywords => 1, Pragmas => 2, Functions => 3, @@ -91,7 +92,37 @@ impl VTabModule for CompletionVTab { } fn filter(cursor: &mut Self::VCursor, arg_count: i32, args: &[Value]) -> ResultCode { - todo!() + if arg_count == 0 || arg_count > 2 { + return ResultCode::InvalidArgs; + } + cursor.reset(); + let prefix = try_option!(args[0].to_text(), ResultCode::InvalidArgs); + + let wholeline = args.get(1).map(|v| v.to_text().unwrap_or("")).unwrap_or(""); + + cursor.line = wholeline.to_string(); + cursor.prefix = prefix.to_string(); + + // Currently best index is not implemented so the correct arg parsing is not done here + if !cursor.line.is_empty() && cursor.prefix.is_empty() { + let mut i = cursor.line.len(); + while let Some(ch) = cursor.line.chars().next() { + if i > 0 && (ch.is_alphanumeric() || ch == '_') { + i -= 1; + } else { + break; + } + } + if cursor.line.len() - i > 0 { + // TODO see if need to inclusive range + cursor.prefix = cursor.line[..i].to_string(); + } + } + + cursor.rowid = 0; + cursor.phase = CompletionPhase::Keywords; + + Self::next(cursor) } } @@ -108,35 +139,56 @@ struct CompletionCursor { // conn: Connection } -impl CompletionCursor {} +impl CompletionCursor { + fn reset(&mut self) { + self.line.clear(); + self.prefix.clear(); + self.inter_phase_counter = 0; + } +} impl VTabCursor for CompletionCursor { type Error = ResultCode; fn next(&mut self) -> ResultCode { - let mut curr_col = -1 as isize; self.rowid += 1; - let mut next_phase = CompletionPhase::FirstPhase; while self.phase != CompletionPhase::Eof { + // dbg!(&self.phase, &self.prefix, &self.curr_row); match self.phase { CompletionPhase::Keywords => { if self.inter_phase_counter >= KEYWORDS.len() { self.curr_row.clear(); - self.phase = CompletionPhase::Databases; + self.phase = CompletionPhase::Eof; } else { - self.inter_phase_counter += 1; + self.curr_row.clear(); self.curr_row.push_str(KEYWORDS[self.inter_phase_counter]); + self.inter_phase_counter += 1; } } - CompletionPhase::Databases => { - // TODO implement this when - // self.stmt = self.conn.prepare("PRAGMA database_list") - curr_col = 1; - next_phase = CompletionPhase::Tables; + + // CompletionPhase::Databases => { + // // TODO implement this when db conn is available + // // self.stmt = self.conn.prepare("PRAGMA database_list") + // curr_col = 1; + // next_phase = CompletionPhase::Tables; + // self.phase = CompletionPhase::Eof; // for now skip other phases + // } + _ => { + return ResultCode::EOF; } - _ => (), } + if self.prefix.is_empty() { + break; + } + if self.prefix.len() <= self.curr_row.len() + && self.prefix == self.curr_row.to_lowercase()[..self.prefix.len()] + { + break; + } + } + if self.phase == CompletionPhase::Eof { + return ResultCode::EOF; } ResultCode::OK } From 04d7d8ab87a7cee5f55755b3955568589f4a9921 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Sat, 22 Feb 2025 14:13:47 -0300 Subject: [PATCH 3/5] autocomplete working --- Cargo.lock | 50 ++++++------- cli/Cargo.toml | 3 +- cli/app.rs | 9 ++- cli/helper.rs | 123 +++++++++++++++++++++++++++++++ cli/main.rs | 11 ++- extensions/completion/src/lib.rs | 43 +++++------ 6 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 cli/helper.rs diff --git a/Cargo.lock b/Cargo.lock index c79c0f1d1..45093903d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,13 +411,11 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" -version = "4.5.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", - "str-buf", - "winapi", ] [[package]] @@ -824,13 +822,9 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "fallible-iterator" @@ -858,13 +852,13 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" -version = "3.0.13" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" dependencies = [ "cfg-if", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1609,6 +1603,7 @@ name = "limbo_cli" version = "0.0.15" dependencies = [ "anyhow", + "cfg-if", "clap", "comfy-table", "csv", @@ -2757,9 +2752,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustyline" -version = "12.0.0" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -2769,13 +2764,24 @@ dependencies = [ "libc", "log", "memchr", - "nix 0.26.4", + "nix 0.29.0", "radix_trie", - "scopeguard", + "rustyline-derive", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width 0.2.0", "utf8parse", - "winapi", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustyline-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] @@ -2907,12 +2913,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "str_stack" version = "0.1.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6e9f4301a..965bae003 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,10 +25,11 @@ comfy-table = "7.1.4" dirs = "5.0.1" env_logger = "0.10.1" limbo_core = { path = "../core", default-features = true, features = ["completion"]} -rustyline = "12.0.0" +rustyline = {version = "15.0.0", default-features = true, features = ["derive"]} ctrlc = "3.4.4" csv = "1.3.1" miette = { version = "7.4.0", features = ["fancy"] } +cfg-if = "1.0.0" [features] default = ["io_uring"] diff --git a/cli/app.rs b/cli/app.rs index 9925585c0..bbc49ac46 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -1,4 +1,5 @@ use crate::{ + helper::LimboHelper, import::{ImportFile, IMPORT_HELP}, input::{get_io, get_writer, DbLocation, Io, OutputMode, Settings, HELP_MSG}, opcodes_dictionary::OPCODE_DESCRIPTIONS, @@ -7,7 +8,7 @@ use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Row, Table use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult}; use clap::{Parser, ValueEnum}; -use rustyline::DefaultEditor; +use rustyline::{history::DefaultHistory, Editor}; use std::{ fmt, io::{self, Write}, @@ -167,7 +168,7 @@ pub struct Limbo<'a> { pub interrupt_count: Arc, input_buff: String, opts: Settings, - pub rl: &'a mut DefaultEditor, + pub rl: &'a mut Editor, } macro_rules! query_internal { @@ -196,7 +197,7 @@ macro_rules! query_internal { } impl<'a> Limbo<'a> { - pub fn new(rl: &'a mut rustyline::DefaultEditor) -> anyhow::Result { + pub fn new(rl: &'a mut rustyline::Editor) -> anyhow::Result { let opts = Opts::parse(); let db_file = opts .database @@ -211,6 +212,8 @@ impl<'a> Limbo<'a> { }; let db = Database::open_file(io.clone(), &db_file)?; let conn = db.connect(); + let h = LimboHelper::new(conn.clone(), io.clone()); + rl.set_helper(Some(h)); let interrupt_count = Arc::new(AtomicUsize::new(0)); { let interrupt_count: Arc = Arc::clone(&interrupt_count); diff --git a/cli/helper.rs b/cli/helper.rs new file mode 100644 index 000000000..3d809741f --- /dev/null +++ b/cli/helper.rs @@ -0,0 +1,123 @@ +use std::rc::Rc; +use std::sync::Arc; + +use limbo_core::{Connection, StepResult}; +use rustyline::completion::{extract_word, Completer, Pair}; +use rustyline::highlight::Highlighter; +use rustyline::{Completer, Helper, Hinter, Validator}; + +macro_rules! try_result { + ($expr:expr, $err:expr) => { + match $expr { + Ok(val) => val, + Err(_) => return Ok($err), + } + }; +} + +#[derive(Helper, Completer, Hinter, Validator)] +pub struct LimboHelper { + #[rustyline(Completer)] + completer: SqlCompleter, +} + +impl LimboHelper { + pub fn new(conn: Rc, io: Arc) -> Self { + LimboHelper { + completer: SqlCompleter::new(conn, io), + } + } +} + +impl Highlighter for LimboHelper {} + +pub struct SqlCompleter { + conn: Rc, + io: Arc, +} + +impl SqlCompleter { + pub fn new(conn: Rc, io: Arc) -> Self { + Self { conn, io } + } +} + +// Got this from the FilenameCompleter. +// TODO have to see what chars break words in Sqlite +cfg_if::cfg_if! { + if #[cfg(unix)] { + // rl_basic_word_break_characters, rl_completer_word_break_characters + const fn default_break_chars(c : char) -> bool { + matches!(c, ' ' | '\t' | '\n' | '"' | '\\' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | + '{' | '(' | '\0') + } + const ESCAPE_CHAR: Option = Some('\\'); + // In double quotes, not all break_chars need to be escaped + // https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html + #[allow(dead_code)] + const fn double_quotes_special_chars(c: char) -> bool { matches!(c, '"' | '$' | '\\' | '`') } + } else if #[cfg(windows)] { + // Remove \ to make file completion works on windows + const fn default_break_chars(c: char) -> bool { + matches!(c, ' ' | '\t' | '\n' | '"' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' | + '(' | '\0') + } + const ESCAPE_CHAR: Option = None; + #[allow(dead_code)] + const fn double_quotes_special_chars(c: char) -> bool { c == '"' } // TODO Validate: only '"' ? + } else if #[cfg(target_arch = "wasm32")] { + const fn default_break_chars(c: char) -> bool { false } + const ESCAPE_CHAR: Option = None; + #[allow(dead_code)] + const fn double_quotes_special_chars(c: char) -> bool { false } + } +} + +impl Completer for SqlCompleter { + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &rustyline::Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + // TODO: have to differentiate words if they are enclosed in single of double quotes + let (prefix_pos, prefix) = extract_word(line, pos, ESCAPE_CHAR, default_break_chars); + let mut candidates = Vec::new(); + + let query = try_result!( + self.conn.query(format!( + "SELECT candidate FROM completion('{prefix}', '{line}') ORDER BY 1;" + )), + (prefix_pos, candidates) + ); + + if let Some(mut rows) = query { + loop { + match try_result!(rows.step(), (prefix_pos, candidates)) { + StepResult::Row => { + let row = rows.row().unwrap(); + let completion: &str = + try_result!(row.get::<&str>(0), (prefix_pos, candidates)); + let pair = Pair { + display: completion.to_string(), + replacement: completion.to_string(), + }; + candidates.push(pair); + } + StepResult::IO => { + try_result!(self.io.run_once(), (prefix_pos, candidates)); + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => { + break; + } + } + } + } + + Ok((prefix_pos, candidates)) + } +} diff --git a/cli/main.rs b/cli/main.rs index e59008a8b..d118c2c77 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -1,15 +1,22 @@ #![allow(clippy::arc_with_non_send_sync)] mod app; +mod helper; mod import; mod input; mod opcodes_dictionary; -use rustyline::{error::ReadlineError, DefaultEditor}; +use rustyline::{error::ReadlineError, Config, Editor}; use std::sync::atomic::Ordering; +fn rustyline_config() -> Config { + Config::builder() + .completion_type(rustyline::CompletionType::List) + .build() +} + fn main() -> anyhow::Result<()> { env_logger::init(); - let mut rl = DefaultEditor::new()?; + let mut rl = Editor::with_config(rustyline_config())?; let mut app = app::Limbo::new(&mut rl)?; let home = dirs::home_dir().expect("Could not determine home directory"); let history_file = home.join(".limbo_history"); diff --git a/extensions/completion/src/lib.rs b/extensions/completion/src/lib.rs index 66bf35499..80889c91f 100644 --- a/extensions/completion/src/lib.rs +++ b/extensions/completion/src/lib.rs @@ -25,15 +25,16 @@ macro_rules! try_option { enum CompletionPhase { #[default] Keywords = 1, - Pragmas = 2, - Functions = 3, - Collations = 4, - Indexes = 5, - Triggers = 6, - Databases = 7, - Tables = 8, // Also VIEWs and TRIGGERs - Columns = 9, - Modules = 10, + // TODO other options now implemented for now + // Pragmas = 2, + // Functions = 3, + // Collations = 4, + // Indexes = 5, + // Triggers = 6, + // Databases = 7, + // Tables = 8, // Also VIEWs and TRIGGERs + // Columns = 9, + // Modules = 10, Eof = 11, } @@ -42,15 +43,15 @@ impl Into for CompletionPhase { use self::CompletionPhase::*; match self { Keywords => 1, - Pragmas => 2, - Functions => 3, - Collations => 4, - Indexes => 5, - Triggers => 6, - Databases => 7, - Tables => 8, - Columns => 9, - Modules => 10, + // Pragmas => 2, + // Functions => 3, + // Collations => 4, + // Indexes => 5, + // Triggers => 6, + // Databases => 7, + // Tables => 8, + // Columns => 9, + // Modules => 10, Eof => 11, } } @@ -166,9 +167,9 @@ impl VTabCursor for CompletionCursor { self.inter_phase_counter += 1; } } - + // TODO implement this when db conn is available // CompletionPhase::Databases => { - // // TODO implement this when db conn is available + // // // self.stmt = self.conn.prepare("PRAGMA database_list") // curr_col = 1; // next_phase = CompletionPhase::Tables; @@ -182,7 +183,7 @@ impl VTabCursor for CompletionCursor { break; } if self.prefix.len() <= self.curr_row.len() - && self.prefix == self.curr_row.to_lowercase()[..self.prefix.len()] + && self.prefix.to_lowercase() == self.curr_row.to_lowercase()[..self.prefix.len()] { break; } From 168a2deffbb3de23c0752117c9fa836ce3aa6995 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 25 Feb 2025 15:32:23 -0300 Subject: [PATCH 4/5] merging changes from extension cleanup PR --- extensions/completion/src/lib.rs | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/completion/src/lib.rs b/extensions/completion/src/lib.rs index 80889c91f..d6cdbb9d7 100644 --- a/extensions/completion/src/lib.rs +++ b/extensions/completion/src/lib.rs @@ -4,9 +4,7 @@ mod keywords; use keywords::KEYWORDS; -use limbo_ext::{ - register_extension, ExtensionApi, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, Value, -}; +use limbo_ext::{register_extension, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, Value}; register_extension! { vtabs: { CompletionVTab } @@ -58,29 +56,30 @@ impl Into for CompletionPhase { } /// A virtual table that generates a sequence of integers -#[derive(Debug, VTabModuleDerive)] +#[derive(Debug, Default, VTabModuleDerive)] struct CompletionVTab {} impl VTabModule for CompletionVTab { type VCursor = CompletionCursor; const NAME: &'static str = "completion"; + const VTAB_KIND: limbo_ext::VTabKind = limbo_ext::VTabKind::TableValuedFunction; + type Error = ResultCode; - fn connect(api: &ExtensionApi) -> ResultCode { - // Create table schema - let sql = "CREATE TABLE completion( + fn create_schema(_args: &[Value]) -> String { + "CREATE TABLE completion( candidate TEXT, prefix TEXT HIDDEN, wholeline TEXT HIDDEN, phase INT HIDDEN - )"; - api.declare_virtual_table(Self::NAME, sql) + )" + .to_string() } - fn open() -> Self::VCursor { - CompletionCursor::default() + fn open(&self) -> Result { + Ok(CompletionCursor::default()) } - fn column(cursor: &Self::VCursor, idx: u32) -> Value { + fn column(cursor: &Self::VCursor, idx: u32) -> Result { cursor.column(idx) } @@ -92,8 +91,8 @@ impl VTabModule for CompletionVTab { cursor.eof() } - fn filter(cursor: &mut Self::VCursor, arg_count: i32, args: &[Value]) -> ResultCode { - if arg_count == 0 || arg_count > 2 { + fn filter(cursor: &mut Self::VCursor, args: &[Value]) -> ResultCode { + if args.len() == 0 || args.len() > 2 { return ResultCode::InvalidArgs; } cursor.reset(); @@ -198,14 +197,15 @@ impl VTabCursor for CompletionCursor { self.phase == CompletionPhase::Eof } - fn column(&self, idx: u32) -> Value { - match idx { + fn column(&self, idx: u32) -> Result { + let val = match idx { 0 => Value::from_text(self.curr_row.clone()), // COMPLETION_COLUMN_CANDIDATE 1 => Value::from_text(self.prefix.clone()), // COMPLETION_COLUMN_PREFIX 2 => Value::from_text(self.line.clone()), // COMPLETION_COLUMN_WHOLELINE 3 => Value::from_integer(self.phase.clone().into()), // COMPLETION_COLUMN_PHASE _ => Value::null(), - } + }; + Ok(val) } fn rowid(&self) -> i64 { From 5d5e6be7ddd5dec11d60cc638289bf9068e9cbe3 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 4 Mar 2025 14:44:11 -0300 Subject: [PATCH 5/5] cleanup comments --- extensions/completion/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/completion/src/lib.rs b/extensions/completion/src/lib.rs index d6cdbb9d7..09b09c479 100644 --- a/extensions/completion/src/lib.rs +++ b/extensions/completion/src/lib.rs @@ -55,7 +55,7 @@ impl Into for CompletionPhase { } } -/// A virtual table that generates a sequence of integers +/// A virtual table that generates candidate completions #[derive(Debug, Default, VTabModuleDerive)] struct CompletionVTab {} @@ -126,7 +126,7 @@ impl VTabModule for CompletionVTab { } } -/// The cursor for iterating over the generated sequence +/// The cursor for iterating over the completions #[derive(Debug, Default)] struct CompletionCursor { line: String,