Skip to main content

zql_cli/core/
editor.rs

1use crate::core::catalog::Catalog;
2use crate::db::database::Database;
3use crate::error::MyResult;
4use crate::regex;
5use regex::{Regex, RegexBuilder};
6use rustyline::completion::Completer;
7use rustyline::config::Configurer;
8use rustyline::highlight::Highlighter;
9use rustyline::hint::Hinter;
10use rustyline::history::DefaultHistory;
11use rustyline::line_buffer::LineBuffer;
12use rustyline::validate::{ValidationContext, ValidationResult, Validator};
13use rustyline::{Changeset, CompletionType, Context, Editor, Helper};
14use std::rc::Rc;
15
16pub fn create_editor(database: Rc<Database>, catalog: Rc<Catalog>) -> MyResult<CommandEditor> {
17    let mut editor = CommandEditor::new()?;
18    let helper = CommandHelper::new(database, catalog)?;
19    editor.set_helper(Some(helper));
20    editor.set_completion_type(CompletionType::List);
21    Ok(editor)
22}
23
24pub type CommandEditor<'a> = Editor<CommandHelper<'a>, DefaultHistory>;
25
26pub struct CommandHelper<'a> {
27    database: Rc<Database<'a>>,
28    catalog: Rc<Catalog>,
29    batch: Regex,
30}
31
32impl<'a> CommandHelper<'a> {
33    pub fn new(database: Rc<Database<'a>>, catalog: Rc<Catalog>) -> MyResult<Self> {
34        let batch = database.get_batch();
35        let batch = Self::build_regex(batch)?;
36        let helper = Self { database, catalog, batch };
37        Ok(helper)
38    }
39
40    fn build_regex(batch: &str) -> MyResult<Regex> {
41        let starting = regex!(r"^\w");
42        let batch = batch.trim();
43        let prefix = if starting.is_match(batch) { r"\b" } else { "" };
44        let batch = format!("{}{}$", prefix, batch);
45        let batch = RegexBuilder::new(&batch).case_insensitive(true).build()?;
46        Ok(batch)
47    }
48
49    fn validate_input(input: &str, batch: &Regex) -> ValidationResult {
50        let input = input.trim_start_matches(char::is_whitespace);
51        let input = input.trim_end_matches(is_whitespace_not_newline);
52        if let Some(':') = input.chars().next() {
53            ValidationResult::Valid(None)
54        } else {
55            match input.chars().last() {
56                Some('\n') => ValidationResult::Valid(None),
57                Some('\r') => ValidationResult::Valid(None),
58                _ => if batch.is_match(input) {
59                    ValidationResult::Valid(None)
60                } else {
61                    ValidationResult::Incomplete
62                }
63            }
64        }
65    }
66}
67
68fn is_whitespace_not_newline(c: char) -> bool {
69    c.is_whitespace() && c != '\n' && c != '\r'
70}
71
72impl<'a> Helper for CommandHelper<'a> {
73}
74
75impl<'a> Completer for CommandHelper<'a> {
76    type Candidate = String;
77
78    fn complete(
79        &self,
80        line: &str,
81        pos: usize,
82        _context: &Context<'_>,
83    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
84        let context = self.database.query_context();
85        let result = self.catalog.complete_line(context, line, pos);
86        Ok(result)
87    }
88
89    fn update(
90        &self,
91        line: &mut LineBuffer,
92        start: usize,
93        elected: &str,
94        changes: &mut Changeset,
95    ) {
96        let end = line.pos();
97        line.replace(start..end, elected, changes);
98    }
99}
100
101impl<'a> Highlighter for CommandHelper<'a> {
102}
103
104impl<'a> Hinter for CommandHelper<'a> {
105    type Hint = String;
106}
107
108impl<'a> Validator for CommandHelper<'a> {
109    fn validate(&self, context: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
110        let input = context.input();
111        let result = Self::validate_input(input, &self.batch);
112        Ok(result)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use crate::core::editor::CommandHelper;
119    use crate::error::MyResult;
120    use pretty_assertions::assert_eq;
121    use rustyline::validate::ValidationResult;
122
123    #[test]
124    fn test_empty_input_is_incomplete() -> MyResult<()> {
125        let batch = CommandHelper::build_regex(";")?;
126        let result = CommandHelper::validate_input("", &batch);
127        assert_eq!(matches!(result, ValidationResult::Incomplete), true);
128        Ok(())
129    }
130
131    #[test]
132    fn test_input_with_final_character_is_incomplete() -> MyResult<()> {
133        let batch = CommandHelper::build_regex(";")?;
134        let result = CommandHelper::validate_input("AAA BBB\nCCC DDD", &batch);
135        assert_eq!(matches!(result, ValidationResult::Incomplete), true);
136        Ok(())
137    }
138
139    #[test]
140    fn test_input_with_internal_semicolon_is_incomplete() -> MyResult<()> {
141        let batch = CommandHelper::build_regex(";")?;
142        let result = CommandHelper::validate_input("AAA BBB;\nCCC DDD", &batch);
143        assert_eq!(matches!(result, ValidationResult::Incomplete), true);
144        Ok(())
145    }
146
147    #[test]
148    fn test_input_with_final_semicolon_is_valid() -> MyResult<()> {
149        let batch = CommandHelper::build_regex(";")?;
150        let result = CommandHelper::validate_input("AAA BBB\nCCC DDD;  ", &batch);
151        assert_eq!(matches!(result, ValidationResult::Valid(None)), true);
152        Ok(())
153    }
154
155    #[test]
156    fn test_input_with_internal_keyword_is_incomplete() -> MyResult<()> {
157        let batch = CommandHelper::build_regex("Go")?;
158        let result = CommandHelper::validate_input("AAA BBB GO\nCCC DDD", &batch);
159        assert_eq!(matches!(result, ValidationResult::Incomplete), true);
160        Ok(())
161    }
162
163    #[test]
164    fn test_input_with_final_keyword_is_incomplete() -> MyResult<()> {
165        let batch = CommandHelper::build_regex("Go")?;
166        let result = CommandHelper::validate_input("AAA BBB\nCCC DDD XGO  ", &batch);
167        assert_eq!(matches!(result, ValidationResult::Incomplete), true);
168        Ok(())
169    }
170
171    #[test]
172    fn test_input_with_final_keyword_is_valid() -> MyResult<()> {
173        let batch = CommandHelper::build_regex("Go")?;
174        let result = CommandHelper::validate_input("AAA BBB\nCCC DDD GO  ", &batch);
175        assert_eq!(matches!(result, ValidationResult::Valid(None)), true);
176        Ok(())
177    }
178
179    #[test]
180    fn test_input_with_final_newline_is_valid() -> MyResult<()> {
181        let batch = CommandHelper::build_regex(";")?;
182        let result = CommandHelper::validate_input("AAA BBB\nCCC DDD\n  ", &batch);
183        assert_eq!(matches!(result, ValidationResult::Valid(None)), true);
184        Ok(())
185    }
186}