1use std::collections::HashSet;
2
3#[derive(Debug, Clone)]
6pub struct CompletionManager {
7 suggestions: Vec<String>,
9
10 current_index: usize,
12
13 last_query: String,
15
16 last_cursor_pos: usize,
18
19 table_names: HashSet<String>,
21
22 column_names: std::collections::HashMap<String, Vec<String>>,
24}
25
26impl CompletionManager {
27 pub fn new() -> Self {
28 Self {
29 suggestions: Vec::new(),
30 current_index: 0,
31 last_query: String::new(),
32 last_cursor_pos: 0,
33 table_names: HashSet::new(),
34 column_names: std::collections::HashMap::new(),
35 }
36 }
37
38 pub fn reset(&mut self) {
40 self.suggestions.clear();
41 self.current_index = 0;
42 self.last_query.clear();
43 self.last_cursor_pos = 0;
44 }
45
46 pub fn has_suggestions(&self) -> bool {
48 !self.suggestions.is_empty()
49 }
50
51 pub fn suggestions(&self) -> &[String] {
53 &self.suggestions
54 }
55
56 pub fn current_index(&self) -> usize {
58 self.current_index
59 }
60
61 pub fn current_suggestion(&self) -> Option<&str> {
63 if self.suggestions.is_empty() {
64 None
65 } else {
66 Some(&self.suggestions[self.current_index])
67 }
68 }
69
70 pub fn next_suggestion(&mut self) {
72 if !self.suggestions.is_empty() {
73 self.current_index = (self.current_index + 1) % self.suggestions.len();
74 }
75 }
76
77 pub fn prev_suggestion(&mut self) {
79 if !self.suggestions.is_empty() {
80 self.current_index = if self.current_index == 0 {
81 self.suggestions.len() - 1
82 } else {
83 self.current_index - 1
84 };
85 }
86 }
87
88 pub fn set_table_names(&mut self, tables: HashSet<String>) {
90 self.table_names = tables;
91 }
92
93 pub fn set_column_names(&mut self, table: String, columns: Vec<String>) {
95 self.column_names.insert(table, columns);
96 }
97
98 pub fn generate_suggestions(
100 &mut self,
101 query: &str,
102 cursor_pos: usize,
103 partial_word: &str,
104 ) -> bool {
105 if query == self.last_query && cursor_pos == self.last_cursor_pos {
107 return self.has_suggestions();
108 }
109
110 self.last_query = query.to_string();
112 self.last_cursor_pos = cursor_pos;
113 self.suggestions.clear();
114 self.current_index = 0;
115
116 let context = self.analyze_context(query, cursor_pos);
118
119 match context {
121 CompletionContext::TableName => {
122 self.suggest_tables(partial_word);
123 }
124 CompletionContext::ColumnName(table) => {
125 self.suggest_columns(&table, partial_word);
126 }
127 CompletionContext::Keyword => {
128 self.suggest_keywords(partial_word);
129 }
130 CompletionContext::Unknown => {
131 self.suggest_keywords(partial_word);
133 self.suggest_tables(partial_word);
134 }
135 }
136
137 self.has_suggestions()
138 }
139
140 fn analyze_context(&self, query: &str, cursor_pos: usize) -> CompletionContext {
142 let before_cursor = &query[..cursor_pos.min(query.len())];
143 let lower = before_cursor.to_lowercase();
144
145 if lower.ends_with("from ") || lower.ends_with("join ") {
147 CompletionContext::TableName
148 } else if lower.contains("select ") && !lower.contains(" from") {
149 if let Some(table) = self.extract_table_from_query(query) {
152 CompletionContext::ColumnName(table)
153 } else {
154 CompletionContext::Keyword
155 }
156 } else if lower.ends_with("where ") || lower.ends_with("and ") || lower.ends_with("or ") {
157 if let Some(table) = self.extract_table_from_query(query) {
159 CompletionContext::ColumnName(table)
160 } else {
161 CompletionContext::Unknown
162 }
163 } else {
164 CompletionContext::Keyword
165 }
166 }
167
168 fn extract_table_from_query(&self, query: &str) -> Option<String> {
170 let lower = query.to_lowercase();
171 if let Some(from_pos) = lower.find(" from ") {
172 let after_from = &query[from_pos + 6..];
173 let table_name = after_from
174 .split_whitespace()
175 .next()
176 .map(|s| s.trim_end_matches(',').trim_end_matches(';'))?;
177
178 if self.table_names.contains(table_name) {
180 Some(table_name.to_string())
181 } else {
182 None
183 }
184 } else {
185 None
186 }
187 }
188
189 fn suggest_tables(&mut self, partial: &str) {
191 let lower_partial = partial.to_lowercase();
192 let mut suggestions: Vec<String> = self
193 .table_names
194 .iter()
195 .filter(|t| t.to_lowercase().starts_with(&lower_partial))
196 .cloned()
197 .collect();
198
199 suggestions.sort();
200 self.suggestions = suggestions;
201 }
202
203 fn suggest_columns(&mut self, table: &str, partial: &str) {
205 if let Some(columns) = self.column_names.get(table) {
206 let lower_partial = partial.to_lowercase();
207 let mut suggestions: Vec<String> = columns
208 .iter()
209 .filter(|c| c.to_lowercase().starts_with(&lower_partial))
210 .cloned()
211 .collect();
212
213 suggestions.sort();
214 self.suggestions = suggestions;
215 }
216 }
217
218 fn suggest_keywords(&mut self, partial: &str) {
220 const SQL_KEYWORDS: &[&str] = &[
221 "SELECT",
222 "FROM",
223 "WHERE",
224 "GROUP BY",
225 "ORDER BY",
226 "HAVING",
227 "JOIN",
228 "LEFT JOIN",
229 "RIGHT JOIN",
230 "INNER JOIN",
231 "OUTER JOIN",
232 "ON",
233 "AND",
234 "OR",
235 "NOT",
236 "IN",
237 "EXISTS",
238 "BETWEEN",
239 "LIKE",
240 "AS",
241 "DISTINCT",
242 "COUNT",
243 "SUM",
244 "AVG",
245 "MIN",
246 "MAX",
247 "INSERT",
248 "UPDATE",
249 "DELETE",
250 "CREATE",
251 "DROP",
252 "ALTER",
253 "TABLE",
254 "INDEX",
255 "VIEW",
256 "UNION",
257 "ALL",
258 "LIMIT",
259 "OFFSET",
260 ];
261
262 let lower_partial = partial.to_lowercase();
263 let mut suggestions: Vec<String> = SQL_KEYWORDS
264 .iter()
265 .filter(|k| k.to_lowercase().starts_with(&lower_partial))
266 .map(|k| k.to_string())
267 .collect();
268
269 suggestions.sort();
270 self.suggestions = suggestions;
271 }
272}
273
274#[derive(Debug, Clone)]
276enum CompletionContext {
277 TableName,
278 ColumnName(String), Keyword,
280 Unknown,
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_completion_manager_creation() {
289 let cm = CompletionManager::new();
290 assert!(!cm.has_suggestions());
291 assert_eq!(cm.current_index(), 0);
292 }
293
294 #[test]
295 fn test_suggestion_navigation() {
296 let mut cm = CompletionManager::new();
297 cm.suggestions = vec![
298 "SELECT".to_string(),
299 "FROM".to_string(),
300 "WHERE".to_string(),
301 ];
302
303 assert_eq!(cm.current_suggestion(), Some("SELECT"));
304
305 cm.next_suggestion();
306 assert_eq!(cm.current_suggestion(), Some("FROM"));
307
308 cm.next_suggestion();
309 assert_eq!(cm.current_suggestion(), Some("WHERE"));
310
311 cm.next_suggestion();
312 assert_eq!(cm.current_suggestion(), Some("SELECT")); cm.prev_suggestion();
315 assert_eq!(cm.current_suggestion(), Some("WHERE"));
316 }
317
318 #[test]
319 fn test_keyword_suggestions() {
320 let mut cm = CompletionManager::new();
321 cm.generate_suggestions("SEL", 3, "SEL");
322
323 assert!(cm.has_suggestions());
324 assert!(cm.suggestions().contains(&"SELECT".to_string()));
325 }
326
327 #[test]
328 fn test_table_suggestions() {
329 let mut cm = CompletionManager::new();
330 let mut tables = HashSet::new();
331 tables.insert("users".to_string());
332 tables.insert("orders".to_string());
333 tables.insert("products".to_string());
334 cm.set_table_names(tables);
335
336 cm.generate_suggestions("SELECT * FROM ", 14, "");
339
340 assert!(cm.has_suggestions());
341 assert!(cm.suggestions().contains(&"users".to_string()));
342 }
343}