mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-04 00:44:19 +01:00
Merge 'Shell command completion' from Pedro Muniz
This PR adds tab completion to the CLI app. To achieve that I implemented a simplified, but similar, Vtable that given a prefix and a line, it outputs rows of candidates. Currently, it only supports auto completion for SQLite keywords, but in the future we can autocomplete database names and columns when `PRAGMA database_list` is implemented. Also, some work will need to be done for detecting whether some syntax would be legal in certain scenarios, but I think this is better delegated to a future PR. Closes #1050
This commit is contained in:
59
Cargo.lock
generated
59
Cargo.lock
generated
@@ -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.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"clap",
|
||||
"comfy-table",
|
||||
"csv",
|
||||
@@ -1620,6 +1615,14 @@ dependencies = [
|
||||
"rustyline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "limbo_completion"
|
||||
version = "0.0.15"
|
||||
dependencies = [
|
||||
"limbo_ext",
|
||||
"mimalloc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "limbo_core"
|
||||
version = "0.0.16"
|
||||
@@ -1639,6 +1642,7 @@ dependencies = [
|
||||
"julian_day_converter",
|
||||
"libc",
|
||||
"libloading",
|
||||
"limbo_completion",
|
||||
"limbo_crypto",
|
||||
"limbo_ext",
|
||||
"limbo_ipaddr",
|
||||
@@ -2748,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",
|
||||
@@ -2760,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]]
|
||||
@@ -2898,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"
|
||||
|
||||
@@ -9,7 +9,8 @@ members = [
|
||||
"bindings/rust",
|
||||
"bindings/wasm",
|
||||
"cli",
|
||||
"core",
|
||||
"core",
|
||||
"extensions/completion",
|
||||
"extensions/core",
|
||||
"extensions/crypto",
|
||||
"extensions/kvstore",
|
||||
@@ -45,6 +46,8 @@ limbo_time = { path = "extensions/time", version = "0.0.16" }
|
||||
limbo_uuid = { path = "extensions/uuid", version = "0.0.16" }
|
||||
limbo_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.0.16" }
|
||||
limbo_ipaddr = { path = "extensions/ipaddr", version = "0.0.16" }
|
||||
limbo_completion = { path = "extensions/completion", version = "0.0.16" }
|
||||
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
|
||||
@@ -24,11 +24,12 @@ clap = { version = "4.5", features = ["derive"] }
|
||||
comfy-table = "7.1.4"
|
||||
dirs = "5.0.1"
|
||||
env_logger = "0.10.1"
|
||||
limbo_core = { path = "../core" }
|
||||
rustyline = "12.0.0"
|
||||
limbo_core = { path = "../core", default-features = true, features = ["completion"]}
|
||||
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"]
|
||||
|
||||
@@ -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<AtomicUsize>,
|
||||
input_buff: String,
|
||||
opts: Settings,
|
||||
pub rl: &'a mut DefaultEditor,
|
||||
pub rl: &'a mut Editor<LimboHelper, DefaultHistory>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
pub fn new(rl: &'a mut rustyline::Editor<LimboHelper, DefaultHistory>) -> anyhow::Result<Self> {
|
||||
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<AtomicUsize> = Arc::clone(&interrupt_count);
|
||||
|
||||
123
cli/helper.rs
Normal file
123
cli/helper.rs
Normal file
@@ -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<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
|
||||
LimboHelper {
|
||||
completer: SqlCompleter::new(conn, io),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for LimboHelper {}
|
||||
|
||||
pub struct SqlCompleter {
|
||||
conn: Rc<Connection>,
|
||||
io: Arc<dyn limbo_core::IO>,
|
||||
}
|
||||
|
||||
impl SqlCompleter {
|
||||
pub fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> 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<char> = 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<char> = 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<char> = 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<Self::Candidate>)> {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
11
cli/main.rs
11
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");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
19
extensions/completion/Cargo.toml
Normal file
19
extensions/completion/Cargo.toml
Normal file
@@ -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 = { workspace = true, 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"]
|
||||
149
extensions/completion/src/keywords.rs
Normal file
149
extensions/completion/src/keywords.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
pub(crate) static KEYWORDS: [&str; 147] = [
|
||||
"ABORT",
|
||||
"ACTION",
|
||||
"ADD",
|
||||
"AFTER",
|
||||
"ALL",
|
||||
"ALTER",
|
||||
"ALWAYS",
|
||||
"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",
|
||||
"EXCLUDE",
|
||||
"EXCLUSIVE",
|
||||
"EXISTS",
|
||||
"EXPLAIN",
|
||||
"FAIL",
|
||||
"FILTER",
|
||||
"FIRST",
|
||||
"FOLLOWING",
|
||||
"FOR",
|
||||
"FOREIGN",
|
||||
"FROM",
|
||||
"FULL",
|
||||
"GENERATED",
|
||||
"GLOB",
|
||||
"GROUP",
|
||||
"GROUPS",
|
||||
"HAVING",
|
||||
"IF",
|
||||
"IGNORE",
|
||||
"IMMEDIATE",
|
||||
"IN",
|
||||
"INDEX",
|
||||
"INDEXED",
|
||||
"INITIALLY",
|
||||
"INNER",
|
||||
"INSERT",
|
||||
"INSTEAD",
|
||||
"INTERSECT",
|
||||
"INTO",
|
||||
"IS",
|
||||
"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",
|
||||
"PLAN",
|
||||
"PRAGMA",
|
||||
"PRECEDING",
|
||||
"PRIMARY",
|
||||
"QUERY",
|
||||
"RAISE",
|
||||
"RANGE",
|
||||
"RECURSIVE",
|
||||
"REFERENCES",
|
||||
"REGEXP",
|
||||
"REINDEX",
|
||||
"RELEASE",
|
||||
"RENAME",
|
||||
"REPLACE",
|
||||
"RESTRICT",
|
||||
"RETURNING",
|
||||
"RIGHT",
|
||||
"ROLLBACK",
|
||||
"ROW",
|
||||
"ROWS",
|
||||
"SAVEPOINT",
|
||||
"SELECT",
|
||||
"SET",
|
||||
"TABLE",
|
||||
"TEMP",
|
||||
"TEMPORARY",
|
||||
"THEN",
|
||||
"TIES",
|
||||
"TO",
|
||||
"TRANSACTION",
|
||||
"TRIGGER",
|
||||
"UNBOUNDED",
|
||||
"UNION",
|
||||
"UNIQUE",
|
||||
"UPDATE",
|
||||
"USING",
|
||||
"VACUUM",
|
||||
"VALUES",
|
||||
"VIEW",
|
||||
"VIRTUAL",
|
||||
"WHEN",
|
||||
"WHERE",
|
||||
"WINDOW",
|
||||
"WITH",
|
||||
"WITHOUT",
|
||||
];
|
||||
217
extensions/completion/src/lib.rs
Normal file
217
extensions/completion/src/lib.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
//! Reference for implementation
|
||||
//! https://github.com/sqlite/sqlite/blob/a80089c5167856f0aadc9c878bd65843df724c06/ext/misc/completion.c
|
||||
|
||||
mod keywords;
|
||||
|
||||
use keywords::KEYWORDS;
|
||||
use limbo_ext::{register_extension, 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]
|
||||
Keywords = 1,
|
||||
// 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,
|
||||
}
|
||||
|
||||
impl Into<i64> for CompletionPhase {
|
||||
fn into(self) -> i64 {
|
||||
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,
|
||||
Eof => 11,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual table that generates candidate completions
|
||||
#[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 create_schema(_args: &[Value]) -> String {
|
||||
"CREATE TABLE completion(
|
||||
candidate TEXT,
|
||||
prefix TEXT HIDDEN,
|
||||
wholeline TEXT HIDDEN,
|
||||
phase INT HIDDEN
|
||||
)"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn open(&self) -> Result<Self::VCursor, Self::Error> {
|
||||
Ok(CompletionCursor::default())
|
||||
}
|
||||
|
||||
fn column(cursor: &Self::VCursor, idx: u32) -> Result<Value, ResultCode> {
|
||||
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, args: &[Value]) -> ResultCode {
|
||||
if args.len() == 0 || args.len() > 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor for iterating over the completions
|
||||
#[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 {
|
||||
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 {
|
||||
self.rowid += 1;
|
||||
|
||||
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::Eof;
|
||||
} else {
|
||||
self.curr_row.clear();
|
||||
self.curr_row.push_str(KEYWORDS[self.inter_phase_counter]);
|
||||
self.inter_phase_counter += 1;
|
||||
}
|
||||
}
|
||||
// TODO implement this when db conn is available
|
||||
// CompletionPhase::Databases => {
|
||||
//
|
||||
// // 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.to_lowercase() == self.curr_row.to_lowercase()[..self.prefix.len()]
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if self.phase == CompletionPhase::Eof {
|
||||
return ResultCode::EOF;
|
||||
}
|
||||
ResultCode::OK
|
||||
}
|
||||
|
||||
fn eof(&self) -> bool {
|
||||
self.phase == CompletionPhase::Eof
|
||||
}
|
||||
|
||||
fn column(&self, idx: u32) -> Result<Value, Self::Error> {
|
||||
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 {
|
||||
self.rowid
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
Reference in New Issue
Block a user