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 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 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}