mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-01 23:44:19 +01:00
Merge 'Dot command completion' from Pedro Muniz
Closes #1201.  Reviewed-by: Preston Thorpe (@PThorpe92) Closes #1253
This commit is contained in:
23
Cargo.lock
generated
23
Cargo.lock
generated
@@ -397,6 +397,18 @@ dependencies = [
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_lex",
|
||||
"is_executable",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
@@ -1400,6 +1412,15 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_executable"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@@ -1644,6 +1665,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"comfy-table",
|
||||
"csv",
|
||||
"ctrlc",
|
||||
@@ -1653,6 +1675,7 @@ dependencies = [
|
||||
"miette",
|
||||
"nu-ansi-term 0.50.1",
|
||||
"rustyline",
|
||||
"shlex",
|
||||
"syntect",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -20,24 +20,26 @@ path = "main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
cfg-if = "1.0.0"
|
||||
clap = { version = "4.5.31", features = ["derive"] }
|
||||
clap_complete = { version = "=4.5.47", features = ["unstable-dynamic"] }
|
||||
comfy-table = "7.1.4"
|
||||
csv = "1.3.1"
|
||||
ctrlc = "3.4.4"
|
||||
dirs = "5.0.1"
|
||||
env_logger = "0.10.1"
|
||||
limbo_core = { path = "../core", default-features = true, features = [
|
||||
"completion",
|
||||
] }
|
||||
miette = { version = "7.4.0", features = ["fancy"] }
|
||||
nu-ansi-term = "0.50.1"
|
||||
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"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing = "0.1.41"
|
||||
shlex = "1.3.0"
|
||||
syntect = "5.2.0"
|
||||
nu-ansi-term = "0.50.1"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
|
||||
|
||||
[features]
|
||||
@@ -46,4 +48,3 @@ io_uring = ["limbo_core/io_uring"]
|
||||
|
||||
[build-dependencies]
|
||||
syntect = "5.2.0"
|
||||
|
||||
|
||||
@@ -467,7 +467,8 @@ impl<'a> Limbo<'a> {
|
||||
}
|
||||
match CommandParser::try_parse_from(args) {
|
||||
Err(err) => {
|
||||
let _ = self.write_fmt(format_args!("{err}"));
|
||||
// Let clap print with Styled Colors instead
|
||||
let _ = err.print();
|
||||
}
|
||||
Ok(cmd) => match cmd.command {
|
||||
Command::Exit(args) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use clap::{Args, ValueEnum};
|
||||
use clap_complete::{ArgValueCompleter, CompletionCandidate, PathCompleter};
|
||||
|
||||
use crate::input::OutputMode;
|
||||
use crate::{input::OutputMode, opcodes_dictionary::OPCODE_DESCRIPTIONS};
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ExitArgs {
|
||||
@@ -12,13 +13,17 @@ pub struct ExitArgs {
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct OpenArgs {
|
||||
/// Path to open database
|
||||
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
|
||||
pub path: String,
|
||||
// TODO see how to have this completed with the output of List Vfs function
|
||||
// Currently not possible to pass arbitrary
|
||||
/// Name of VFS
|
||||
pub vfs_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct SchemaArgs {
|
||||
// TODO depends on PRAGMA table_list for completions
|
||||
/// Table name to visualize schema
|
||||
pub table_name: Option<String>,
|
||||
}
|
||||
@@ -26,6 +31,7 @@ pub struct SchemaArgs {
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct SetOutputArgs {
|
||||
/// File path to send output to
|
||||
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
@@ -35,15 +41,40 @@ pub struct OutputModeArgs {
|
||||
pub mode: OutputMode,
|
||||
}
|
||||
|
||||
fn opcodes_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
let mut completions = vec![];
|
||||
|
||||
let Some(current) = current.to_str() else {
|
||||
return completions;
|
||||
};
|
||||
|
||||
let current = current.to_lowercase();
|
||||
|
||||
let opcodes = &OPCODE_DESCRIPTIONS;
|
||||
|
||||
for op in opcodes {
|
||||
// TODO if someone know how to do prefix_match with case insensitve in Rust
|
||||
// without converting the String to lowercase first, please fix this.
|
||||
let op_name = op.name.to_ascii_lowercase();
|
||||
if op_name.starts_with(¤t) {
|
||||
completions.push(CompletionCandidate::new(op.name).help(Some(op.description.into())));
|
||||
}
|
||||
}
|
||||
|
||||
completions
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct OpcodesArgs {
|
||||
/// Opcode to display description
|
||||
#[arg(add = ArgValueCompleter::new(opcodes_completer))]
|
||||
pub opcode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CwdArgs {
|
||||
/// Target directory
|
||||
#[arg(add = ArgValueCompleter::new(PathCompleter::dir()))]
|
||||
pub directory: String,
|
||||
}
|
||||
|
||||
@@ -72,11 +103,6 @@ pub struct TablesArgs {
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct LoadExtensionArgs {
|
||||
/// Path to extension file
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ListVfsArgs {
|
||||
/// Path to extension file
|
||||
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use clap::Args;
|
||||
use clap_complete::{ArgValueCompleter, PathCompleter};
|
||||
use limbo_core::Connection;
|
||||
use std::{fs::File, io::Write, path::PathBuf, rc::Rc, sync::Arc};
|
||||
|
||||
@@ -13,6 +14,7 @@ pub struct ImportArgs {
|
||||
/// Skip the first N rows of input
|
||||
#[arg(long, default_value = "0")]
|
||||
skip: u64,
|
||||
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
|
||||
file: PathBuf,
|
||||
table: String,
|
||||
}
|
||||
|
||||
@@ -35,9 +35,6 @@ pub enum Command {
|
||||
/// Open a database file
|
||||
#[command(display_name = ".open")]
|
||||
Open(OpenArgs),
|
||||
/// Print this message or the help of the given subcommand(s)
|
||||
// #[command(display_name = ".help")]
|
||||
// Help,
|
||||
/// Display schema for a table
|
||||
#[command(display_name = ".schema")]
|
||||
Schema(SchemaArgs),
|
||||
|
||||
159
cli/helper.rs
159
cli/helper.rs
@@ -1,12 +1,18 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use limbo_core::{Connection, StepResult};
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use rustyline::completion::{extract_word, Completer, Pair};
|
||||
use rustyline::highlight::Highlighter;
|
||||
use rustyline::hint::HistoryHinter;
|
||||
use rustyline::{Completer, Helper, Hinter, Validator};
|
||||
use shlex::Shlex;
|
||||
use std::cell::RefCell;
|
||||
use std::marker::PhantomData;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{ffi::OsString, path::PathBuf, str::FromStr as _};
|
||||
|
||||
use crate::commands::CommandParser;
|
||||
|
||||
macro_rules! try_result {
|
||||
($expr:expr, $err:expr) => {
|
||||
@@ -20,7 +26,7 @@ macro_rules! try_result {
|
||||
#[derive(Helper, Completer, Hinter, Validator)]
|
||||
pub struct LimboHelper {
|
||||
#[rustyline(Completer)]
|
||||
completer: SqlCompleter,
|
||||
completer: SqlCompleter<CommandParser>,
|
||||
#[rustyline(Hinter)]
|
||||
hinter: HistoryHinter,
|
||||
}
|
||||
@@ -77,57 +83,70 @@ impl Highlighter for LimboHelper {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SqlCompleter {
|
||||
pub struct SqlCompleter<C: Parser + Send + Sync + 'static> {
|
||||
conn: Rc<Connection>,
|
||||
io: Arc<dyn limbo_core::IO>,
|
||||
// Has to be a ref cell as Rustyline takes immutable reference to self
|
||||
// This problem would be solved with Reedline as it uses &mut self for completions
|
||||
cmd: RefCell<clap::Command>,
|
||||
_cmd_phantom: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl SqlCompleter {
|
||||
impl<C: Parser + Send + Sync + 'static> SqlCompleter<C> {
|
||||
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')
|
||||
Self {
|
||||
conn,
|
||||
io,
|
||||
cmd: C::command().into(),
|
||||
_cmd_phantom: PhantomData::default(),
|
||||
}
|
||||
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(
|
||||
fn dot_completion(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &rustyline::Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
mut line: &str,
|
||||
mut pos: usize,
|
||||
) -> rustyline::Result<(usize, Vec<Pair>)> {
|
||||
// TODO maybe check to see if the line is empty and then just output the command names
|
||||
line = &line[1..];
|
||||
pos = pos - 1;
|
||||
|
||||
let (prefix_pos, _) = extract_word(line, pos, ESCAPE_CHAR, default_break_chars);
|
||||
|
||||
let args = Shlex::new(line);
|
||||
let mut args = std::iter::once("".to_owned())
|
||||
.chain(args)
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>();
|
||||
if line.ends_with(' ') {
|
||||
args.push(OsString::new());
|
||||
}
|
||||
let arg_index = args.len() - 1;
|
||||
// dbg!(&pos, line, &args, arg_index);
|
||||
|
||||
let mut cmd = self.cmd.borrow_mut();
|
||||
match clap_complete::engine::complete(
|
||||
&mut cmd,
|
||||
args,
|
||||
arg_index,
|
||||
PathBuf::from_str(".").ok().as_deref(),
|
||||
) {
|
||||
Ok(candidates) => {
|
||||
let candidates = candidates
|
||||
.iter()
|
||||
.map(|candidate| Pair {
|
||||
display: candidate.get_value().to_string_lossy().into_owned(),
|
||||
replacement: candidate.get_value().to_string_lossy().into_owned(),
|
||||
})
|
||||
.collect::<Vec<Pair>>();
|
||||
|
||||
Ok((prefix_pos + 1, candidates))
|
||||
}
|
||||
Err(_) => Ok((prefix_pos + 1, Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn sql_completion(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec<Pair>)> {
|
||||
// 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();
|
||||
@@ -167,3 +186,51 @@ impl Completer for SqlCompleter {
|
||||
Ok((prefix_pos, candidates))
|
||||
}
|
||||
}
|
||||
|
||||
// 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<C: Parser + Send + Sync + 'static> Completer for SqlCompleter<C> {
|
||||
type Candidate = Pair;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &rustyline::Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
if line.starts_with(".") {
|
||||
self.dot_completion(line, pos)
|
||||
} else {
|
||||
self.sql_completion(line, pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user