1use super::{CompletionContext, ReplResult};
7use std::collections::HashSet;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct CompletionSuggestion {
12 pub text: String,
14 pub display: String,
16 pub description: Option<String>,
18 pub completion_type: CompletionType,
20 pub priority: u8,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum CompletionType {
27 Keyword,
29 MetaCommand,
31 Table,
33 Column,
35 Keyspace,
37 Function,
39 DataType,
41 FilePath,
43 ConfigOption,
45}
46
47pub struct CompletionEngine {
49 cql_keywords: HashSet<String>,
51 meta_commands: HashSet<String>,
53 cql_functions: HashSet<String>,
55 data_types: HashSet<String>,
57 config_options: HashSet<String>,
59}
60
61impl CompletionEngine {
62 pub fn new() -> Self {
64 let mut engine = Self {
65 cql_keywords: HashSet::new(),
66 meta_commands: HashSet::new(),
67 cql_functions: HashSet::new(),
68 data_types: HashSet::new(),
69 config_options: HashSet::new(),
70 };
71
72 engine.initialize_static_completions();
73 engine
74 }
75
76 fn initialize_static_completions(&mut self) {
78 let keywords = [
80 "SELECT",
81 "INSERT",
82 "UPDATE",
83 "DELETE",
84 "TRUNCATE",
85 "CREATE",
86 "ALTER",
87 "DROP",
88 "USE",
89 "DESCRIBE",
90 "FROM",
91 "WHERE",
92 "ORDER",
93 "GROUP",
94 "HAVING",
95 "BY",
96 "ASC",
97 "DESC",
98 "LIMIT",
99 "OFFSET",
100 "AND",
101 "OR",
102 "NOT",
103 "IN",
104 "LIKE",
105 "IS",
106 "NULL",
107 "PRIMARY",
108 "KEY",
109 "CLUSTERING",
110 "INDEX",
111 "TABLE",
112 "KEYSPACE",
113 "TYPE",
114 "FUNCTION",
115 "IF",
116 "EXISTS",
117 "WITH",
118 "OPTIONS",
119 "ALLOW",
120 "FILTERING",
121 "TOKEN",
122 "COUNT",
123 "SUM",
124 "AVG",
125 "MIN",
126 "MAX",
127 "DISTINCT",
128 "AS",
129 "CAST",
130 "TTL",
131 "WRITETIME",
132 "BATCH",
133 "BEGIN",
134 "UNLOGGED",
135 "COUNTER",
136 "STATIC",
137 "FROZEN",
138 "TUPLE",
139 "MAP",
140 "SET",
141 "LIST",
142 ];
143
144 for keyword in &keywords {
145 self.cql_keywords.insert(keyword.to_string());
146 }
147
148 let meta_commands = [
150 ":help",
151 ":quit",
152 ":exit",
153 ":q",
154 ":clear",
155 ":cls",
156 ":tables",
157 ":list",
158 ":describe",
159 ":desc",
160 ":use",
161 ":config",
162 ":show",
163 ":set",
164 ":history",
165 ":timing",
166 ":source",
167 ":load",
168 ":keyspaces",
169 ":info",
170 ":schema",
171 ];
172
173 for cmd in &meta_commands {
174 self.meta_commands.insert(cmd.to_string());
175 }
176
177 let functions = [
179 "now",
180 "uuid",
181 "timeuuid",
182 "dateof",
183 "unixTimestampOf",
184 "toDate",
185 "toTimestamp",
186 "toUnixTimestamp",
187 "minTimeuuid",
188 "maxTimeuuid",
189 "count",
190 "sum",
191 "avg",
192 "min",
193 "max",
194 "writetime",
195 "ttl",
196 "token",
197 "cast",
198 "toJson",
199 "fromJson",
200 "blobasbigint",
201 "blobAsInt",
202 "blobAsText",
203 "bigintAsBlob",
204 "intAsBlob",
205 "textAsBlob",
206 ];
207
208 for func in &functions {
209 self.cql_functions.insert(func.to_string());
210 }
211
212 let data_types = [
214 "ascii",
215 "bigint",
216 "blob",
217 "boolean",
218 "counter",
219 "date",
220 "decimal",
221 "double",
222 "duration",
223 "float",
224 "inet",
225 "int",
226 "smallint",
227 "text",
228 "time",
229 "timestamp",
230 "timeuuid",
231 "tinyint",
232 "uuid",
233 "varchar",
234 "varint",
235 "map",
236 "set",
237 "list",
238 "tuple",
239 "frozen",
240 ];
241
242 for dtype in &data_types {
243 self.data_types.insert(dtype.to_string());
244 }
245
246 let config_options = [
248 "output_format",
249 "page_size",
250 "show_timing",
251 "enable_paging",
252 "enable_colors",
253 "data_dir",
254 "keyspace",
255 "prompt",
256 "prompt_continuation",
257 "max_history_size",
258 ];
259
260 for option in &config_options {
261 self.config_options.insert(option.to_string());
262 }
263 }
264
265 pub fn get_completions(
267 &self,
268 context: &CompletionContext,
269 ) -> ReplResult<Vec<CompletionSuggestion>> {
270 let mut suggestions = Vec::new();
271
272 let analysis = self.analyze_input(&context.line, context.pos);
274
275 match analysis.completion_context {
276 InputContext::MetaCommand => {
277 suggestions.extend(self.complete_meta_commands(&analysis.current_word));
278 }
279 InputContext::CqlKeyword => {
280 suggestions.extend(self.complete_cql_keywords(&analysis.current_word));
281 suggestions.extend(self.complete_functions(&analysis.current_word));
282 }
283 InputContext::TableName => {
284 suggestions
285 .extend(self.complete_table_names(&analysis.current_word, &context.tables));
286 }
287 InputContext::ColumnName => {
288 suggestions
289 .extend(self.complete_column_names(&analysis.current_word, &context.tables));
290 }
291 InputContext::KeyspaceName => {
292 suggestions.extend(
293 self.complete_keyspace_names(&analysis.current_word, &context.keyspaces),
294 );
295 }
296 InputContext::DataType => {
297 suggestions.extend(self.complete_data_types(&analysis.current_word));
298 }
299 InputContext::ConfigOption => {
300 suggestions.extend(self.complete_config_options(&analysis.current_word));
301 }
302 InputContext::FilePath => {
303 suggestions.extend(self.complete_file_paths(&analysis.current_word));
304 }
305 InputContext::Unknown => {
306 suggestions.extend(self.complete_cql_keywords(&analysis.current_word));
308 suggestions.extend(self.complete_meta_commands(&analysis.current_word));
309 }
310 }
311
312 suggestions.sort_by(|a, b| {
314 b.priority
315 .cmp(&a.priority)
316 .then_with(|| a.text.len().cmp(&b.text.len()))
317 .then_with(|| a.text.cmp(&b.text))
318 });
319
320 suggestions.truncate(50);
322
323 Ok(suggestions)
324 }
325
326 fn analyze_input(&self, line: &str, pos: usize) -> InputAnalysis {
328 let safe_pos = pos.min(line.len());
329 let text_before_cursor = &line[..safe_pos];
330
331 let current_word = self.extract_current_word(text_before_cursor);
333
334 let context = if text_before_cursor.trim_start().starts_with(':') {
336 if text_before_cursor.contains(" config ") {
337 InputContext::ConfigOption
338 } else if text_before_cursor.contains(" use ") {
339 InputContext::KeyspaceName
340 } else if text_before_cursor.contains(" describe ")
341 || text_before_cursor.contains(" desc ")
342 {
343 InputContext::TableName
344 } else if text_before_cursor.contains(" source ")
345 || text_before_cursor.contains(" load ")
346 {
347 InputContext::FilePath
348 } else {
349 InputContext::MetaCommand
350 }
351 } else {
352 self.analyze_cql_context(text_before_cursor)
353 };
354
355 InputAnalysis {
356 current_word,
357 completion_context: context,
358 line_before_cursor: text_before_cursor.to_string(),
359 }
360 }
361
362 fn analyze_cql_context(&self, text: &str) -> InputContext {
364 let upper_text = text.to_uppercase();
365 let words: Vec<&str> = upper_text.split_whitespace().collect();
366
367 if words.is_empty() {
368 return InputContext::CqlKeyword;
369 }
370
371 if let Some(last_word) = words.last() {
373 if words.len() >= 2 {
374 let prev_word = words[words.len() - 2];
375
376 if prev_word == "FROM"
378 || prev_word == "JOIN"
379 || prev_word == "UPDATE"
380 || prev_word == "INTO"
381 {
382 return InputContext::TableName;
383 }
384
385 if prev_word == "USE" {
387 return InputContext::KeyspaceName;
388 }
389
390 if words.len() >= 3 {
392 let context_phrase = format!("{} {}", words[words.len() - 3], prev_word);
393 if context_phrase.contains("PRIMARY KEY")
394 || context_phrase.contains("CLUSTERING KEY")
395 || prev_word == "TYPE"
396 {
397 return InputContext::DataType;
398 }
399 }
400 }
401
402 if last_word.ends_with('(') || text.contains('(') {
404 return InputContext::CqlKeyword; }
406 }
407
408 if upper_text.contains("SELECT") && !upper_text.contains("FROM") {
410 return InputContext::ColumnName;
411 }
412
413 InputContext::CqlKeyword
415 }
416
417 fn extract_current_word(&self, text: &str) -> String {
419 let chars: Vec<char> = text.chars().collect();
420 let mut start = chars.len();
421 let end = chars.len();
422
423 for i in (0..chars.len()).rev() {
425 let ch = chars[i];
426 if ch.is_whitespace() || ch == '(' || ch == ')' || ch == ',' || ch == ';' {
427 start = i + 1;
428 break;
429 }
430 if i == 0 {
431 start = 0;
432 break;
433 }
434 }
435
436 if start < end {
438 chars[start..end].iter().collect()
439 } else {
440 String::new()
441 }
442 }
443
444 fn complete_meta_commands(&self, prefix: &str) -> Vec<CompletionSuggestion> {
446 let mut suggestions = Vec::new();
447 let lower_prefix = prefix.to_lowercase();
448
449 for cmd in &self.meta_commands {
450 if cmd.to_lowercase().starts_with(&lower_prefix) {
451 let description = self.get_meta_command_description(cmd);
452 suggestions.push(CompletionSuggestion {
453 text: cmd.clone(),
454 display: cmd.clone(),
455 description: Some(description),
456 completion_type: CompletionType::MetaCommand,
457 priority: self.calculate_priority(cmd, prefix, 8),
458 });
459 }
460 }
461
462 suggestions
463 }
464
465 fn complete_cql_keywords(&self, prefix: &str) -> Vec<CompletionSuggestion> {
467 let mut suggestions = Vec::new();
468 let upper_prefix = prefix.to_uppercase();
469
470 for keyword in &self.cql_keywords {
471 if keyword.starts_with(&upper_prefix) {
472 suggestions.push(CompletionSuggestion {
473 text: keyword.clone(),
474 display: keyword.clone(),
475 description: None,
476 completion_type: CompletionType::Keyword,
477 priority: self.calculate_priority(keyword, prefix, 7),
478 });
479 }
480 }
481
482 suggestions
483 }
484
485 fn complete_functions(&self, prefix: &str) -> Vec<CompletionSuggestion> {
487 let mut suggestions = Vec::new();
488 let lower_prefix = prefix.to_lowercase();
489
490 for func in &self.cql_functions {
491 if func.to_lowercase().starts_with(&lower_prefix) {
492 suggestions.push(CompletionSuggestion {
493 text: format!("{}()", func),
494 display: format!("{}()", func),
495 description: Some(format!("CQL function: {}", func)),
496 completion_type: CompletionType::Function,
497 priority: self.calculate_priority(func, prefix, 6),
498 });
499 }
500 }
501
502 suggestions
503 }
504
505 fn complete_table_names(&self, prefix: &str, tables: &[String]) -> Vec<CompletionSuggestion> {
507 let mut suggestions = Vec::new();
508 let lower_prefix = prefix.to_lowercase();
509
510 for table in tables {
511 if table.to_lowercase().starts_with(&lower_prefix) {
512 suggestions.push(CompletionSuggestion {
513 text: table.clone(),
514 display: table.clone(),
515 description: Some("Table".to_string()),
516 completion_type: CompletionType::Table,
517 priority: self.calculate_priority(table, prefix, 9),
518 });
519 }
520 }
521
522 suggestions
523 }
524
525 fn complete_column_names(&self, prefix: &str, _tables: &[String]) -> Vec<CompletionSuggestion> {
527 let mut suggestions = Vec::new();
528 let lower_prefix = prefix.to_lowercase();
529
530 let common_columns = [
532 "id",
533 "name",
534 "email",
535 "created_at",
536 "updated_at",
537 "user_id",
538 "timestamp",
539 "value",
540 "data",
541 "type",
542 "status",
543 "description",
544 "title",
545 "content",
546 ];
547
548 for col in &common_columns {
549 if col.starts_with(&lower_prefix) {
550 suggestions.push(CompletionSuggestion {
551 text: col.to_string(),
552 display: col.to_string(),
553 description: Some("Column".to_string()),
554 completion_type: CompletionType::Column,
555 priority: self.calculate_priority(col, prefix, 5),
556 });
557 }
558 }
559
560 suggestions
561 }
562
563 fn complete_keyspace_names(
565 &self,
566 prefix: &str,
567 keyspaces: &[String],
568 ) -> Vec<CompletionSuggestion> {
569 let mut suggestions = Vec::new();
570 let lower_prefix = prefix.to_lowercase();
571
572 for keyspace in keyspaces {
573 if keyspace.to_lowercase().starts_with(&lower_prefix) {
574 suggestions.push(CompletionSuggestion {
575 text: keyspace.clone(),
576 display: keyspace.clone(),
577 description: Some("Keyspace".to_string()),
578 completion_type: CompletionType::Keyspace,
579 priority: self.calculate_priority(keyspace, prefix, 8),
580 });
581 }
582 }
583
584 suggestions
585 }
586
587 fn complete_data_types(&self, prefix: &str) -> Vec<CompletionSuggestion> {
589 let mut suggestions = Vec::new();
590 let lower_prefix = prefix.to_lowercase();
591
592 for dtype in &self.data_types {
593 if dtype.starts_with(&lower_prefix) {
594 suggestions.push(CompletionSuggestion {
595 text: dtype.clone(),
596 display: dtype.clone(),
597 description: Some("Data type".to_string()),
598 completion_type: CompletionType::DataType,
599 priority: self.calculate_priority(dtype, prefix, 6),
600 });
601 }
602 }
603
604 suggestions
605 }
606
607 fn complete_config_options(&self, prefix: &str) -> Vec<CompletionSuggestion> {
609 let mut suggestions = Vec::new();
610 let lower_prefix = prefix.to_lowercase();
611
612 for option in &self.config_options {
613 if option.starts_with(&lower_prefix) {
614 let description = self.get_config_option_description(option);
615 suggestions.push(CompletionSuggestion {
616 text: option.clone(),
617 display: option.clone(),
618 description: Some(description),
619 completion_type: CompletionType::ConfigOption,
620 priority: self.calculate_priority(option, prefix, 7),
621 });
622 }
623 }
624
625 suggestions
626 }
627
628 fn complete_file_paths(&self, prefix: &str) -> Vec<CompletionSuggestion> {
630 let mut suggestions = Vec::new();
631
632 if prefix.ends_with('/') || prefix.is_empty() {
634 suggestions.push(CompletionSuggestion {
636 text: "./".to_string(),
637 display: "./".to_string(),
638 description: Some("Current directory".to_string()),
639 completion_type: CompletionType::FilePath,
640 priority: 5,
641 });
642 }
643
644 suggestions
645 }
646
647 fn calculate_priority(&self, suggestion: &str, prefix: &str, base_priority: u8) -> u8 {
649 if prefix.is_empty() {
650 return base_priority;
651 }
652
653 let lower_suggestion = suggestion.to_lowercase();
654 let lower_prefix = prefix.to_lowercase();
655
656 if lower_suggestion == lower_prefix {
658 return base_priority + 3;
659 }
660
661 if lower_suggestion.starts_with(&lower_prefix) {
663 return base_priority + 2;
664 }
665
666 if lower_suggestion.contains(&lower_prefix) {
668 return base_priority + 1;
669 }
670
671 base_priority
672 }
673
674 fn get_meta_command_description(&self, cmd: &str) -> String {
676 match cmd {
677 ":help" => "Show help information".to_string(),
678 ":quit" | ":exit" | ":q" => "Exit the REPL".to_string(),
679 ":clear" | ":cls" => "Clear the screen".to_string(),
680 ":tables" | ":list" => "List all tables".to_string(),
681 ":describe" | ":desc" => "Describe an object".to_string(),
682 ":use" => "Switch to a keyspace".to_string(),
683 ":config" => "Show/set configuration".to_string(),
684 ":history" => "Show command history".to_string(),
685 ":timing" => "Toggle timing display".to_string(),
686 ":source" | ":load" => "Execute commands from file".to_string(),
687 ":keyspaces" => "List all keyspaces".to_string(),
688 ":info" => "Show object information".to_string(),
689 ":schema" => "Show schema information".to_string(),
690 _ => "Meta-command".to_string(),
691 }
692 }
693
694 fn get_config_option_description(&self, option: &str) -> String {
696 match option {
697 "output_format" => "Output format (table, csv, json, raw)".to_string(),
698 "page_size" => "Number of rows per page".to_string(),
699 "show_timing" => "Show query execution timing".to_string(),
700 "enable_paging" => "Enable result paging".to_string(),
701 "enable_colors" => "Enable colored output".to_string(),
702 "data_dir" => "Cassandra data directory path".to_string(),
703 "keyspace" => "Default keyspace".to_string(),
704 "prompt" => "REPL prompt string".to_string(),
705 "prompt_continuation" => "Multi-line prompt string".to_string(),
706 "max_history_size" => "Maximum history entries".to_string(),
707 _ => "Configuration option".to_string(),
708 }
709 }
710}
711
712#[derive(Debug)]
714struct InputAnalysis {
715 current_word: String,
717 completion_context: InputContext,
719 line_before_cursor: String,
721}
722
723#[derive(Debug, PartialEq)]
725enum InputContext {
726 MetaCommand,
727 CqlKeyword,
728 TableName,
729 ColumnName,
730 KeyspaceName,
731 DataType,
732 ConfigOption,
733 FilePath,
734 Unknown,
735}
736
737impl Default for CompletionEngine {
738 fn default() -> Self {
739 Self::new()
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use super::super::SessionState;
746 use super::*;
747
748 #[test]
749 fn test_meta_command_completion() {
750 let engine = CompletionEngine::new();
751 let context = CompletionContext {
752 line: ":he".to_string(),
753 pos: 3,
754 session_state: SessionState::Ready,
755 tables: vec![],
756 keyspaces: vec![],
757 };
758
759 let completions = engine.get_completions(&context).unwrap();
760 assert!(!completions.is_empty());
761 assert!(completions.iter().any(|c| c.text == ":help"));
762 }
763
764 #[test]
765 fn test_keyword_completion() {
766 let engine = CompletionEngine::new();
767 let context = CompletionContext {
768 line: "SEL".to_string(),
769 pos: 3,
770 session_state: SessionState::Ready,
771 tables: vec![],
772 keyspaces: vec![],
773 };
774
775 let completions = engine.get_completions(&context).unwrap();
776 assert!(!completions.is_empty());
777 assert!(completions.iter().any(|c| c.text == "SELECT"));
778 }
779
780 #[test]
781 fn test_table_completion() {
782 let engine = CompletionEngine::new();
783 let tables = vec!["users".to_string(), "orders".to_string()];
784 let context = CompletionContext {
785 line: "SELECT * FROM us".to_string(),
786 pos: 16,
787 session_state: SessionState::Ready,
788 tables,
789 keyspaces: vec![],
790 };
791
792 let completions = engine.get_completions(&context).unwrap();
793 assert!(completions.iter().any(|c| c.text == "users"));
794 }
795
796 #[test]
797 fn test_current_word_extraction() {
798 let engine = CompletionEngine::new();
799
800 assert_eq!(engine.extract_current_word("SELECT * FROM us"), "us");
801 assert_eq!(engine.extract_current_word(":he"), ":he");
802 assert_eq!(engine.extract_current_word("SELECT count("), "");
804 assert_eq!(
805 engine.extract_current_word("SELECT * FROM users WHERE name = 'jo"),
806 "'jo"
807 );
808 }
809}