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}