partiql_cli/
repl.rs

1#![deny(rustdoc::broken_intra_doc_links)]
2
3use partiql_parser::ParseError;
4use rustyline::completion::Completer;
5use rustyline::config::Configurer;
6use rustyline::highlight::Highlighter;
7use rustyline::hint::{Hinter, HistoryHinter};
8
9use rustyline::validate::{ValidationContext, ValidationResult, Validator};
10use rustyline::{ColorMode, Context, Helper};
11use std::borrow::Cow;
12use std::fs::OpenOptions;
13
14use std::path::Path;
15
16use syntect::easy::HighlightLines;
17use syntect::highlighting::{Style, ThemeSet};
18use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
19use syntect::util::as_24_bit_terminal_escaped;
20
21use miette::{IntoDiagnostic, Report};
22use owo_colors::OwoColorize;
23
24use crate::error::CLIErrors;
25
26static ION_SYNTAX: &str = include_str!("ion.sublime-syntax");
27static PARTIQL_SYNTAX: &str = include_str!("partiql.sublime-syntax");
28
29struct PartiqlHelperConfig {
30    dark_theme: bool,
31}
32
33impl PartiqlHelperConfig {
34    pub fn infer() -> Self {
35        const TERM_TIMEOUT_MILLIS: u64 = 20;
36        let timeout = std::time::Duration::from_millis(TERM_TIMEOUT_MILLIS);
37        let theme = termbg::theme(timeout);
38        let dark_theme = match theme {
39            Ok(termbg::Theme::Light) => false,
40            Ok(termbg::Theme::Dark) => true,
41            _ => true,
42        };
43        PartiqlHelperConfig { dark_theme }
44    }
45}
46struct PartiqlHelper {
47    config: PartiqlHelperConfig,
48    syntaxes: SyntaxSet,
49    themes: ThemeSet,
50}
51
52impl PartiqlHelper {
53    pub fn new(config: PartiqlHelperConfig) -> Result<Self, ()> {
54        let ion_def = SyntaxDefinition::load_from_str(ION_SYNTAX, false, Some("ion")).unwrap();
55        let partiql_def =
56            SyntaxDefinition::load_from_str(PARTIQL_SYNTAX, false, Some("partiql")).unwrap();
57        let mut builder = SyntaxSetBuilder::new();
58        builder.add(ion_def);
59        builder.add(partiql_def);
60
61        let syntaxes = builder.build();
62
63        let _ps = SyntaxSet::load_defaults_newlines();
64        let themes = ThemeSet::load_defaults();
65        Ok(PartiqlHelper {
66            config,
67            syntaxes,
68            themes,
69        })
70    }
71}
72
73impl Helper for PartiqlHelper {}
74
75impl Completer for PartiqlHelper {
76    type Candidate = String;
77}
78impl Hinter for PartiqlHelper {
79    type Hint = String;
80
81    fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<Self::Hint> {
82        let hinter = HistoryHinter {};
83        hinter.hint(line, pos, ctx)
84    }
85}
86
87impl Highlighter for PartiqlHelper {
88    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
89        let syntax = self
90            .syntaxes
91            .find_syntax_by_extension("partiql")
92            .unwrap()
93            .clone();
94        let theme = if self.config.dark_theme {
95            &self.themes.themes["Solarized (dark)"]
96        } else {
97            &self.themes.themes["Solarized (light)"]
98        };
99        let mut highlighter = HighlightLines::new(&syntax, theme);
100
101        let ranges: Vec<(Style, &str)> = highlighter.highlight_line(line, &self.syntaxes).unwrap();
102        (as_24_bit_terminal_escaped(&ranges[..], true) + "\x1b[0m").into()
103    }
104    fn highlight_char(&self, line: &str, pos: usize) -> bool {
105        let _ = (line, pos);
106        true
107    }
108}
109
110impl Validator for PartiqlHelper {
111    fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
112        // TODO remove this command parsing hack do something better
113        let mut source = ctx.input();
114        let flag_display = source.starts_with("\\ast");
115        if flag_display {
116            source = &source[4..];
117        }
118
119        let parser = partiql_parser::Parser::default();
120        let result = parser.parse(source);
121        match result {
122            Ok(_parsed) => {
123                #[cfg(feature = "visualize")]
124                if flag_display {
125                    use crate::visualize::render::display;
126                    display(&_parsed.ast);
127                }
128
129                Ok(ValidationResult::Valid(None))
130            }
131            Err(e) => {
132                if e.errors
133                    .iter()
134                    .any(|err| matches!(err, ParseError::UnexpectedEndOfInput))
135                {
136                    // TODO For now, this is what allows you to do things like hit `<ENTER>` and continue writing the query on the next line in the middle of a query.
137                    // TODO we should probably do something more ergonomic. Perhaps require a `;` or two newlines to end?
138                    Ok(ValidationResult::Incomplete)
139                } else {
140                    let err = Report::new(CLIErrors::from_parser_error(e));
141                    Ok(ValidationResult::Invalid(Some(format!("\n\n{:?}", err))))
142                }
143            }
144        }
145    }
146}
147
148pub fn repl() -> miette::Result<()> {
149    let mut rl = rustyline::Editor::<PartiqlHelper>::new().into_diagnostic()?;
150    rl.set_color_mode(ColorMode::Forced);
151    rl.set_helper(Some(
152        PartiqlHelper::new(PartiqlHelperConfig::infer()).unwrap(),
153    ));
154    let expanded = shellexpand::tilde("~/partiql_cli.history").to_string();
155    let history_path = Path::new(&expanded);
156    OpenOptions::new()
157        .write(true)
158        .create(true)
159        .append(true)
160        .open(history_path)
161        .expect("history file create if not exists");
162    rl.load_history(history_path).expect("history load");
163
164    println!("===============================");
165    println!("PartiQL REPL");
166    println!("CTRL-D on an empty line to quit");
167    println!("===============================");
168
169    loop {
170        let readline = rl.readline(">> ");
171        match readline {
172            Ok(line) => {
173                println!("{}", "Parse OK!".green());
174                rl.add_history_entry(line);
175            }
176            Err(_) => {
177                println!("Exiting...");
178                rl.append_history(history_path).expect("append history");
179                break;
180            }
181        }
182    }
183
184    Ok(())
185}