spacetimedb_cli/subcommands/
repl.rs

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