spacetimedb_cli/subcommands/
repl.rs1use crate::api::{ClientApi, Connection};
2use crate::sql::run_sql;
3use colored::*;
4use dirs::home_dir;
5use std::env::temp_dir;
6
7use rustyline::completion::Completer;
8use rustyline::error::ReadlineError;
9use rustyline::highlight::Highlighter;
10use rustyline::hint::Hinter;
11use rustyline::history::DefaultHistory;
12use rustyline::validate::{MatchingBracketValidator, Validator};
13use rustyline::{Editor, Helper};
14
15use syntect::easy::HighlightLines;
16use syntect::highlighting::{Theme, ThemeSet};
17use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
18use syntect::util::LinesWithEndings;
19
20static SQL_SYNTAX: &str = include_str!("../../tools/sublime/SpacetimeDBSQL.sublime-syntax");
21static SYNTAX_NAME: &str = "SQL (SpacetimeDB)";
22
23static AUTO_COMPLETE: &str = "\
24true
25false
26select
27from
28insert
29into
30values
31update,
32delete,
33create,
34where
35join
36sort by
37.exit
38.clear
39";
40
41pub async fn exec(con: Connection) -> Result<(), anyhow::Error> {
42 let database = con.database.clone();
43 let mut rl = Editor::<ReplHelper, DefaultHistory>::new().unwrap();
44 let history = home_dir().unwrap_or_else(temp_dir).join(".stdb.history.txt");
45 if rl.load_history(&history).is_err() {
46 eprintln!("No previous history.");
47 }
48 rl.set_helper(Some(ReplHelper::new().unwrap()));
49
50 println!(
51 "\
52┌──────────────────────────────────────────────────────────┐
53│ .exit: Exit the REPL │
54│ .clear: Clear the Screen │
55│ │
56│ Give us feedback in our Discord server: │
57│ https://discord.gg/w2DVqNZXdN │
58└──────────────────────────────────────────────────────────┘",
59 );
60
61 let api = ClientApi::new(con);
62
63 loop {
64 let readline = rl.readline(&format!("🪐{}>", &database).green());
65 match readline {
66 Ok(line) => match line.as_str() {
67 ".exit" => break,
68 ".clear" => {
69 rl.clear_screen().ok();
70 }
71 sql => {
72 rl.add_history_entry(sql).ok();
73
74 if let Err(err) = run_sql(api.sql(), sql, true).await {
75 eprintln!("{}", err.to_string().red())
76 }
77 }
78 },
79 Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
80 println!("\n{}", "Aborted!".red());
81 break;
82 }
83 x => {
84 eprintln!("\nUnexpected: {x:?}");
85 break;
86 }
87 }
88 }
89
90 rl.save_history(&history).ok();
91
92 Ok(())
93}
94
95pub(crate) struct ReplHelper {
96 syntaxes: SyntaxSet,
97 theme: Theme,
98 brackets: MatchingBracketValidator,
99}
100
101impl ReplHelper {
102 pub fn new() -> Result<Self, ()> {
103 let syntax_def = SyntaxDefinition::load_from_str(SQL_SYNTAX, false, Some(SYNTAX_NAME)).unwrap();
104 let mut builder = SyntaxSetBuilder::new();
105 builder.add(syntax_def);
106
107 let syntaxes = builder.build();
108
109 let _ps = SyntaxSet::load_defaults_newlines();
110 let ts = ThemeSet::load_defaults();
111 let theme = ts.themes["base16-ocean.dark"].clone();
112
113 Ok(ReplHelper {
114 syntaxes,
115 theme,
116 brackets: MatchingBracketValidator::new(),
117 })
118 }
119}
120
121impl Helper for ReplHelper {}
122
123impl Completer for ReplHelper {
124 type Candidate = String;
125
126 fn complete(
127 &self,
128 line: &str,
129 pos: usize,
130 _: &rustyline::Context<'_>,
131 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
132 let mut name = String::new();
133 let mut name_pos = pos;
134 while let Some(char) = line
135 .chars()
136 .nth(name_pos.wrapping_sub(1))
137 .filter(|c| c.is_ascii_alphanumeric() || ['_', '.'].contains(c))
138 {
139 name.push(char);
140 name_pos -= 1;
141 }
142 if name.is_empty() {
143 return Ok((0, vec![]));
144 }
145 name = name.chars().rev().collect();
146
147 let completions: Vec<_> = AUTO_COMPLETE
148 .split('\n')
149 .filter(|it| it.starts_with(&name))
150 .map(str::to_owned)
151 .collect();
152
153 Ok((name_pos, completions))
154 }
155}
156
157impl Hinter for ReplHelper {
158 type Hint = String;
159
160 fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
161 if line.len() > pos {
162 return None;
163 }
164 if let Ok((mut completion_pos, completions)) = self.complete(line, pos, ctx) {
165 if completions.is_empty() {
166 return None;
167 }
168 let mut hint = completions[0].clone();
169 while completion_pos < pos {
170 if hint.is_empty() {
171 return None;
172 }
173 hint.remove(0);
174 completion_pos += 1;
175 }
176 Some(hint)
177 } else {
178 None
179 }
180 }
181}
182
183impl Highlighter for ReplHelper {
184 fn highlight<'l>(&self, line: &'l str, _: usize) -> std::borrow::Cow<'l, str> {
185 let mut h = HighlightLines::new(self.syntaxes.find_syntax_by_name(SYNTAX_NAME).unwrap(), &self.theme);
186 let mut out = String::new();
187 for line in LinesWithEndings::from(line) {
188 let ranges = h.highlight_line(line, &self.syntaxes).unwrap();
189 let escaped = syntect::util::as_24_bit_terminal_escaped(&ranges[..], false);
190 out += &escaped;
191 }
192 std::borrow::Cow::Owned(out)
193 }
194
195 fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str, _: bool) -> std::borrow::Cow<'b, str> {
196 std::borrow::Cow::Owned(prompt.green().to_string())
197 }
198
199 fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
200 std::borrow::Cow::Owned(hint.bright_black().to_string())
201 }
202
203 fn highlight_candidate<'c>(&self, candidate: &'c str, _: rustyline::CompletionType) -> std::borrow::Cow<'c, str> {
204 std::borrow::Cow::Owned(candidate.bright_cyan().to_string())
205 }
206
207 fn highlight_char(&self, _: &str, _: usize) -> bool {
208 true
209 }
210}
211
212impl Validator for ReplHelper {
213 fn validate(
214 &self,
215 ctx: &mut rustyline::validate::ValidationContext,
216 ) -> rustyline::Result<rustyline::validate::ValidationResult> {
217 self.brackets.validate(ctx)
218 }
219}