Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce reedline #9

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 357 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 23 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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,
Expand All @@ -31,7 +38,7 @@ pub mod context;
pub mod input;

pub struct App {
context: Rc<RwLock<Context>>,
context: Arc<RwLock<Context>>,
input: Box<dyn Input>,
}

Expand All @@ -48,7 +55,7 @@ pub enum AppError {
}

impl App {
pub fn new(input: Box<dyn Input>, context: Rc<RwLock<Context>>) -> Result<Self, AppError> {
pub fn new(input: Box<dyn Input>, context: Arc<RwLock<Context>>) -> Result<Self, AppError> {
Ok(App { input, context })
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)?;
}
Expand Down Expand Up @@ -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),
}
}
Expand Down
73 changes: 55 additions & 18 deletions src/app/completer.rs
Original file line number Diff line number Diff line change
@@ -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<RwLock<Context>>,
connection: Arc<RwLock<Context>>,
tables: RefCell<Vec<String>>,
views: RefCell<Vec<String>>,
}

impl YspCompleter {
pub fn new(connection: Rc<RwLock<Context>>) -> Self {
pub fn new(connection: Arc<RwLock<Context>>) -> Self {
Self {
connection,
tables: Default::default(),
Expand All @@ -20,6 +24,7 @@ impl YspCompleter {
}
}

/// For rustyline
impl Completer for YspCompleter {
type Candidate = YspCandidate;

Expand All @@ -30,6 +35,34 @@ impl Completer for YspCompleter {
ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
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<reedline::Suggestion> {
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<YspCandidate> {
let mut results = vec![];
if let Some(trailing) = line
.trim_end_matches(';')
Expand Down Expand Up @@ -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<YspCandidate> {
let mut results = vec![];
if self.tables.take().is_empty() {
Expand Down Expand Up @@ -161,6 +180,7 @@ pub enum YspCandidate {
Column(String),
}

/// For rustyline
impl Candidate for YspCandidate {
fn display(&self) -> &str {
match self {
Expand All @@ -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,
},
}
}
}
48 changes: 35 additions & 13 deletions src/app/context.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,58 @@
use colored::Colorize;
use yasqlplus_client::wrapper::Connection;

use crate::command::Command;

#[derive(Default)]
pub struct Context {
connection: Option<Connection>,
prompt_conn: String,
connection: Option<ConnectionWrapper>,
prompt_conn: Prompt,
last_command: Option<Command>,
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<Connection> {
&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<Connection>) {
self.connection = conn;
self.connection = conn.map(ConnectionWrapper);
}

pub fn get_command(&self) -> &Option<Command> {
Expand Down
14 changes: 3 additions & 11 deletions src/app/helper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{rc::Rc, sync::RwLock};
use std::sync::{Arc, RwLock};

use rustyline::{hint::HistoryHinter, Completer, Helper, Highlighter, Hinter, Validator};

Expand All @@ -19,20 +19,12 @@ pub struct YspHelper {
}

impl YspHelper {
pub fn new(context: Rc<RwLock<Context>>) -> Self {
pub fn new(context: Arc<RwLock<Context>>) -> 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;
}
}
40 changes: 39 additions & 1 deletion src/app/highlight.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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::<Vec<_>>(),
}
}
}

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)
}
5 changes: 5 additions & 0 deletions src/app/input/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Option<(Command, String)>, InputError>;
fn line(&self, prompt: &str) -> Result<String, InputError>;
Expand Down
Loading
Loading