diff --git a/Cargo.lock b/Cargo.lock index 5b6deef..c0ffe87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi-str" version = "0.8.0" @@ -33,7 +48,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "220044e6a1bb31ddee4e3db724d29767f352de47445a6cd75e1a173142136c83" dependencies = [ "nom", - "vte", + "vte 0.10.1", ] [[package]] @@ -96,6 +111,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "base64" version = "0.21.5" @@ -145,6 +166,15 @@ name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecount" @@ -176,6 +206,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -265,6 +308,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "crc32fast" version = "1.3.2" @@ -274,6 +323,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "deranged" version = "0.3.10" @@ -344,6 +419,17 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "fd-lock" version = "4.0.2" @@ -409,6 +495,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -419,12 +528,30 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -485,6 +612,16 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -512,6 +649,18 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -542,6 +691,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -589,6 +756,29 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -712,6 +902,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "reedline" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07046f804ccb26a6fa8b638f505ccd2a8b7702399a89b9640a0d40ecda49233d" +dependencies = [ + "chrono", + "crossterm", + "fd-lock 3.0.13", + "itertools", + "nu-ansi-term", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "regex" version = "1.10.2" @@ -766,6 +976,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "rustyline" version = "13.0.0" @@ -774,7 +990,7 @@ dependencies = [ "bitflags 2.4.1", "cfg-if", "clipboard-win", - "fd-lock", + "fd-lock 4.0.2", "home", "libc", "log", @@ -819,6 +1035,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.193" @@ -856,18 +1078,76 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte 0.11.1", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.39", +] + [[package]] name = "syn" version = "1.0.109" @@ -1037,6 +1317,16 @@ dependencies = [ "vte_generate_state_changes", ] +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + [[package]] name = "vte_generate_state_changes" version = "0.1.1" @@ -1063,6 +1353,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + [[package]] name = "which" version = "4.4.2" @@ -1106,6 +1450,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -1322,6 +1675,8 @@ dependencies = [ "colored", "console", "dirs", + "nu-ansi-term", + "reedline", "rustyline", "syntect", "tabled", diff --git a/Cargo.toml b/Cargo.toml index 2f111c8..74c5977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ clap = { version = "4.4.12", features = ["derive"] } colored = "2.1.0" console = "0.15.7" dirs = "5.0.1" +nu-ansi-term = "0.49.0" +reedline = "0.27.1" syntect = "5.1.0" tabled = { version = "0.15.0", features = ["ansi"] } terminal_size = "0.3.0" diff --git a/src/app.rs b/src/app.rs index d705f98..b73bfd8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,8 @@ -use std::{io::Write, process::Stdio, rc::Rc, sync::RwLock}; +use std::{ + io::Write, + process::Stdio, + sync::{Arc, RwLock}, +}; use colored::Colorize; @@ -13,7 +17,10 @@ use tabled::{ use terminal_size::{terminal_size, Height, Width}; use yasqlplus_client::wrapper::{Connection, DiagInfo, Error, Executed, LazyExecuted}; -use crate::command::{self, Command, InternalCommand, ParseError}; +use crate::{ + app::input::INDICATOR, + command::{self, Command, InternalCommand, ParseError}, +}; use self::{ context::Context, @@ -31,7 +38,7 @@ pub mod context; pub mod input; pub struct App { - context: Rc>, + context: Arc>, input: Box, } @@ -48,7 +55,7 @@ pub enum AppError { } impl App { - pub fn new(input: Box, context: Rc>) -> Result { + pub fn new(input: Box, context: Arc>) -> Result { Ok(App { input, context }) } @@ -86,7 +93,15 @@ impl App { ctx.set_command(command); if ctx.need_echo() { - println!("{}{}", ctx.get_prompt(), command_str.unwrap_or_default()); + let start = format!("{}{}", ctx.get_prompt().to_string(), INDICATOR); + let rest_start = format!( + "{}{}", + ".".repeat(ctx.get_prompt().to_string().chars().count()), + " ".repeat(INDICATOR.chars().count()) + ); + for (idx, line) in command_str.unwrap_or_default().lines().enumerate() { + println!("{}{}", if idx == 0 { &start } else { &rest_start }, line); + } } let command = ctx.get_command(); @@ -120,11 +135,12 @@ impl App { match self.connect(host.clone(), *port, username.clone(), password.clone()) { Ok((conn, prompt)) => { ctx.set_connection(Some(conn)); - ctx.set_prompt(prompt); + ctx.set_prompt(context::Prompt::Connected(prompt)); println!("Connected!"); } Err(err) => { ctx.set_connection(None); + ctx.set_prompt(context::Prompt::Ready); println!("Failed to connect: "); self.print_execute_sql_error(err)?; } @@ -340,7 +356,7 @@ impl App { None => self.input.line("Password: ").unwrap_or_default(), }; match Connection::connect(&host, port, &username, &password) { - Ok(conn) => Ok((conn, format!("{username}@{host}:{port} > "))), + Ok(conn) => Ok((conn, format!("{username}@{host}:{port}"))), Err(err) => Err(err), } } diff --git a/src/app/completer.rs b/src/app/completer.rs index fe09258..48a9a8c 100644 --- a/src/app/completer.rs +++ b/src/app/completer.rs @@ -1,17 +1,21 @@ -use std::{cell::RefCell, rc::Rc, sync::RwLock}; +use std::{ + cell::RefCell, + sync::{Arc, RwLock}, +}; +use reedline::{Span, Suggestion}; use rustyline::completion::{Candidate, Completer}; use super::context::Context; pub struct YspCompleter { - connection: Rc>, + connection: Arc>, tables: RefCell>, views: RefCell>, } impl YspCompleter { - pub fn new(connection: Rc>) -> Self { + pub fn new(connection: Arc>) -> Self { Self { connection, tables: Default::default(), @@ -20,6 +24,7 @@ impl YspCompleter { } } +/// For rustyline impl Completer for YspCompleter { type Candidate = YspCandidate; @@ -30,6 +35,34 @@ impl Completer for YspCompleter { ctx: &rustyline::Context<'_>, ) -> rustyline::Result<(usize, Vec)> { let _ = (line, pos, ctx); + + Ok((pos, self.get_completions(line))) + } + + fn update( + &self, + line: &mut rustyline::line_buffer::LineBuffer, + start: usize, + elected: &str, + cl: &mut rustyline::Changeset, + ) { + let end = line.pos(); + line.replace(start..end, elected, cl); + } +} + +impl reedline::Completer for YspCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + self.get_completions(line) + .into_iter() + .map(|x| x.into_suggestion(pos)) + .collect() + } +} + +/// Common impl +impl YspCompleter { + pub fn get_completions(&self, line: &str) -> Vec { let mut results = vec![]; if let Some(trailing) = line .trim_end_matches(';') @@ -60,23 +93,9 @@ impl Completer for YspCompleter { .map(YspCandidate::Keyword), ); } - - Ok((pos, results)) - } - - fn update( - &self, - line: &mut rustyline::line_buffer::LineBuffer, - start: usize, - elected: &str, - cl: &mut rustyline::Changeset, - ) { - let end = line.pos(); - line.replace(start..end, elected, cl); + results } -} -impl YspCompleter { pub fn complete_query(&self, trailing: &str) -> Vec { let mut results = vec![]; if self.tables.take().is_empty() { @@ -161,6 +180,7 @@ pub enum YspCandidate { Column(String), } +/// For rustyline impl Candidate for YspCandidate { fn display(&self) -> &str { match self { @@ -180,3 +200,20 @@ impl Candidate for YspCandidate { } } } + +impl YspCandidate { + pub fn into_suggestion(self, pos: usize) -> Suggestion { + match self { + YspCandidate::Keyword(v) + | YspCandidate::Table(v) + | YspCandidate::View(v) + | YspCandidate::Column(v) => Suggestion { + value: v, + description: None, + extra: None, + span: Span::new(pos, pos + 1), + append_whitespace: false, + }, + } + } +} diff --git a/src/app/context.rs b/src/app/context.rs index b862865..23c75cf 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -1,36 +1,58 @@ -use colored::Colorize; use yasqlplus_client::wrapper::Connection; use crate::command::Command; #[derive(Default)] pub struct Context { - connection: Option, - prompt_conn: String, + connection: Option, + prompt_conn: Prompt, last_command: Option, need_echo: bool, less_enabled: bool, } -impl Context { - pub fn get_prompt(&self) -> String { - if self.connection.is_none() { - "SQL > ".to_owned() - } else { - self.prompt_conn.green().to_string() +pub struct ConnectionWrapper(pub Connection); + +unsafe impl Sync for ConnectionWrapper {} +unsafe impl Send for ConnectionWrapper {} + +#[derive(Debug, Clone)] +pub enum Prompt { + Ready, + Connected(String), +} + +impl Default for Prompt { + fn default() -> Self { + Self::Ready + } +} + +impl ToString for Prompt { + fn to_string(&self) -> String { + match self { + Prompt::Ready => "SQL", + Prompt::Connected(c) => c, } + .to_string() + } +} + +impl Context { + pub fn get_prompt(&self) -> Prompt { + self.prompt_conn.clone() } - pub fn set_prompt(&mut self, prompt: String) { + pub fn set_prompt(&mut self, prompt: Prompt) { self.prompt_conn = prompt; } - pub fn get_connection(&self) -> &Option { - &self.connection + pub fn get_connection(&self) -> Option<&Connection> { + self.connection.as_ref().map(|x| &x.0) } pub fn set_connection(&mut self, conn: Option) { - self.connection = conn; + self.connection = conn.map(ConnectionWrapper); } pub fn get_command(&self) -> &Option { diff --git a/src/app/helper.rs b/src/app/helper.rs index ad1113e..1c993f7 100644 --- a/src/app/helper.rs +++ b/src/app/helper.rs @@ -1,4 +1,4 @@ -use std::{rc::Rc, sync::RwLock}; +use std::sync::{Arc, RwLock}; use rustyline::{hint::HistoryHinter, Completer, Helper, Highlighter, Hinter, Validator}; @@ -19,20 +19,12 @@ pub struct YspHelper { } impl YspHelper { - pub fn new(context: Rc>) -> Self { + pub fn new(context: Arc>) -> Self { YspHelper { - validator: YspValidator::new(), + validator: YspValidator, hinter: HistoryHinter::new(), hightligter: YspHightligter::new(), completer: YspCompleter::new(context), } } - - pub fn disable_validation(&mut self) { - self.validator.enabled = false; - } - - pub fn enable_validation(&mut self) { - self.validator.enabled = true; - } } diff --git a/src/app/highlight.rs b/src/app/highlight.rs index 97de6d3..57ae202 100644 --- a/src/app/highlight.rs +++ b/src/app/highlight.rs @@ -1,7 +1,8 @@ use colored::Colorize; +use reedline::StyledText; use rustyline::highlight::Highlighter; use syntect::easy::HighlightLines; -use syntect::highlighting::{Style, Theme, ThemeSet}; +use syntect::highlighting::{FontStyle, Style, Theme, ThemeSet}; use syntect::parsing::SyntaxSet; use syntect::util::as_24_bit_terminal_escaped; @@ -34,3 +35,40 @@ impl Highlighter for YspHightligter { std::borrow::Cow::Owned(escaped) } } + +impl reedline::Highlighter for YspHightligter { + fn highlight(&self, line: &str, _cursor: usize) -> reedline::StyledText { + let syntax_set = SyntaxSet::load_defaults_newlines(); + let ts = ThemeSet::load_defaults(); + let theme = ts.themes["base16-ocean.dark"].clone(); + + let syntax: &syntect::parsing::SyntaxReference = + syntax_set.find_syntax_by_extension("sql").unwrap(); + let mut h = HighlightLines::new(syntax, &theme); + let ranges = h.highlight_line(line, &syntax_set).unwrap(); + StyledText { + buffer: ranges + .iter() + .map(|(st, str)| { + let mut style = nu_ansi_term::Style::new() + .fg(hl_color_to_nu_color(st.foreground)) + .on(hl_color_to_nu_color(st.background)); + if st.font_style.contains(FontStyle::BOLD) { + style = style.bold(); + } + if st.font_style.contains(FontStyle::ITALIC) { + style = style.italic(); + } + if st.font_style.contains(FontStyle::UNDERLINE) { + style = style.underline(); + } + (style, str.to_string()) + }) + .collect::>(), + } + } +} + +fn hl_color_to_nu_color(color: syntect::highlighting::Color) -> nu_ansi_term::Color { + nu_ansi_term::Color::Rgb(color.r, color.g, color.b) +} diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index f5c6084..4f17d6a 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -1,15 +1,20 @@ mod error; mod reader; +mod reed; mod shell; mod single; pub use error::*; pub use reader::*; +pub use reed::*; pub use shell::*; pub use single::*; use crate::command::Command; +pub const INDICATOR: &str = " > "; +pub const INDICATOR_NORMAL: &str = " : "; + pub trait Input { fn get_command(&self) -> Result, InputError>; fn line(&self, prompt: &str) -> Result; diff --git a/src/app/input/reed.rs b/src/app/input/reed.rs new file mode 100644 index 0000000..148a148 --- /dev/null +++ b/src/app/input/reed.rs @@ -0,0 +1,161 @@ +use std::{ + borrow::Cow, + cell::RefCell, + sync::{Arc, RwLock}, +}; + +use nu_ansi_term::Style; +use reedline::{ + default_emacs_keybindings, ColumnarMenu, DefaultHinter, DefaultPrompt, DefaultPromptSegment, + Emacs, FileBackedHistory, KeyCode, KeyModifiers, Prompt, Reedline, ReedlineEvent, ReedlineMenu, + Signal, +}; + +use crate::{ + app::{ + completer::YspCompleter, + context::{self, Context}, + highlight::YspHightligter, + validate::YspValidator, + }, + command::{parse_command, Command, ParseError}, +}; + +use super::{Input, InputError, INDICATOR, INDICATOR_NORMAL}; + +pub struct Reed { + context: Arc>, + rl: RefCell, + rl_line: RefCell, +} + +impl Reed { + pub fn new(context: Arc>, history_file: String) -> Result { + // TODO completion + let mut keybindings = default_emacs_keybindings(); + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + let edit_mode = Box::new(Emacs::new(keybindings)); + + let rl_line = Reedline::create(); + let rl = Reedline::create() + .with_validator(Box::new(YspValidator)) + .with_highlighter(Box::new(YspHightligter::new())) + .with_completer(Box::new(YspCompleter::new(context.clone()))) + .with_menu(ReedlineMenu::EngineCompleter(Box::new( + ColumnarMenu::default().with_name("completion_menu"), + ))) + .with_hinter(Box::new( + DefaultHinter::default().with_style(Style::new().bold()), + )) + .with_history(Box::new(FileBackedHistory::with_file( + 4096, + history_file.clone().into(), + )?)) + .with_edit_mode(edit_mode); + Ok(Self { + context, + rl: RefCell::new(rl), + rl_line: RefCell::new(rl_line), + }) + } +} + +impl Input for Reed { + fn get_command(&self) -> Result, InputError> { + let mut rl = self.rl.borrow_mut(); + let prompt = self.context.read().unwrap().get_prompt(); + let prompt = MyPrompt { + prompt: prompt.clone(), + base_prompt: DefaultPrompt::new( + DefaultPromptSegment::Basic(prompt.to_string()), + DefaultPromptSegment::CurrentDateTime, + ), + }; + match rl.read_line(&prompt)? { + Signal::Success(input) => match parse_command(&input) { + Ok(command) => Ok(Some((command, input))), + Err(ParseError::Empty) => Ok(None), + Err(err) => Err(err.into()), + }, + Signal::CtrlC => Err(InputError::Cancelled), + Signal::CtrlD => Err(InputError::Eof), + } + } + + fn line(&self, prompt: &str) -> Result { + let mut rl = self.rl_line.borrow_mut(); + match rl.read_line(&DefaultPrompt::new( + DefaultPromptSegment::Basic(prompt.to_string()), + DefaultPromptSegment::CurrentDateTime, + ))? { + Signal::Success(input) => Ok(input), + Signal::CtrlC => Err(InputError::Cancelled), + Signal::CtrlD => Err(InputError::Eof), + } + } +} + +struct MyPrompt { + prompt: context::Prompt, + base_prompt: DefaultPrompt, +} + +impl Prompt for MyPrompt { + fn render_prompt_left(&self) -> std::borrow::Cow { + self.base_prompt.render_prompt_left() + } + + fn render_prompt_right(&self) -> std::borrow::Cow { + self.base_prompt.render_prompt_right() + } + + fn render_prompt_indicator( + &self, + prompt_mode: reedline::PromptEditMode, + ) -> std::borrow::Cow { + match prompt_mode { + reedline::PromptEditMode::Default => Cow::Borrowed(INDICATOR), + reedline::PromptEditMode::Emacs => Cow::Borrowed(INDICATOR), + reedline::PromptEditMode::Vi(v) => match v { + reedline::PromptViMode::Normal => Cow::Borrowed(INDICATOR_NORMAL), + reedline::PromptViMode::Insert => Cow::Borrowed(INDICATOR), + }, + reedline::PromptEditMode::Custom(_) => Cow::Borrowed(INDICATOR), + } + } + + fn render_prompt_multiline_indicator(&self) -> std::borrow::Cow { + match &self.base_prompt.left_prompt { + DefaultPromptSegment::Basic(s) => { + (".".repeat(s.chars().count()) + &(" ".repeat(INDICATOR.len()))).into() + } + _ => "".into(), + } + } + + fn render_prompt_history_search_indicator( + &self, + history_search: reedline::PromptHistorySearch, + ) -> std::borrow::Cow { + self.base_prompt + .render_prompt_history_search_indicator(history_search) + } + + fn get_indicator_color(&self) -> reedline::Color { + self.get_prompt_color() + } + + fn get_prompt_color(&self) -> reedline::Color { + match self.prompt { + context::Prompt::Ready => reedline::Color::White, + context::Prompt::Connected(_) => reedline::Color::Green, + } + } +} diff --git a/src/app/input/shell.rs b/src/app/input/shell.rs index 92fdb9d..ea02b4d 100644 --- a/src/app/input/shell.rs +++ b/src/app/input/shell.rs @@ -1,24 +1,27 @@ -use std::{cell::RefCell, rc::Rc, sync::RwLock}; +use std::sync::Arc; +use std::{cell::RefCell, sync::RwLock}; +use colored::Colorize; use rustyline::{ history::FileHistory, Cmd, CompletionType, Config, EditMode, Editor, EventHandler, KeyEvent, }; -use rustyline::{KeyCode, Modifiers}; +use rustyline::{DefaultEditor, KeyCode, Modifiers}; use crate::command::{parse_command, Command, ParseError}; use crate::app::{context::Context, helper::YspHelper}; -use super::{Input, InputError}; +use super::{Input, InputError, INDICATOR}; pub struct ShellInput { - context: Rc>, + context: Arc>, rl: RefCell>, + rl2: RefCell, history_file: String, } impl ShellInput { - pub fn new(context: Rc>, history_file: String) -> Result { + pub fn new(context: Arc>, history_file: String) -> Result { let config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::Circular) @@ -36,6 +39,7 @@ impl ShellInput { let _ = rl.load_history(&history_file); Ok(Self { rl: RefCell::new(rl), + rl2: RefCell::new(DefaultEditor::new()?), context, history_file, }) @@ -44,10 +48,13 @@ impl ShellInput { impl Input for ShellInput { fn get_command(&self) -> Result, InputError> { - let input = self - .rl - .borrow_mut() - .readline(&self.context.read().unwrap().get_prompt())?; + let prompt = match &self.context.read().unwrap().get_prompt() { + crate::app::context::Prompt::Ready => format!("SQL{}", INDICATOR), + crate::app::context::Prompt::Connected(c) => { + format!("{c}{}", INDICATOR).green().to_string() + } + }; + let input = self.rl.borrow_mut().readline(&prompt)?; let command = match parse_command(&input) { Ok(command) => Some((command, input)), Err(ParseError::Empty) => None, @@ -58,10 +65,8 @@ impl Input for ShellInput { } fn line(&self, prompt: &str) -> Result { - let mut rl = self.rl.borrow_mut(); - rl.helper_mut().unwrap().disable_validation(); + let mut rl = self.rl2.borrow_mut(); let input = rl.readline(prompt); - rl.helper_mut().unwrap().enable_validation(); input.map_err(InputError::from) } } diff --git a/src/app/validate.rs b/src/app/validate.rs index 53dbb89..bf890f3 100644 --- a/src/app/validate.rs +++ b/src/app/validate.rs @@ -1,23 +1,18 @@ -use rustyline::validate::{ValidationContext, ValidationResult, Validator}; +use rustyline::validate::{ValidationContext, ValidationResult}; use crate::command::parse_command; -pub struct YspValidator { - pub enabled: bool, -} +pub struct YspValidator; -impl Validator for YspValidator { +impl rustyline::validate::Validator for YspValidator { fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { let input = ctx.input(); // validate sql mainly - if self.enabled { - return match parse_command(input) { - Err(crate::command::ParseError::Incomplete(_)) => Ok(ValidationResult::Incomplete), - _ => Ok(ValidationResult::Valid(None)), - }; + match parse_command(input) { + Err(crate::command::ParseError::Incomplete(_)) => Ok(ValidationResult::Incomplete), + _ => Ok(ValidationResult::Valid(None)), } - Ok(ValidationResult::Valid(None)) } fn validate_while_typing(&self) -> bool { @@ -25,8 +20,14 @@ impl Validator for YspValidator { } } -impl YspValidator { - pub fn new() -> Self { - YspValidator { enabled: true } +impl reedline::Validator for YspValidator { + fn validate(&self, line: &str) -> reedline::ValidationResult { + let input = line; + match parse_command(input) { + Err(crate::command::ParseError::Incomplete(_)) => { + reedline::ValidationResult::Incomplete + } + _ => reedline::ValidationResult::Complete, + } } } diff --git a/src/main.rs b/src/main.rs index bda309e..121acbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,13 @@ -use std::{fs::File, io::BufReader, path::PathBuf, rc::Rc, sync::RwLock}; +use std::{ + fs::File, + io::{BufReader, IsTerminal}, + path::PathBuf, + sync::{Arc, RwLock}, +}; use app::{ context::Context, - input::{BufReaderInput, Input, ShellInput, SingleCommand}, + input::{BufReaderInput, Input, Reed, ShellInput, SingleCommand}, AppError, }; use clap::Parser; @@ -71,6 +76,11 @@ struct Cli { /// History file name. #[arg(short = 'F', long, default_value = "yasqlplus-history.txt")] history_file: String, + + /// Use reedline as line editor. + /// EXPERIMENTAL FEATURE. + #[arg(long, verbatim_doc_comment)] + reedline: bool, } fn main() -> Result<(), AppError> { @@ -80,22 +90,25 @@ fn main() -> Result<(), AppError> { ctx.set_need_echo(args.echo); ctx.set_less_enabled(!args.no_less); - let ctx = Rc::new(RwLock::new(ctx)); + let history_file = args + .history_path + .map(PathBuf::from) + .unwrap_or_else(|| dirs::home_dir().unwrap_or_default()) + .join(args.history_file) + .to_str() + .unwrap() + .to_owned(); + + let stdin = std::io::stdin(); + let ctx = Arc::new(RwLock::new(ctx)); let input: Box = match args.command { Some(command) => Box::new(SingleCommand::new(command)), + None if !stdin.is_terminal() => Box::new(BufReaderInput::new(BufReader::new(stdin))), None => match args.file { // TODO support network file(e.g. http/https) Some(input) => Box::new(BufReaderInput::new(BufReader::new(File::open(input)?))), - None => Box::new(ShellInput::new( - ctx.clone(), - args.history_path - .map(PathBuf::from) - .unwrap_or_else(|| dirs::home_dir().unwrap_or_default()) - .join(args.history_file) - .to_str() - .unwrap() - .to_owned(), - )?), + None if args.reedline => Box::new(Reed::new(ctx.clone(), history_file)?), + None => Box::new(ShellInput::new(ctx.clone(), history_file)?), }, }; let mut app = app::App::new(input, ctx)?;