diff --git a/canvas/examples/suggestions2.rs b/canvas/examples/suggestions2.rs new file mode 100644 index 0000000..3751296 --- /dev/null +++ b/canvas/examples/suggestions2.rs @@ -0,0 +1,1234 @@ +// examples/suggestions2.rs +//! Demonstrates automatic cursor management + INTELLIGENT SUGGESTIONS +//! +//! This example REQUIRES the `cursor-style` feature to compile. +//! +//! Run with: +//! cargo run --example suggestions2 --features "gui,cursor-style,suggestions" + +// REQUIRE cursor-style feature - example won't compile without it +#[cfg(not(feature = "cursor-style"))] +compile_error!( + "This example requires the 'cursor-style' feature. \ + Run with: cargo run --example suggestions2 --features \"gui,cursor-style,suggestions\"" +); + +use std::io; +use std::collections::HashMap; +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, + }, + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + }, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, Terminal, +}; + +use canvas::{ + canvas::{ + gui::render_canvas_default, + modes::{AppMode, ModeManager, HighlightState}, + CursorManager, // This import only exists when cursor-style feature is enabled + }, + suggestions::gui::render_suggestions_dropdown, + DataProvider, FormEditor, SuggestionsProvider, SuggestionItem, +}; + +use async_trait::async_trait; +use anyhow::Result; + +// Enhanced FormEditor that demonstrates automatic cursor management + SUGGESTIONS +struct AutoCursorFormEditor { + editor: FormEditor, + has_unsaved_changes: bool, + debug_message: String, + command_buffer: String, // For multi-key vim commands like "gg" +} + +impl AutoCursorFormEditor { + fn new(data_provider: D) -> Self { + Self { + editor: FormEditor::new(data_provider), + has_unsaved_changes: false, + debug_message: "🎯 Automatic Cursor + Suggestions Demo - cursor-style feature enabled!".to_string(), + command_buffer: String::new(), + } + } + + // === COMMAND BUFFER HANDLING === + + fn clear_command_buffer(&mut self) { + self.command_buffer.clear(); + } + + fn add_to_command_buffer(&mut self, ch: char) { + self.command_buffer.push(ch); + } + + fn get_command_buffer(&self) -> &str { + &self.command_buffer + } + + fn has_pending_command(&self) -> bool { + !self.command_buffer.is_empty() + } + + // === VISUAL/HIGHLIGHT MODE SUPPORT === + + fn enter_visual_mode(&mut self) { + // Use the library method instead of manual state setting + self.editor.enter_highlight_mode(); + self.debug_message = "πŸ”₯ VISUAL MODE - Cursor: Blinking Block β–ˆ".to_string(); + } + + fn enter_visual_line_mode(&mut self) { + // Use the library method instead of manual state setting + self.editor.enter_highlight_line_mode(); + self.debug_message = "πŸ”₯ VISUAL LINE MODE - Cursor: Blinking Block β–ˆ".to_string(); + } + + fn exit_visual_mode(&mut self) { + // Use the library method + self.editor.exit_highlight_mode(); + self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); + } + + fn update_visual_selection(&mut self) { + if self.editor.is_highlight_mode() { + use canvas::canvas::state::SelectionState; + match self.editor.selection_state() { + SelectionState::Characterwise { anchor } => { + self.debug_message = format!( + "🎯 Visual selection: anchor=({},{}) current=({},{}) - Cursor: Blinking Block β–ˆ", + anchor.0, anchor.1, + self.editor.current_field(), + self.editor.cursor_position() + ); + } + SelectionState::Linewise { anchor_field } => { + self.debug_message = format!( + "🎯 Visual LINE selection: anchor={} current={} - Cursor: Blinking Block β–ˆ", + anchor_field, + self.editor.current_field() + ); + } + _ => {} + } + } + } + + // === ENHANCED MOVEMENT WITH VISUAL UPDATES === + + fn move_left(&mut self) { + self.editor.move_left(); + self.update_visual_selection(); + } + + fn move_right(&mut self) { + self.editor.move_right(); + self.update_visual_selection(); + } + + fn move_up(&mut self) { + let _ = self.editor.move_up(); + self.update_visual_selection(); + } + + fn move_down(&mut self) { + let _ = self.editor.move_down(); + self.update_visual_selection(); + } + + fn move_word_next(&mut self) { + self.editor.move_word_next(); + self.update_visual_selection(); + } + + fn move_word_prev(&mut self) { + self.editor.move_word_prev(); + self.update_visual_selection(); + } + + fn move_word_end(&mut self) { + self.editor.move_word_end(); + self.update_visual_selection(); + } + + fn move_word_end_prev(&mut self) { + self.editor.move_word_end_prev(); + self.update_visual_selection(); + } + + fn move_line_start(&mut self) { + self.editor.move_line_start(); + self.update_visual_selection(); + } + + fn move_line_end(&mut self) { + self.editor.move_line_end(); + self.update_visual_selection(); + } + + fn move_first_line(&mut self) { + self.editor.move_first_line(); + self.update_visual_selection(); + } + + fn move_last_line(&mut self) { + self.editor.move_last_line(); + self.update_visual_selection(); + } + + fn prev_field(&mut self) { + let _ = self.editor.prev_field(); + self.update_visual_selection(); + } + + fn next_field(&mut self) { + let _ = self.editor.next_field(); + self.update_visual_selection(); + } + + // === DELETE OPERATIONS === + + fn delete_backward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_backward(); + if result.is_ok() { + self.has_unsaved_changes = true; + self.debug_message = "⌫ Deleted character backward".to_string(); + } + Ok(result?) + } + + fn delete_forward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_forward(); + if result.is_ok() { + self.has_unsaved_changes = true; + self.debug_message = "⌦ Deleted character forward".to_string(); + } + Ok(result?) + } + + // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === + + fn enter_edit_mode(&mut self) { + self.editor.enter_edit_mode(); // 🎯 Library automatically sets cursor to bar | + self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar |".to_string(); + } + + fn enter_append_mode(&mut self) { + self.editor.enter_append_mode(); // 🎯 Library automatically positions cursor and sets mode + self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar |".to_string(); + } + + fn exit_edit_mode(&mut self) { + let _ = self.editor.exit_edit_mode(); // 🎯 Library automatically sets cursor to block β–ˆ + self.exit_visual_mode(); + self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); + } + + fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { + let result = self.editor.insert_char(ch); + if result.is_ok() { + self.has_unsaved_changes = true; + } + Ok(result?) + } + + // === SUGGESTIONS SUPPORT === + + async fn trigger_suggestions(&mut self, provider: &mut A) -> anyhow::Result<()> + where + A: SuggestionsProvider, + { + self.editor.trigger_suggestions(provider).await + } + + fn suggestions_next(&mut self) { + self.editor.suggestions_next(); + } + + fn apply_suggestion(&mut self) -> Option { + self.editor.apply_suggestion() + } + + fn is_suggestions_active(&self) -> bool { + self.editor.is_suggestions_active() + } + + fn suggestions(&self) -> &[SuggestionItem] { + self.editor.suggestions() + } + + // === MANUAL CURSOR OVERRIDE DEMONSTRATION === + + /// Demonstrate manual cursor control (for advanced users) + fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> { + // Users can still manually control cursor if needed + CursorManager::update_for_mode(AppMode::Command)?; + self.debug_message = "πŸ”§ Manual override: Command cursor _".to_string(); + Ok(()) + } + + fn restore_automatic_cursor(&mut self) -> std::io::Result<()> { + // Restore automatic cursor based on current mode + CursorManager::update_for_mode(self.editor.mode())?; + self.debug_message = "🎯 Restored automatic cursor management".to_string(); + Ok(()) + } + + // === DELEGATE TO ORIGINAL EDITOR === + + fn current_field(&self) -> usize { + self.editor.current_field() + } + + fn cursor_position(&self) -> usize { + self.editor.cursor_position() + } + + fn mode(&self) -> AppMode { + self.editor.mode() + } + + fn current_text(&self) -> &str { + self.editor.current_text() + } + + fn data_provider(&self) -> &D { + self.editor.data_provider() + } + + fn ui_state(&self) -> &canvas::EditorState { + self.editor.ui_state() + } + + fn set_mode(&mut self, mode: AppMode) { + self.editor.set_mode(mode); // 🎯 Library automatically updates cursor + if mode != AppMode::Highlight { + self.exit_visual_mode(); + } + } + + // === STATUS AND DEBUG === + + fn set_debug_message(&mut self, msg: String) { + self.debug_message = msg; + } + + fn debug_message(&self) -> &str { + &self.debug_message + } + + fn has_unsaved_changes(&self) -> bool { + self.has_unsaved_changes + } +} + +// Demo form data with interesting text for cursor demonstration + SUGGESTIONS +struct CursorDemoData { + fields: Vec<(String, String)>, +} + +impl CursorDemoData { + fn new() -> Self { + Self { + fields: vec![ + ("πŸ‘€ Name".to_string(), "John-Paul McDonald".to_string()), + ("πŸ“§ Email".to_string(), "user@exa".to_string()), // Partial for suggestions demo + ("πŸ“± Phone".to_string(), "+1 (555) 123-4567".to_string()), + ("🏒 Company".to_string(), "tech".to_string()), // Partial for suggestions demo + ("🏠 Address".to_string(), "123 Main St, Apt 4B".to_string()), + ("πŸ’» Skills".to_string(), "rust,py".to_string()), // Multi-tag demo + ("πŸ“ Notes".to_string(), "Watch the cursor change! Normal=β–ˆ Insert=| Visual=blinkingβ–ˆ".to_string()), + ("🎯 Cursor + Suggestions".to_string(), "Try: Email (user@exa), Company (tech), Skills (rust,py) + Tab for smart suggestions!".to_string()), + ], + } + } +} + +impl DataProvider for CursorDemoData { + fn field_count(&self) -> usize { + self.fields.len() + } + + fn field_name(&self, index: usize) -> &str { + &self.fields[index].0 + } + + fn field_value(&self, index: usize) -> &str { + &self.fields[index].1 + } + + fn set_field_value(&mut self, index: usize, value: String) { + self.fields[index].1 = value; + } + + fn supports_suggestions(&self, field_index: usize) -> bool { + // Enable suggestions for email, company, and skills fields + matches!(field_index, 1 | 3 | 5) + } + + fn display_value(&self, _index: usize) -> Option<&str> { + None + } +} + +// =================================================================== +// INTELLIGENT SUGGESTIONS PROVIDER - More logical behavior +// =================================================================== + +struct PowerfulSuggestionsProvider { + // Cache for performance (realistic behavior) + email_cache: HashMap>, + company_cache: HashMap>, +} + +impl PowerfulSuggestionsProvider { + fn new() -> Self { + Self { + email_cache: HashMap::new(), + company_cache: HashMap::new(), + } + } +} + +#[async_trait] +impl SuggestionsProvider for PowerfulSuggestionsProvider { + async fn fetch_suggestions(&mut self, field_index: usize, query: &str) -> Result> { + // Minimum query length check (realistic behavior) + if query.trim().is_empty() { + return Ok(Vec::new()); + } + + // Simulate realistic network delay based on field type + let delay_ms = match field_index { + 1 => 150, // Email: fast local lookup + 3 => 300, // Company: API call + 5 => 100, // Skills: local data + _ => 50, + }; + tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; + + // Route to appropriate suggestion logic + match field_index { + 1 => self.get_email_suggestions(query).await, + 3 => self.get_company_suggestions(query).await, + 5 => self.get_skills_suggestions(query).await, + _ => Ok(Vec::new()), + } + } +} + +impl PowerfulSuggestionsProvider { + /// Smart email suggestions with domain completion and validation + async fn get_email_suggestions(&mut self, query: &str) -> Result> { + // Check cache first (realistic performance optimization) + if let Some(cached) = self.email_cache.get(query) { + return Ok(cached.clone()); + } + + let suggestions = if let Some(at_pos) = query.find('@') { + // Domain completion mode: "user@exa" β†’ complete domains + let username = &query[..at_pos]; + let domain_part = &query[at_pos + 1..]; + + // Validate username (basic email validation) + if username.is_empty() || username.len() > 64 { + return Ok(Vec::new()); + } + + let domains = vec![ + ("example.com", "Example Domain - For testing"), + ("gmail.com", "Gmail - Google's email service"), + ("outlook.com", "Outlook - Microsoft email"), + ("yahoo.com", "Yahoo Mail - Classic email"), + ("hotmail.com", "Hotmail - Microsoft legacy"), + ("company.com", "Company Email - Corporate"), + ("university.edu", "University - Educational"), + ("protonmail.com", "ProtonMail - Secure email"), + ("icloud.com", "iCloud - Apple's email service"), + ("fastmail.com", "FastMail - Premium email"), + ]; + + // Smart filtering: exact prefix match first, then contains + let mut exact_matches = Vec::new(); + let mut fuzzy_matches = Vec::new(); + + for (domain, desc) in domains { + if domain.starts_with(domain_part) { + exact_matches.push((domain, desc)); + } else if domain.contains(domain_part) && !domain_part.is_empty() { + fuzzy_matches.push((domain, desc)); + } + } + + // Combine results: exact matches first, then fuzzy + let mut all_matches = exact_matches; + all_matches.extend(fuzzy_matches); + + all_matches.into_iter() + .take(8) // Limit results (realistic UX) + .map(|(domain, desc)| { + let full_email = format!("{}@{}", username, domain); + SuggestionItem { + display_text: format!("{} - {}", full_email, desc), + value_to_store: full_email, + } + }) + .collect() + } else { + // Username mode: "user" β†’ suggest common email formats + if query.len() >= 2 && query.len() <= 32 { + vec![ + SuggestionItem { + display_text: format!("{}@gmail.com - Most popular email", query), + value_to_store: format!("{}@gmail.com", query), + }, + SuggestionItem { + display_text: format!("{}@outlook.com - Microsoft email", query), + value_to_store: format!("{}@outlook.com", query), + }, + SuggestionItem { + display_text: format!("{}@company.com - Work email", query), + value_to_store: format!("{}@company.com", query), + }, + ] + } else { + Vec::new() + } + }; + + // Cache the result (realistic performance) + self.email_cache.insert(query.to_string(), suggestions.clone()); + Ok(suggestions) + } + + /// Intelligent company suggestions with fuzzy matching and scoring + async fn get_company_suggestions(&mut self, query: &str) -> Result> { + // Check cache first + if let Some(cached) = self.company_cache.get(query) { + return Ok(cached.clone()); + } + + let companies = vec![ + ("Google", "Technology", "Search, Cloud, AI", 10), + ("Microsoft", "Technology", "Software, Cloud, Gaming", 10), + ("Apple", "Technology", "Consumer Electronics", 10), + ("Amazon", "E-commerce", "Cloud, Retail, Logistics", 10), + ("Meta", "Social Media", "Facebook, Instagram, WhatsApp", 9), + ("Tesla", "Automotive", "Electric Vehicles, Energy", 8), + ("Netflix", "Entertainment", "Streaming, Content", 8), + ("SpaceX", "Aerospace", "Space Exploration", 7), + ("Uber", "Transportation", "Ride-sharing, Delivery", 7), + ("Airbnb", "Hospitality", "Home Sharing", 7), + ("Stripe", "Fintech", "Payment Processing", 8), + ("Shopify", "E-commerce", "Online Store Platform", 7), + ("Slack", "Productivity", "Team Communication", 6), + ("Zoom", "Communication", "Video Conferencing", 6), + ("Figma", "Design", "Collaborative Design Tools", 5), + ("Tech Startup", "Technology", "Early Stage Company", 3), + ("Tech Corporation", "Technology", "Large Enterprise", 4), + ("Technology Solutions", "Technology", "Custom Software", 3), + ]; + + let query_lower = query.to_lowercase(); + + // Smart scoring algorithm + let mut scored_companies: Vec<_> = companies.iter() + .filter_map(|(name, category, desc, popularity)| { + let name_lower = name.to_lowercase(); + let category_lower = category.to_lowercase(); + let desc_lower = desc.to_lowercase(); + + let mut score = 0; + + // Exact name prefix (highest score) + if name_lower.starts_with(&query_lower) { + score += 100; + } + // Name contains query + else if name_lower.contains(&query_lower) { + score += 50; + } + // Category match + else if category_lower.contains(&query_lower) { + score += 30; + } + // Description match + else if desc_lower.contains(&query_lower) { + score += 20; + } + // Fuzzy match (simple version) + else if fuzzy_match_simple(&name_lower, &query_lower) { + score += 10; + } + + if score > 0 { + score += popularity; // Add popularity bonus + Some((name, category, desc, score)) + } else { + None + } + }) + .collect(); + + // Sort by score (highest first) + scored_companies.sort_by(|a, b| b.3.cmp(&a.3)); + + let suggestions: Vec = scored_companies.into_iter() + .take(10) // Limit results + .map(|(name, category, desc, _score)| SuggestionItem { + display_text: format!("{} - {} β€’ {}", name, category, desc), + value_to_store: name.to_string(), + }) + .collect(); + + // Cache the result + self.company_cache.insert(query.to_string(), suggestions.clone()); + Ok(suggestions) + } + + /// Smart multi-tag skills suggestions with context awareness + async fn get_skills_suggestions(&mut self, query: &str) -> Result> { + let skills_db = vec![ + // Programming Languages + ("Rust", "Language", "Systems programming"), + ("Python", "Language", "Versatile, data science"), + ("JavaScript", "Language", "Web development"), + ("TypeScript", "Language", "Typed JavaScript"), + ("Go", "Language", "Cloud, microservices"), + ("Java", "Language", "Enterprise, Android"), + ("C++", "Language", "Performance critical"), + ("C#", "Language", "Microsoft ecosystem"), + ("Swift", "Language", "iOS development"), + ("Kotlin", "Language", "Android, JVM"), + + // Frontend Frameworks + ("React", "Frontend", "Popular UI library"), + ("Vue", "Frontend", "Progressive framework"), + ("Angular", "Frontend", "Full framework"), + ("Svelte", "Frontend", "Compile-time framework"), + + // Backend Technologies + ("Node.js", "Backend", "JavaScript runtime"), + ("Express", "Backend", "Web framework"), + ("Django", "Backend", "Python web framework"), + ("Flask", "Backend", "Micro web framework"), + ("Rails", "Backend", "Ruby web framework"), + + // Databases + ("PostgreSQL", "Database", "Advanced SQL database"), + ("MongoDB", "Database", "Document database"), + ("Redis", "Database", "In-memory cache"), + ("MySQL", "Database", "Popular SQL database"), + + // DevOps & Cloud + ("Docker", "DevOps", "Containerization"), + ("Kubernetes", "DevOps", "Container orchestration"), + ("AWS", "Cloud", "Amazon Web Services"), + ("GCP", "Cloud", "Google Cloud Platform"), + ("Azure", "Cloud", "Microsoft cloud"), + + // Tools & Methodologies + ("Git", "Tool", "Version control"), + ("Linux", "Tool", "Operating system"), + ("DevOps", "Methodology", "Development operations"), + ("CI/CD", "Methodology", "Continuous integration"), + ("TDD", "Methodology", "Test-driven development"), + ("Agile", "Methodology", "Project management"), + ("Scrum", "Methodology", "Agile framework"), + + // Emerging Tech + ("Machine Learning", "AI", "Predictive models"), + ("Data Science", "Analytics", "Data analysis"), + ("Blockchain", "Distributed", "Decentralized systems"), + ("WebAssembly", "Performance", "High-performance web"), + ]; + + // Parse existing tags to avoid duplicates + let current_tags: Vec<&str> = query.split(',').map(|s| s.trim()).collect(); + let last_tag = current_tags.last().unwrap_or(&"").to_lowercase(); + + // Don't suggest if last tag is empty + if last_tag.is_empty() { + return Ok(Vec::new()); + } + + // Find matching skills + let mut matching_skills: Vec<_> = skills_db.iter() + .filter(|(skill, _, _)| { + let skill_lower = skill.to_lowercase(); + // Check if skill matches and isn't already selected + skill_lower.contains(&last_tag) && + !current_tags[..current_tags.len()-1].iter().any(|&existing| + existing.eq_ignore_ascii_case(skill) + ) + }) + .collect(); + + // Sort by relevance (exact prefix first) + matching_skills.sort_by(|a, b| { + let a_exact = a.0.to_lowercase().starts_with(&last_tag); + let b_exact = b.0.to_lowercase().starts_with(&last_tag); + + match (a_exact, b_exact) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.0.cmp(b.0), + } + }); + + let suggestions = matching_skills.into_iter() + .take(8) // Limit results + .map(|(skill, category, desc)| { + let mut new_tags = current_tags[..current_tags.len()-1].to_vec(); + new_tags.push(skill); + let new_value = new_tags.join(", "); + + SuggestionItem { + display_text: format!("+ {} ({} β€’ {})", skill, category, desc), + value_to_store: new_value, + } + }) + .collect(); + + Ok(suggestions) + } +} + +/// Simple fuzzy matching: check if all characters of pattern appear in text in order +fn fuzzy_match_simple(text: &str, pattern: &str) -> bool { + if pattern.is_empty() { + return true; + } + + let mut pattern_chars = pattern.chars(); + let mut current_char = pattern_chars.next(); + + for text_char in text.chars() { + if let Some(pattern_char) = current_char { + if text_char == pattern_char { + current_char = pattern_chars.next(); + if current_char.is_none() { + return true; // All pattern characters matched + } + } + } + } + + false +} + +/// Automatic cursor management demonstration + SUGGESTIONS +/// Features the CursorManager directly to show it's working +async fn handle_key_press( + key: KeyCode, + modifiers: KeyModifiers, + editor: &mut AutoCursorFormEditor, + suggestions_provider: &mut PowerfulSuggestionsProvider, +) -> anyhow::Result { + let mode = editor.mode(); + + // Quit handling + if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) + || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) + || key == KeyCode::F(10) + { + return Ok(false); + } + + match (mode, key, modifiers) { + // === SUGGESTIONS HANDLING (NEW!) === + + // Tab: Trigger or navigate suggestions + (_, KeyCode::Tab, _) => { + if editor.is_suggestions_active() { + editor.suggestions_next(); + editor.set_debug_message("πŸ“ Next suggestion".to_string()); + } else if editor.data_provider().supports_suggestions(editor.current_field()) { + match editor.trigger_suggestions(suggestions_provider).await { + Ok(_) => { + if editor.suggestions().is_empty() { + editor.set_debug_message("πŸ” No suggestions found".to_string()); + } else { + editor.set_debug_message(format!("✨ {} suggestions found", editor.suggestions().len())); + } + } + Err(e) => { + editor.set_debug_message(format!("❌ Suggestion error: {}", e)); + } + } + } else { + editor.next_field(); + editor.set_debug_message("Tab: next field".to_string()); + } + } + + // Enter: Apply suggestion or move to next field + (_, KeyCode::Enter, _) => { + if editor.is_suggestions_active() { + if let Some(applied) = editor.apply_suggestion() { + editor.set_debug_message(format!("βœ… Applied: {}", applied)); + } else { + editor.set_debug_message("❌ No suggestion selected".to_string()); + } + } else { + editor.next_field(); + editor.set_debug_message("Enter: next field".to_string()); + } + } + + // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === + (AppMode::ReadOnly, KeyCode::Char('i'), _) => { + editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + editor.enter_append_mode(); + editor.set_debug_message("✏️ INSERT (append) - Cursor: Steady Bar |".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('A'), _) => { + editor.move_line_end(); + editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | + editor.set_debug_message("✏️ INSERT (end of line) - Cursor: Steady Bar |".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('o'), _) => { + editor.move_line_end(); + editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | + editor.set_debug_message("✏️ INSERT (open line) - Cursor: Steady Bar |".to_string()); + editor.clear_command_buffer(); + } + + // From Normal Mode: Enter visual modes + (AppMode::ReadOnly, KeyCode::Char('v'), _) => { + editor.enter_visual_mode(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('V'), _) => { + editor.enter_visual_line_mode(); + editor.clear_command_buffer(); + } + + // From Visual Mode: Switch between visual modes or exit + (AppMode::Highlight, KeyCode::Char('v'), _) => { + use canvas::canvas::state::SelectionState; + match editor.editor.selection_state() { + SelectionState::Characterwise { .. } => { + // Already in characterwise mode, exit visual mode (vim behavior) + editor.exit_visual_mode(); + editor.set_debug_message("πŸ”’ Exited visual mode".to_string()); + } + _ => { + // Switch from linewise to characterwise mode + editor.editor.enter_highlight_mode(); + editor.update_visual_selection(); + editor.set_debug_message("πŸ”₯ Switched to VISUAL mode".to_string()); + } + } + editor.clear_command_buffer(); + } + + (AppMode::Highlight, KeyCode::Char('V'), _) => { + use canvas::canvas::state::SelectionState; + match editor.editor.selection_state() { + SelectionState::Linewise { .. } => { + // Already in linewise mode, exit visual mode (vim behavior) + editor.exit_visual_mode(); + editor.set_debug_message("πŸ”’ Exited visual mode".to_string()); + } + _ => { + // Switch from characterwise to linewise mode + editor.editor.enter_highlight_line_mode(); + editor.update_visual_selection(); + editor.set_debug_message("πŸ”₯ Switched to VISUAL LINE mode".to_string()); + } + } + editor.clear_command_buffer(); + } + + // Escape: Exit any mode back to normal (and cancel suggestions) + (_, KeyCode::Esc, _) => { + match mode { + AppMode::Edit => { + editor.exit_edit_mode(); // Exit insert mode (suggestions auto-cancelled) + } + AppMode::Highlight => { + editor.exit_visual_mode(); // Exit visual mode + } + _ => { + // Already in normal mode, just clear command buffer + editor.clear_command_buffer(); + } + } + } + + // === CURSOR MANAGEMENT DEMONSTRATION === + (AppMode::ReadOnly, KeyCode::F(1), _) => { + editor.demo_manual_cursor_control()?; + } + (AppMode::ReadOnly, KeyCode::F(2), _) => { + editor.restore_automatic_cursor()?; + } + + // === MOVEMENT: VIM-STYLE NAVIGATION === + + // Basic movement (hjkl and arrows) + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('h'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => { + editor.move_left(); + editor.set_debug_message("← left".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => { + editor.move_right(); + editor.set_debug_message("β†’ right".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => { + editor.move_down(); + editor.set_debug_message("↓ next field".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { + editor.move_up(); + editor.set_debug_message("↑ previous field".to_string()); + editor.clear_command_buffer(); + } + + // Word movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => { + editor.move_word_next(); + editor.set_debug_message("w: next word start".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => { + editor.move_word_prev(); + editor.set_debug_message("b: previous word start".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => { + editor.move_word_end(); + editor.set_debug_message("e: word end".to_string()); + editor.clear_command_buffer(); + } + + // Line movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => { + editor.move_line_start(); + editor.set_debug_message("0: line start".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('$'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::End, _) => { + editor.move_line_end(); + editor.set_debug_message("$: line end".to_string()); + } + + // Field/document movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => { + if editor.get_command_buffer() == "g" { + editor.move_first_line(); + editor.set_debug_message("gg: first field".to_string()); + editor.clear_command_buffer(); + } else { + editor.clear_command_buffer(); + editor.add_to_command_buffer('g'); + editor.set_debug_message("g".to_string()); + } + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => { + editor.move_last_line(); + editor.set_debug_message("G: last field".to_string()); + editor.clear_command_buffer(); + } + + // === EDIT MODE MOVEMENT === + (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_prev(); + editor.set_debug_message("Ctrl+← word back".to_string()); + } + (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_next(); + editor.set_debug_message("Ctrl+β†’ word forward".to_string()); + } + (AppMode::Edit, KeyCode::Left, _) => { + editor.move_left(); + } + (AppMode::Edit, KeyCode::Right, _) => { + editor.move_right(); + } + (AppMode::Edit, KeyCode::Up, _) => { + editor.move_up(); + } + (AppMode::Edit, KeyCode::Down, _) => { + editor.move_down(); + } + (AppMode::Edit, KeyCode::Home, _) => { + editor.move_line_start(); + } + (AppMode::Edit, KeyCode::End, _) => { + editor.move_line_end(); + } + + // === DELETE OPERATIONS === + (AppMode::Edit, KeyCode::Backspace, _) => { + editor.delete_backward()?; + } + (AppMode::Edit, KeyCode::Delete, _) => { + editor.delete_forward()?; + } + + // Delete operations in normal mode (vim x) + (AppMode::ReadOnly, KeyCode::Char('x'), _) => { + editor.delete_forward()?; + editor.set_debug_message("x: deleted character".to_string()); + } + (AppMode::ReadOnly, KeyCode::Char('X'), _) => { + editor.delete_backward()?; + editor.set_debug_message("X: deleted character backward".to_string()); + } + + // === CHARACTER INPUT === + (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { + editor.insert_char(c)?; + } + + // === DEBUG/INFO COMMANDS === + (AppMode::ReadOnly, KeyCode::Char('?'), _) => { + editor.set_debug_message(format!( + "Field {}/{}, Pos {}, Mode: {:?} - Smart suggestions: Email domains, Company scoring, Skills multi-tag", + editor.current_field() + 1, + editor.data_provider().field_count(), + editor.cursor_position(), + editor.mode() + )); + } + + _ => { + if editor.has_pending_command() { + editor.clear_command_buffer(); + editor.set_debug_message("Invalid command sequence".to_string()); + } else { + editor.set_debug_message(format!( + "Unhandled: {:?} + {:?} in {:?} mode", + key, modifiers, mode + )); + } + } + } + + Ok(true) +} + +async fn run_app( + terminal: &mut Terminal, + mut editor: AutoCursorFormEditor, +) -> io::Result<()> { + let mut suggestions_provider = PowerfulSuggestionsProvider::new(); + + loop { + terminal.draw(|f| ui(f, &editor))?; + + if let Event::Key(key) = event::read()? { + match handle_key_press(key.code, key.modifiers, &mut editor, &mut suggestions_provider).await { + Ok(should_continue) => { + if !should_continue { + break; + } + } + Err(e) => { + editor.set_debug_message(format!("Error: {}", e)); + } + } + } + } + + Ok(()) +} + +fn ui(f: &mut Frame, editor: &AutoCursorFormEditor) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(8), Constraint::Length(10)]) + .split(f.area()); + + let active_field_rect = render_enhanced_canvas(f, chunks[0], editor); + + // Render suggestions dropdown if active + if let Some(input_rect) = active_field_rect { + render_suggestions_dropdown( + f, + chunks[0], + input_rect, + &canvas::canvas::theme::DefaultCanvasTheme::default(), + &editor.editor, + ); + } + + render_status_and_help(f, chunks[1], editor); +} + +fn render_enhanced_canvas( + f: &mut Frame, + area: ratatui::layout::Rect, + editor: &AutoCursorFormEditor, +) -> Option { + render_canvas_default(f, area, &editor.editor) +} + +fn render_status_and_help( + f: &mut Frame, + area: ratatui::layout::Rect, + editor: &AutoCursorFormEditor, +) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Length(7)]) + .split(area); + + // Status bar with cursor information + suggestions + let mode_text = match editor.mode() { + AppMode::Edit => "INSERT | (bar cursor)", + AppMode::ReadOnly => "NORMAL β–ˆ (block cursor)", + AppMode::Highlight => { + // Use library selection state instead of editor.highlight_state() + use canvas::canvas::state::SelectionState; + match editor.editor.selection_state() { + SelectionState::Characterwise { .. } => "VISUAL β–ˆ (blinking block)", + SelectionState::Linewise { .. } => "VISUAL LINE β–ˆ (blinking block)", + _ => "VISUAL β–ˆ (blinking block)", + } + }, + _ => "NORMAL β–ˆ (block cursor)", + }; + + let suggestions_info = if editor.is_suggestions_active() { + if editor.editor.ui_state().is_suggestions_loading() { + " | ⏳ Loading suggestions..." + } else if !editor.suggestions().is_empty() { + " | ✨ Suggestions available" + } else { + " | πŸ” No suggestions" + } + } else { + "" + }; + + let status_text = if editor.has_pending_command() { + format!("-- {} -- {} [{}]{}", mode_text, editor.debug_message(), editor.get_command_buffer(), suggestions_info) + } else if editor.has_unsaved_changes() { + format!("-- {} -- [Modified] {}{}", mode_text, editor.debug_message(), suggestions_info) + } else { + format!("-- {} -- {}{}", mode_text, editor.debug_message(), suggestions_info) + }; + + let status = Paragraph::new(Line::from(Span::raw(status_text))) + .block(Block::default().borders(Borders::ALL).title("🎯 Smart Cursor + Intelligent Suggestions")); + + f.render_widget(status, chunks[0]); + + // Enhanced help text with suggestions info + let help_text = match editor.mode() { + AppMode::ReadOnly => { + if editor.has_pending_command() { + match editor.get_command_buffer() { + "g" => "Press 'g' again for first field, or any other key to cancel", + _ => "Pending command... (Esc to cancel)" + } + } else { + "🎯 SMART SUGGESTIONS DEMO: Normal β–ˆ | Insert | | Visual blinkingβ–ˆ\n\ + Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last\n\ + i/a/A=insert, v/V=visual, x/X=delete, ?=info\n\ + Tab=smart suggestions: Email domains, Company scoring, Multi-tag skills\n\ + Features: caching, fuzzy matching, validation, scoring | F1/F2=cursor demo" + } + } + AppMode::Edit => { + "🎯 INSERT MODE - Cursor: | (bar)\n\ + arrows=move, Ctrl+arrows=words, Backspace/Del=delete\n\ + Tab=smart suggestions: domains, companies, skills with fuzzy matching\n\ + Enter=apply suggestion, Esc=normal mode" + } + AppMode::Highlight => { + "🎯 VISUAL MODE - Cursor: β–ˆ (blinking block)\n\ + hjkl/arrows=extend selection, w/b/e=word selection\n\ + Esc=normal" + } + _ => "🎯 Watch the cursor change automatically! Tab for intelligent suggestions on certain fields!" + }; + + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("πŸš€ Smart Cursor + Intelligent Suggestions")) + .style(Style::default().fg(Color::Gray)); + + f.render_widget(help, chunks[1]); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Print feature status + println!("🎯 Smart Cursor + Intelligent Suggestions Demo"); + println!("βœ… cursor-style feature: ENABLED"); + println!("βœ… suggestions feature: ENABLED"); + println!("πŸš€ Automatic cursor management: ACTIVE"); + println!("✨ Intelligent suggestions: ACTIVE"); + println!("πŸ“– Watch your terminal cursor change based on mode!"); + println!(); + println!("🎯 Try these logical suggestions:"); + println!(" πŸ“§ Email field: 'user@exa' + Tab β†’ smart domain completion"); + println!(" 🏒 Company field: 'tech' + Tab β†’ scored company matches"); + println!(" πŸ’» Skills field: 'rust,py' + Tab β†’ intelligent multi-tag skills"); + println!(" 🧠 Features: caching, fuzzy matching, scoring, validation"); + println!(); + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let data = CursorDemoData::new(); + let mut editor = AutoCursorFormEditor::new(data); + + // Initialize with normal mode - library automatically sets block cursor + editor.set_mode(AppMode::ReadOnly); + + // Demonstrate that CursorManager is available and working + CursorManager::update_for_mode(AppMode::ReadOnly)?; + + let res = run_app(&mut terminal, editor).await; + + // Library automatically resets cursor on FormEditor::drop() + // But we can also manually reset if needed + CursorManager::reset()?; + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + println!("🎯 Cursor automatically reset to default! Smart suggestions cache cleared!"); + Ok(()) +} diff --git a/canvas/src/canvas/gui.rs b/canvas/src/canvas/gui.rs index eae3ec2..9a02563 100644 --- a/canvas/src/canvas/gui.rs +++ b/canvas/src/canvas/gui.rs @@ -67,6 +67,15 @@ pub fn render_canvas_with_highlight( let current_field_idx = ui_state.current_field(); let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit); + // Precompute completion for active field + let active_completion = if ui_state.is_suggestions_active() + && ui_state.suggestions.active_field == Some(current_field_idx) + { + ui_state.suggestions.completion_text.clone() + } else { + None + }; + render_canvas_fields( f, area, @@ -111,6 +120,14 @@ pub fn render_canvas_with_highlight( false } }, + // NEW: provide completion for the active field + |i| { + if i == current_field_idx { + active_completion.clone() + } else { + None + } + }, ) } @@ -128,7 +145,7 @@ fn convert_selection_to_highlight(selection: &crate::canvas::state::SelectionSta /// Core canvas field rendering #[cfg(feature = "gui")] -fn render_canvas_fields( +fn render_canvas_fields( f: &mut Frame, area: Rect, fields: &[&str], @@ -141,10 +158,12 @@ fn render_canvas_fields( has_unsaved_changes: bool, get_display_value: F1, has_display_override: F2, + get_completion: F3, ) -> Option where F1: Fn(usize) -> String, F2: Fn(usize) -> bool, + F3: Fn(usize) -> Option, { // Create layout let columns = Layout::default() @@ -198,6 +217,7 @@ where current_cursor_pos, get_display_value, has_display_override, + get_completion, ) } @@ -229,7 +249,7 @@ fn render_field_labels( /// Render field values with highlighting #[cfg(feature = "gui")] -fn render_field_values( +fn render_field_values( f: &mut Frame, input_rows: Vec, inputs: &[String], @@ -239,35 +259,54 @@ fn render_field_values( current_cursor_pos: usize, get_display_value: F1, has_display_override: F2, + get_completion: F3, ) -> Option where F1: Fn(usize) -> String, F2: Fn(usize) -> bool, + F3: Fn(usize) -> Option, { let mut active_field_input_rect = None; for (i, _input) in inputs.iter().enumerate() { let is_active = i == *current_field_idx; - let text = get_display_value(i); + let typed_text = get_display_value(i); - // Apply highlighting - let line = apply_highlighting( - &text, - i, - current_field_idx, - current_cursor_pos, - highlight_state, - theme, - is_active, - ); + let line = if is_active { + // Compose typed + gray completion for the active field + let normal_style = Style::default().fg(theme.fg()); + let gray_style = Style::default().fg(theme.suggestion_gray()); + + let mut spans: Vec = Vec::new(); + spans.push(Span::styled(typed_text.clone(), normal_style)); + + if let Some(completion) = get_completion(i) { + if !completion.is_empty() { + spans.push(Span::styled(completion, gray_style)); + } + } + + Line::from(spans) + } else { + // Non-active fields: keep existing highlighting logic + apply_highlighting( + &typed_text, + i, + current_field_idx, + current_cursor_pos, + highlight_state, + theme, + is_active, + ) + }; let input_display = Paragraph::new(line).alignment(Alignment::Left); f.render_widget(input_display, input_rows[i]); - // Set cursor for active field + // Set cursor for active field at end of typed text (not after completion) if is_active { active_field_input_rect = Some(input_rows[i]); - set_cursor_position(f, input_rows[i], &text, current_cursor_pos, has_display_override(i)); + set_cursor_position(f, input_rows[i], &typed_text, current_cursor_pos, has_display_override(i)); } } diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index dd68c50..fe2c709 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -35,6 +35,7 @@ pub struct SuggestionsUIState { pub(crate) is_loading: bool, pub(crate) selected_index: Option, pub(crate) active_field: Option, + pub(crate) completion_text: Option, } #[derive(Debug, Clone)] @@ -56,6 +57,7 @@ impl EditorState { is_loading: false, selected_index: None, active_field: None, + completion_text: None, }, selection: SelectionState::None, #[cfg(feature = "validation")] @@ -148,6 +150,7 @@ impl EditorState { self.suggestions.is_loading = true; self.suggestions.active_field = Some(field_index); self.suggestions.selected_index = None; + self.suggestions.completion_text = None; } pub(crate) fn deactivate_suggestions(&mut self) { @@ -155,6 +158,7 @@ impl EditorState { self.suggestions.is_loading = false; self.suggestions.active_field = None; self.suggestions.selected_index = None; + self.suggestions.completion_text = None; } } diff --git a/canvas/src/canvas/theme.rs b/canvas/src/canvas/theme.rs index d2f02d2..09ee89c 100644 --- a/canvas/src/canvas/theme.rs +++ b/canvas/src/canvas/theme.rs @@ -14,6 +14,7 @@ pub trait CanvasTheme { fn highlight(&self) -> Color; fn highlight_bg(&self) -> Color; fn warning(&self) -> Color; + fn suggestion_gray(&self) -> Color; } @@ -47,4 +48,7 @@ impl CanvasTheme for DefaultCanvasTheme { fn warning(&self) -> Color { Color::Red } + fn suggestion_gray(&self) -> Color { + Color::DarkGray + } } diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 211096a..3344139 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -40,6 +40,24 @@ impl FormEditor { editor } + /// Compute inline completion for current selection and current text. + fn compute_current_completion(&self) -> Option { + let typed = self.current_text(); + let idx = self.ui_state.suggestions.selected_index?; + let sugg = self.suggestions.get(idx)?; + if let Some(rest) = sugg.value_to_store.strip_prefix(typed) { + if !rest.is_empty() { + return Some(rest.to_string()); + } + } + None + } + + /// Update UI state's completion text from current selection + fn update_inline_completion(&mut self) { + self.ui_state.suggestions.completion_text = self.compute_current_completion(); + } + /// Initialize validation configurations from data provider #[cfg(feature = "validation")] fn initialize_validation(&mut self) { @@ -605,6 +623,8 @@ impl FormEditor { self.ui_state.suggestions.is_loading = false; if !self.suggestions.is_empty() { self.ui_state.suggestions.selected_index = Some(0); + // Compute initial inline completion from first suggestion + self.update_inline_completion(); } Ok(()) @@ -619,6 +639,9 @@ impl FormEditor { let current = self.ui_state.suggestions.selected_index.unwrap_or(0); let next = (current + 1) % self.suggestions.len(); self.ui_state.suggestions.selected_index = Some(next); + + // Update inline completion to reflect new highlighted item + self.update_inline_completion(); } /// Apply selected suggestion