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