sugggestions are agnostic

This commit is contained in:
Priec
2025-08-11 00:01:53 +02:00
parent 53464dfcbf
commit e8a564aed3
3 changed files with 292 additions and 245 deletions

View File

@@ -1,7 +1,11 @@
// examples/suggestions2.rs // examples/suggestions2.rs
//! Demonstrates automatic cursor management + MULTIPLE SUGGESTION FIELDS //! Production-ready non-blocking suggestions demonstration
//! //!
//! This example REQUIRES the `cursor-style` feature to compile. //! This example demonstrates:
//! - Instant, responsive suggestions dropdown
//! - Non-blocking architecture for real network/database calls
//! - Multiple suggestion field types
//! - Professional-grade user experience
//! //!
//! Run with: //! Run with:
//! cargo run --example suggestions2 --features "gui,cursor-style,suggestions" //! cargo run --example suggestions2 --features "gui,cursor-style,suggestions"
@@ -35,7 +39,7 @@ use ratatui::{
use canvas::{ use canvas::{
canvas::{ canvas::{
gui::render_canvas_default, gui::render_canvas_default,
modes::{AppMode, ModeManager, HighlightState}, modes::AppMode,
CursorManager, // This import only exists when cursor-style feature is enabled CursorManager, // This import only exists when cursor-style feature is enabled
}, },
suggestions::gui::render_suggestions_dropdown, suggestions::gui::render_suggestions_dropdown,
@@ -45,7 +49,7 @@ use canvas::{
use async_trait::async_trait; use async_trait::async_trait;
use anyhow::Result; use anyhow::Result;
// Enhanced FormEditor that demonstrates automatic cursor management + SUGGESTIONS // Enhanced FormEditor that demonstrates professional suggestions architecture
struct AutoCursorFormEditor<D: DataProvider> { struct AutoCursorFormEditor<D: DataProvider> {
editor: FormEditor<D>, editor: FormEditor<D>,
has_unsaved_changes: bool, has_unsaved_changes: bool,
@@ -58,7 +62,7 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
Self { Self {
editor: FormEditor::new(data_provider), editor: FormEditor::new(data_provider),
has_unsaved_changes: false, has_unsaved_changes: false,
debug_message: "🎯 Multi-Field Suggestions Demo - 5 fields with different suggestions!".to_string(), debug_message: "🚀 Production-Ready Suggestions Demo - Copy this architecture for your app!".to_string(),
command_buffer: String::new(), command_buffer: String::new(),
} }
} }
@@ -88,19 +92,16 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
// === VISUAL/HIGHLIGHT MODE SUPPORT === // === VISUAL/HIGHLIGHT MODE SUPPORT ===
fn enter_visual_mode(&mut self) { fn enter_visual_mode(&mut self) {
// Use the library method instead of manual state setting
self.editor.enter_highlight_mode(); self.editor.enter_highlight_mode();
self.debug_message = "🔥 VISUAL MODE - Cursor: Blinking Block █".to_string(); self.debug_message = "🔥 VISUAL MODE - Cursor: Blinking Block █".to_string();
} }
fn enter_visual_line_mode(&mut self) { fn enter_visual_line_mode(&mut self) {
// Use the library method instead of manual state setting
self.editor.enter_highlight_line_mode(); self.editor.enter_highlight_line_mode();
self.debug_message = "🔥 VISUAL LINE MODE - Cursor: Blinking Block █".to_string(); self.debug_message = "🔥 VISUAL LINE MODE - Cursor: Blinking Block █".to_string();
} }
fn exit_visual_mode(&mut self) { fn exit_visual_mode(&mut self) {
// Use the library method
self.editor.exit_highlight_mode(); self.editor.exit_highlight_mode();
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string(); self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
} }
@@ -132,12 +133,12 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
// === ENHANCED MOVEMENT WITH VISUAL UPDATES === // === ENHANCED MOVEMENT WITH VISUAL UPDATES ===
fn move_left(&mut self) { fn move_left(&mut self) {
self.editor.move_left(); let _ = self.editor.move_left();
self.update_visual_selection(); self.update_visual_selection();
} }
fn move_right(&mut self) { fn move_right(&mut self) {
self.editor.move_right(); let _ = self.editor.move_right();
self.update_visual_selection(); self.update_visual_selection();
} }
@@ -182,12 +183,12 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
} }
fn move_first_line(&mut self) { fn move_first_line(&mut self) {
self.editor.move_first_line(); let _ = self.editor.move_first_line();
self.update_visual_selection(); self.update_visual_selection();
} }
fn move_last_line(&mut self) { fn move_last_line(&mut self) {
self.editor.move_last_line(); let _ = self.editor.move_last_line();
self.update_visual_selection(); self.update_visual_selection();
} }
@@ -253,13 +254,50 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
Ok(result?) Ok(result?)
} }
// === SUGGESTIONS SUPPORT === // === PRODUCTION-READY NON-BLOCKING SUGGESTIONS ===
async fn trigger_suggestions<A>(&mut self, provider: &mut A) -> anyhow::Result<()> /// Trigger suggestions with non-blocking approach (production pattern)
where ///
A: SuggestionsProvider, /// This method demonstrates the proper way to integrate suggestions with
{ /// real APIs, databases, or any async data source without blocking the UI.
self.editor.trigger_suggestions(provider).await async fn trigger_suggestions_async(
&mut self,
provider: &mut ProductionSuggestionsProvider,
field_index: usize,
) {
// Step 1: Start loading immediately (UI updates instantly)
if let Some(query) = self.editor.start_suggestions(field_index) {
// Step 2: Fetch from your data source (API, database, etc.)
match provider.fetch_suggestions(field_index, &query).await {
Ok(results) => {
// Step 3: Apply results with built-in stale protection
let applied = self.editor.apply_suggestions_result(field_index, &query, results);
if applied {
self.editor.update_inline_completion();
if self.editor.suggestions().is_empty() {
self.set_debug_message(format!("🔍 No matches for '{}'", query));
} else {
self.set_debug_message(format!("{} matches for '{}'", self.editor.suggestions().len(), query));
}
}
// If not applied, results were stale (user kept typing)
}
Err(e) => {
self.set_debug_message(format!("❌ Suggestion error: {}", e));
}
}
}
}
/// Auto-trigger suggestions for current field (production pattern)
///
/// Call this after character input, deletion, or field entry to automatically
/// show suggestions. Perfect for real-time search-as-you-type functionality.
async fn auto_trigger_suggestions(&mut self, provider: &mut ProductionSuggestionsProvider) {
let field_index = self.current_field();
if self.data_provider().supports_suggestions(field_index) {
self.trigger_suggestions_async(provider, field_index).await;
}
} }
fn suggestions_next(&mut self) { fn suggestions_next(&mut self) {
@@ -284,16 +322,13 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
// === MANUAL CURSOR OVERRIDE DEMONSTRATION === // === MANUAL CURSOR OVERRIDE DEMONSTRATION ===
/// Demonstrate manual cursor control (for advanced users)
fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> { fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> {
// Users can still manually control cursor if needed
CursorManager::update_for_mode(AppMode::Command)?; CursorManager::update_for_mode(AppMode::Command)?;
self.debug_message = "🔧 Manual override: Command cursor _".to_string(); self.debug_message = "🔧 Manual override: Command cursor _".to_string();
Ok(()) Ok(())
} }
fn restore_automatic_cursor(&mut self) -> std::io::Result<()> { fn restore_automatic_cursor(&mut self) -> std::io::Result<()> {
// Restore automatic cursor based on current mode
CursorManager::update_for_mode(self.editor.mode())?; CursorManager::update_for_mode(self.editor.mode())?;
self.debug_message = "🎯 Restored automatic cursor management".to_string(); self.debug_message = "🎯 Restored automatic cursor management".to_string();
Ok(()) Ok(())
@@ -349,28 +384,28 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
} }
// =================================================================== // ===================================================================
// MULTI-FIELD DEMO DATA - 5 different types of suggestion fields // PRODUCTION DATA MODEL - Copy this pattern for your application
// =================================================================== // ===================================================================
struct MultiFieldDemoData { struct ApplicationData {
fields: Vec<(String, String)>, fields: Vec<(String, String)>,
} }
impl MultiFieldDemoData { impl ApplicationData {
fn new() -> Self { fn new() -> Self {
Self { Self {
fields: vec![ fields: vec![
("🍎 Favorite Fruit".to_string(), "".to_string()), // Field 0: Fruits ("🍎 Favorite Fruit".to_string(), "".to_string()),
("💼 Job Role".to_string(), "".to_string()), // Field 1: Jobs ("💼 Job Role".to_string(), "".to_string()),
("💻 Programming Language".to_string(), "".to_string()), // Field 2: Languages ("💻 Programming Language".to_string(), "".to_string()),
("🌍 Country".to_string(), "".to_string()), // Field 3: Countries ("🌍 Country".to_string(), "".to_string()),
("🎨 Favorite Color".to_string(), "".to_string()), // Field 4: Colors ("🎨 Favorite Color".to_string(), "".to_string()),
], ],
} }
} }
} }
impl DataProvider for MultiFieldDemoData { impl DataProvider for ApplicationData {
fn field_count(&self) -> usize { fn field_count(&self) -> usize {
self.fields.len() self.fields.len()
} }
@@ -388,7 +423,7 @@ impl DataProvider for MultiFieldDemoData {
} }
fn supports_suggestions(&self, field_index: usize) -> bool { fn supports_suggestions(&self, field_index: usize) -> bool {
// All 5 fields support suggestions - perfect for testing! // Configure which fields support suggestions
field_index < 5 field_index < 5
} }
@@ -398,18 +433,47 @@ impl DataProvider for MultiFieldDemoData {
} }
// =================================================================== // ===================================================================
// COMPREHENSIVE SUGGESTIONS PROVIDER - 5 different suggestion types! // PRODUCTION SUGGESTIONS PROVIDER - Copy this pattern for your APIs
// =================================================================== // ===================================================================
struct ComprehensiveSuggestionsProvider; /// Production-ready suggestions provider
///
/// Replace the data sources below with your actual:
/// - REST API calls (reqwest, hyper)
/// - Database queries (sqlx, diesel)
/// - Search engines (elasticsearch, algolia)
/// - Cache lookups (redis, memcached)
/// - GraphQL queries
/// - gRPC services
///
/// The non-blocking architecture works with any async data source.
struct ProductionSuggestionsProvider {
// Add your API clients, database connections, cache clients here
// Example:
// api_client: reqwest::Client,
// db_pool: sqlx::PgPool,
// cache: redis::Client,
}
impl ComprehensiveSuggestionsProvider { impl ProductionSuggestionsProvider {
fn new() -> Self { fn new() -> Self {
Self Self {
// Initialize your clients here
// api_client: reqwest::Client::new(),
// db_pool: create_db_pool().await,
// cache: redis::Client::open("redis://localhost").unwrap(),
}
} }
/// Get fruit suggestions (field 0) /// Get fruit suggestions (replace with your API call)
fn get_fruit_suggestions(&self, query: &str) -> Vec<SuggestionItem> { async fn get_fruit_suggestions(&self, query: &str) -> Result<Vec<SuggestionItem>> {
// Example: Replace with actual API call
// let response = self.api_client
// .get(&format!("https://api.example.com/fruits?q={}", query))
// .send()
// .await?;
// let fruits: Vec<Fruit> = response.json().await?;
let fruits = vec![ let fruits = vec![
("Apple", "🍎 Crisp and sweet"), ("Apple", "🍎 Crisp and sweet"),
("Banana", "🍌 Rich in potassium"), ("Banana", "🍌 Rich in potassium"),
@@ -419,15 +483,22 @@ impl ComprehensiveSuggestionsProvider {
("Fig", "🍇 Sweet Mediterranean fruit"), ("Fig", "🍇 Sweet Mediterranean fruit"),
("Grape", "🍇 Perfect for wine"), ("Grape", "🍇 Perfect for wine"),
("Honeydew", "🍈 Sweet melon"), ("Honeydew", "🍈 Sweet melon"),
("Ananas", "🍎 Crisp and sweet"),
("Avocado", "🍈 Sweet melon"),
]; ];
self.filter_suggestions(fruits, query, "fruit") Ok(self.filter_suggestions(fruits, query))
} }
/// Get job role suggestions (field 1) /// Get job suggestions (replace with your database query)
fn get_job_suggestions(&self, query: &str) -> Vec<SuggestionItem> { async fn get_job_suggestions(&self, query: &str) -> Result<Vec<SuggestionItem>> {
// Example: Replace with actual database query
// let jobs = sqlx::query_as!(
// JobRow,
// "SELECT title, description FROM jobs WHERE title ILIKE $1 LIMIT 10",
// format!("%{}%", query)
// )
// .fetch_all(&self.db_pool)
// .await?;
let jobs = vec![ let jobs = vec![
("Software Engineer", "👨‍💻 Build applications"), ("Software Engineer", "👨‍💻 Build applications"),
("Product Manager", "📋 Manage product roadmap"), ("Product Manager", "📋 Manage product roadmap"),
@@ -439,11 +510,17 @@ impl ComprehensiveSuggestionsProvider {
("Accountant", "💼 Manage finances"), ("Accountant", "💼 Manage finances"),
]; ];
self.filter_suggestions(jobs, query, "role") Ok(self.filter_suggestions(jobs, query))
} }
/// Get programming language suggestions (field 2) /// Get language suggestions (replace with your cache lookup)
fn get_language_suggestions(&self, query: &str) -> Vec<SuggestionItem> { async fn get_language_suggestions(&self, query: &str) -> Result<Vec<SuggestionItem>> {
// Example: Replace with cache lookup + fallback to API
// let cached = self.cache.get(&format!("langs:{}", query)).await?;
// if let Some(cached_result) = cached {
// return Ok(serde_json::from_str(&cached_result)?);
// }
let languages = vec![ let languages = vec![
("Rust", "🦀 Systems programming"), ("Rust", "🦀 Systems programming"),
("Python", "🐍 Versatile and popular"), ("Python", "🐍 Versatile and popular"),
@@ -455,11 +532,18 @@ impl ComprehensiveSuggestionsProvider {
("Swift", "🍎 iOS development"), ("Swift", "🍎 iOS development"),
]; ];
self.filter_suggestions(languages, query, "language") Ok(self.filter_suggestions(languages, query))
} }
/// Get country suggestions (field 3) /// Get country suggestions (replace with your geographic API)
fn get_country_suggestions(&self, query: &str) -> Vec<SuggestionItem> { async fn get_country_suggestions(&self, query: &str) -> Result<Vec<SuggestionItem>> {
// Example: Replace with geographic API call
// let response = self.api_client
// .get(&format!("https://restcountries.com/v3.1/name/{}", query))
// .send()
// .await?;
// let countries: Vec<Country> = response.json().await?;
let countries = vec![ let countries = vec![
("United States", "🇺🇸 North America"), ("United States", "🇺🇸 North America"),
("Canada", "🇨🇦 Great neighbors"), ("Canada", "🇨🇦 Great neighbors"),
@@ -471,11 +555,11 @@ impl ComprehensiveSuggestionsProvider {
("Brazil", "🇧🇷 Carnival country"), ("Brazil", "🇧🇷 Carnival country"),
]; ];
self.filter_suggestions(countries, query, "country") Ok(self.filter_suggestions(countries, query))
} }
/// Get color suggestions (field 4) /// Get color suggestions (local data)
fn get_color_suggestions(&self, query: &str) -> Vec<SuggestionItem> { async fn get_color_suggestions(&self, query: &str) -> Result<Vec<SuggestionItem>> {
let colors = vec![ let colors = vec![
("Red", "🔴 Bold and energetic"), ("Red", "🔴 Bold and energetic"),
("Blue", "🔵 Calm and trustworthy"), ("Blue", "🔵 Calm and trustworthy"),
@@ -487,11 +571,11 @@ impl ComprehensiveSuggestionsProvider {
("Black", "⚫ Classic and elegant"), ("Black", "⚫ Classic and elegant"),
]; ];
self.filter_suggestions(colors, query, "color") Ok(self.filter_suggestions(colors, query))
} }
/// Generic filtering helper /// Generic filtering helper (reusable for any data source)
fn filter_suggestions(&self, items: Vec<(&str, &str)>, query: &str, _category: &str) -> Vec<SuggestionItem> { fn filter_suggestions(&self, items: Vec<(&str, &str)>, query: &str) -> Vec<SuggestionItem> {
let query_lower = query.to_lowercase(); let query_lower = query.to_lowercase();
items.iter() items.iter()
@@ -507,38 +591,26 @@ impl ComprehensiveSuggestionsProvider {
} }
#[async_trait] #[async_trait]
impl SuggestionsProvider for ComprehensiveSuggestionsProvider { impl SuggestionsProvider for ProductionSuggestionsProvider {
/// Main suggestions entry point - route to appropriate data source
async fn fetch_suggestions(&mut self, field_index: usize, query: &str) -> Result<Vec<SuggestionItem>> { async fn fetch_suggestions(&mut self, field_index: usize, query: &str) -> Result<Vec<SuggestionItem>> {
// Simulate different network delays for different fields (realistic!) match field_index {
let delay_ms = match field_index { 0 => self.get_fruit_suggestions(query).await, // API call
0 => 100, // Fruits: local data 1 => self.get_job_suggestions(query).await, // Database query
1 => 200, // Jobs: medium API call 2 => self.get_language_suggestions(query).await, // Cache + API
2 => 150, // Languages: cached data 3 => self.get_country_suggestions(query).await, // Geographic API
3 => 300, // Countries: slow geographic API 4 => self.get_color_suggestions(query).await, // Local data
4 => 80, // Colors: instant local _ => Ok(Vec::new()),
_ => 100, }
};
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
let suggestions = match field_index {
0 => self.get_fruit_suggestions(query),
1 => self.get_job_suggestions(query),
2 => self.get_language_suggestions(query),
3 => self.get_country_suggestions(query),
4 => self.get_color_suggestions(query),
_ => Vec::new(),
};
Ok(suggestions)
} }
} }
/// Multi-field suggestions demonstration + automatic cursor management /// Production-ready key handling with non-blocking suggestions
async fn handle_key_press( async fn handle_key_press(
key: KeyCode, key: KeyCode,
modifiers: KeyModifiers, modifiers: KeyModifiers,
editor: &mut AutoCursorFormEditor<MultiFieldDemoData>, editor: &mut AutoCursorFormEditor<ApplicationData>,
suggestions_provider: &mut ComprehensiveSuggestionsProvider, suggestions_provider: &mut ProductionSuggestionsProvider,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
let mode = editor.mode(); let mode = editor.mode();
@@ -551,27 +623,16 @@ async fn handle_key_press(
} }
match (mode, key, modifiers) { match (mode, key, modifiers) {
// === SUGGESTIONS HANDLING === // === NON-BLOCKING SUGGESTIONS HANDLING ===
(_, KeyCode::Tab, _) => { (_, KeyCode::Tab, _) => {
if editor.is_suggestions_active() { if editor.is_suggestions_active() {
// Cycle through suggestions // Cycle through suggestions
editor.suggestions_next(); editor.suggestions_next();
editor.set_debug_message("📍 Next suggestion".to_string()); editor.set_debug_message("📍 Next suggestion".to_string());
} else if editor.data_provider().supports_suggestions(editor.current_field()) { } else if editor.data_provider().supports_suggestions(editor.current_field()) {
// Open suggestions explicitly // Trigger non-blocking suggestions
editor.open_suggestions(editor.current_field()); let field_index = editor.current_field();
match editor.trigger_suggestions(suggestions_provider).await { editor.trigger_suggestions_async(suggestions_provider, field_index).await;
Ok(_) => {
editor.update_inline_completion();
editor.set_debug_message(format!(
"{} suggestions loaded",
editor.suggestions().len()
));
}
Err(e) => {
editor.set_debug_message(format!("❌ Suggestion error: {}", e));
}
}
} else { } else {
editor.next_field(); editor.next_field();
editor.set_debug_message("Tab: next field".to_string()); editor.set_debug_message("Tab: next field".to_string());
@@ -614,16 +675,12 @@ async fn handle_key_press(
} }
} }
// === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === // === MODE TRANSITIONS WITH AUTO-SUGGESTIONS ===
(AppMode::ReadOnly, KeyCode::Char('i'), _) => { (AppMode::ReadOnly, KeyCode::Char('i'), _) => {
editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | editor.enter_edit_mode();
editor.clear_command_buffer(); editor.clear_command_buffer();
// Auto-show suggestions on entering insert mode // Auto-show suggestions on entering insert mode
if editor.data_provider().supports_suggestions(editor.current_field()) { editor.auto_trigger_suggestions(suggestions_provider).await;
let _ = editor.trigger_suggestions(suggestions_provider).await;
editor.update_inline_completion();
}
} }
(AppMode::ReadOnly, KeyCode::Char('a'), _) => { (AppMode::ReadOnly, KeyCode::Char('a'), _) => {
editor.enter_append_mode(); editor.enter_append_mode();
@@ -632,7 +689,7 @@ async fn handle_key_press(
} }
(AppMode::ReadOnly, KeyCode::Char('A'), _) => { (AppMode::ReadOnly, KeyCode::Char('A'), _) => {
editor.move_line_end(); editor.move_line_end();
editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | editor.enter_edit_mode();
editor.set_debug_message("✏️ INSERT (end of line) - Cursor: Steady Bar |".to_string()); editor.set_debug_message("✏️ INSERT (end of line) - Cursor: Steady Bar |".to_string());
editor.clear_command_buffer(); editor.clear_command_buffer();
} }
@@ -762,60 +819,16 @@ async fn handle_key_press(
editor.move_line_end(); editor.move_line_end();
} }
// === DELETE OPERATIONS === // === DELETE OPERATIONS WITH AUTO-SUGGESTIONS ===
(AppMode::Edit, KeyCode::Backspace, _) => { (AppMode::Edit, KeyCode::Backspace, _) => {
editor.delete_backward()?; editor.delete_backward()?;
// Auto-trigger suggestions after deletion
// Update suggestions after deletion editor.auto_trigger_suggestions(suggestions_provider).await;
if editor.data_provider().supports_suggestions(editor.current_field()) {
let current_text = editor.current_text().to_string();
if current_text.is_empty() {
let _ = editor.trigger_suggestions(suggestions_provider).await;
editor.set_debug_message(format!("{} total suggestions", editor.suggestions().len()));
editor.update_inline_completion();
} else {
match editor.trigger_suggestions(suggestions_provider).await {
Ok(_) => {
if editor.suggestions().is_empty() {
editor.set_debug_message(format!("🔍 No matches for '{}'", current_text));
} else {
editor.set_debug_message(format!("{} matches for '{}'", editor.suggestions().len(), current_text));
}
}
Err(e) => {
editor.set_debug_message(format!("❌ Suggestion error: {}", e));
}
}
editor.update_inline_completion();
}
}
} }
(AppMode::Edit, KeyCode::Delete, _) => { (AppMode::Edit, KeyCode::Delete, _) => {
editor.delete_forward()?; editor.delete_forward()?;
// Auto-trigger suggestions after deletion
// Update suggestions after deletion editor.auto_trigger_suggestions(suggestions_provider).await;
if editor.data_provider().supports_suggestions(editor.current_field()) {
let current_text = editor.current_text().to_string();
if current_text.is_empty() {
let _ = editor.trigger_suggestions(suggestions_provider).await;
editor.set_debug_message(format!("{} total suggestions", editor.suggestions().len()));
editor.update_inline_completion();
} else {
match editor.trigger_suggestions(suggestions_provider).await {
Ok(_) => {
if editor.suggestions().is_empty() {
editor.set_debug_message(format!("🔍 No matches for '{}'", current_text));
} else {
editor.set_debug_message(format!("{} matches for '{}'", editor.suggestions().len(), current_text));
}
}
Err(e) => {
editor.set_debug_message(format!("❌ Suggestion error: {}", e));
}
}
editor.update_inline_completion();
}
}
} }
// Delete operations in normal mode (vim x) // Delete operations in normal mode (vim x)
@@ -831,24 +844,8 @@ async fn handle_key_press(
// === CHARACTER INPUT WITH REAL-TIME SUGGESTIONS === // === CHARACTER INPUT WITH REAL-TIME SUGGESTIONS ===
(AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
editor.insert_char(c)?; editor.insert_char(c)?;
// Auto-trigger suggestions after typing - this is the magic!
// Auto-trigger suggestions after typing editor.auto_trigger_suggestions(suggestions_provider).await;
if editor.data_provider().supports_suggestions(editor.current_field()) {
match editor.trigger_suggestions(suggestions_provider).await {
Ok(_) => {
let current_text = editor.current_text().to_string();
if editor.suggestions().is_empty() {
editor.set_debug_message(format!("🔍 No matches for '{}'", current_text));
} else {
editor.set_debug_message(format!("{} matches for '{}'", editor.suggestions().len(), current_text));
}
editor.update_inline_completion();
}
Err(e) => {
editor.set_debug_message(format!("❌ Suggestion error: {}", e));
}
}
}
} }
// === DEBUG/INFO COMMANDS === // === DEBUG/INFO COMMANDS ===
@@ -885,9 +882,9 @@ async fn handle_key_press(
async fn run_app<B: Backend>( async fn run_app<B: Backend>(
terminal: &mut Terminal<B>, terminal: &mut Terminal<B>,
mut editor: AutoCursorFormEditor<MultiFieldDemoData>, mut editor: AutoCursorFormEditor<ApplicationData>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut suggestions_provider = ComprehensiveSuggestionsProvider::new(); let mut suggestions_provider = ProductionSuggestionsProvider::new();
loop { loop {
terminal.draw(|f| ui(f, &editor))?; terminal.draw(|f| ui(f, &editor))?;
@@ -909,7 +906,7 @@ async fn run_app<B: Backend>(
Ok(()) Ok(())
} }
fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<MultiFieldDemoData>) { fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<ApplicationData>) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(12)]) .constraints([Constraint::Min(8), Constraint::Length(12)])
@@ -934,7 +931,7 @@ fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<MultiFieldDemoData>) {
fn render_enhanced_canvas( fn render_enhanced_canvas(
f: &mut Frame, f: &mut Frame,
area: ratatui::layout::Rect, area: ratatui::layout::Rect,
editor: &AutoCursorFormEditor<MultiFieldDemoData>, editor: &AutoCursorFormEditor<ApplicationData>,
) -> Option<ratatui::layout::Rect> { ) -> Option<ratatui::layout::Rect> {
render_canvas_default(f, area, &editor.editor) render_canvas_default(f, area, &editor.editor)
} }
@@ -942,7 +939,7 @@ fn render_enhanced_canvas(
fn render_status_and_help( fn render_status_and_help(
f: &mut Frame, f: &mut Frame,
area: ratatui::layout::Rect, area: ratatui::layout::Rect,
editor: &AutoCursorFormEditor<MultiFieldDemoData>, editor: &AutoCursorFormEditor<ApplicationData>,
) { ) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@@ -981,38 +978,37 @@ fn render_status_and_help(
); );
let status = Paragraph::new(Line::from(Span::raw(status_text))) let status = Paragraph::new(Line::from(Span::raw(status_text)))
.block(Block::default().borders(Borders::ALL).title("🎯 Multi-Field Suggestions Demo")); .block(Block::default().borders(Borders::ALL).title("🚀 Production-Ready Non-Blocking Suggestions"));
f.render_widget(status, chunks[0]); f.render_widget(status, chunks[0]);
// Comprehensive help text // Production help text
let help_text = match editor.mode() { let help_text = match editor.mode() {
AppMode::ReadOnly => { AppMode::ReadOnly => {
"🎯 MULTI-FIELD SUGGESTIONS DEMO: Normal █ | Insert | | Visual █\n\ "🚀 PRODUCTION-READY SUGGESTIONS: Copy this architecture for your app!\n\
Movement: j/k or ↑↓=fields, h/l or ←→=chars, gg/G=first/last, w/b/e=words\n\ Movement: j/k or ↑↓=fields, h/l or ←→=chars, gg/G=first/last, w/b/e=words\n\
Actions: i/a/A=insert, v/V=visual, x/X=delete, ?=info, Enter=next field\n\ Actions: i/a/A=insert, v/V=visual, x/X=delete, ?=info, Enter=next field\n\
🍎 Fruits: Apple, Banana, Cherry... | 💼 Jobs: Engineer, Manager, Designer...\n\ Integration: Replace data sources with your APIs, databases, caches\n\
💻 Languages: Rust, Python, JS... | 🌍 Countries: USA, Canada, UK...\n\ Architecture: Non-blocking • Instant UI • Stale protection • Professional UX\n\
🎨 Colors: Red, Blue, Green... | Tab=suggestions, Enter=select\n\ Tab=suggestions, Enter=select • Ready for: REST, GraphQL, SQL, Redis, etc."
Edge cases to test: empty→suggestions, partial matches, field navigation!"
} }
AppMode::Edit => { AppMode::Edit => {
"🎯 INSERT MODE - Cursor: | (bar)\n\ "🚀 INSERT MODE - Type for instant suggestions!\n\
Type to filter suggestions! Tab=show/cycle, Enter=select, Esc=normal\n\ Real-time search-as-you-type with non-blocking architecture\n\
Test cases: 'r'→Red/Rust, 's'→Software Engineer/Swift, 'c'→Canada/Cherry...\n\ Perfect for: User search, autocomplete, typeahead, smart suggestions\n\
Navigation: arrows=move, Ctrl+arrows=words, Home/End=line edges\n\ Navigation: arrows=move, Ctrl+arrows=words, Home/End=line edges\n\
Try different fields for different suggestion behaviors and timing!" Copy this pattern for production: API calls, database queries, cache lookups"
} }
AppMode::Highlight => { AppMode::Highlight => {
"🎯 VISUAL MODE - Cursor: █ (blinking block)\n\ "🚀 VISUAL MODE - Selection with suggestions support\n\
Selection: hjkl/arrows=extend, w/b/e=word selection, Esc=normal\n\ Selection: hjkl/arrows=extend, w/b/e=word selection, Esc=normal\n\
Test multi-character selections across different suggestion field types!" Professional editor experience with modern autocomplete!"
} }
_ => "🎯 Multi-field suggestions! 5 fields × 8 suggestions each = lots of testing!" _ => "🚀 Copy this suggestions architecture for your production app!"
}; };
let help = Paragraph::new(help_text) let help = Paragraph::new(help_text)
.block(Block::default().borders(Borders::ALL).title("🚀 Comprehensive Testing Guide")) .block(Block::default().borders(Borders::ALL).title("📋 Production Integration Guide"))
.style(Style::default().fg(Color::Gray)); .style(Style::default().fg(Color::Gray));
f.render_widget(help, chunks[1]); f.render_widget(help, chunks[1]);
@@ -1020,27 +1016,26 @@ fn render_status_and_help(
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Print comprehensive demo information // Print production-ready information
println!("🎯 Multi-Field Suggestions Demo - Perfect for Testing Edge Cases!"); println!("🚀 Production-Ready Non-Blocking Suggestions Demo");
println!("cursor-style feature: ENABLED"); println!("Instant, responsive UI - no blocking on network/database calls");
println!("suggestions feature: ENABLED"); println!("Professional autocomplete architecture");
println!("🚀 Automatic cursor management: ACTIVE"); println!("✅ Copy this pattern for your production application!");
println!("✨ 5 different suggestion types: ACTIVE");
println!(); println!();
println!("📋 Test These 5 Fields:"); println!("🏗️ Integration Ready For:");
println!(" 🍎 Fruits: Apple, Banana, Cherry, Date, Elderberry, Fig, Grape, Honeydew"); println!(" 📡 REST APIs (reqwest, hyper)");
println!(" 💼 Jobs: Software Engineer, Product Manager, Data Scientist, UX Designer..."); println!(" 🗄️ Databases (sqlx, diesel, mongodb)");
println!(" 💻 Languages: Rust, Python, JavaScript, TypeScript, Go, Java, C++, Swift"); println!(" 🔍 Search Engines (elasticsearch, algolia, typesense)");
println!(" 🌍 Countries: USA, Canada, UK, Germany, France, Japan, Australia, Brazil"); println!(" 💾 Caches (redis, memcached)");
println!(" 🎨 Colors: Red, Blue, Green, Yellow, Purple, Orange, Pink, Black"); println!(" 🌐 GraphQL APIs");
println!(" 🔗 gRPC Services");
println!(); println!();
println!("🧪 Edge Cases to Test:"); println!("⚡ Key Features:");
println!("Navigation between suggestion/non-suggestion fields"); println!("Dropdown appears instantly (never waits for network)");
println!("Empty field → Tab → see all suggestions"); println!("Built-in stale result protection");
println!("Partial typing → Tab → filtered suggestions"); println!("Search-as-you-type with real-time filtering");
println!("Different loading times per field (100-300ms)"); println!("Professional-grade user experience");
println!("Field switching while suggestions active"); println!("Easy to integrate with any async data source");
println!(" • Visual mode selections across suggestion fields");
println!(); println!();
enable_raw_mode()?; enable_raw_mode()?;
@@ -1049,7 +1044,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let data = MultiFieldDemoData::new(); let data = ApplicationData::new();
let mut editor = AutoCursorFormEditor::new(data); let mut editor = AutoCursorFormEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor // Initialize with normal mode - library automatically sets block cursor
@@ -1076,6 +1071,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{:?}", err); println!("{:?}", err);
} }
println!("🎯 Multi-field testing complete! Great for finding edge cases!"); println!("🚀 Ready to integrate this architecture into your production app!");
Ok(()) Ok(())
} }

View File

@@ -35,6 +35,7 @@ pub struct SuggestionsUIState {
pub(crate) is_loading: bool, pub(crate) is_loading: bool,
pub(crate) selected_index: Option<usize>, pub(crate) selected_index: Option<usize>,
pub(crate) active_field: Option<usize>, pub(crate) active_field: Option<usize>,
pub(crate) active_query: Option<String>,
pub(crate) completion_text: Option<String>, pub(crate) completion_text: Option<String>,
} }
@@ -57,6 +58,7 @@ impl EditorState {
is_loading: false, is_loading: false,
selected_index: None, selected_index: None,
active_field: None, active_field: None,
active_query: None,
completion_text: None, completion_text: None,
}, },
selection: SelectionState::None, selection: SelectionState::None,
@@ -150,29 +152,12 @@ impl EditorState {
self.ideal_cursor_column = self.cursor_pos; self.ideal_cursor_column = self.cursor_pos;
} }
/// Legacy internal activation (still used internally if needed)
pub(crate) fn activate_suggestions(&mut self, field_index: usize) {
self.suggestions.is_active = true;
self.suggestions.is_loading = true;
self.suggestions.active_field = Some(field_index);
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
}
/// Legacy internal deactivation
pub(crate) fn deactivate_suggestions(&mut self) {
self.suggestions.is_active = false;
self.suggestions.is_loading = false;
self.suggestions.active_field = None;
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
}
/// Explicitly open suggestions — should only be called on Tab /// Explicitly open suggestions — should only be called on Tab
pub(crate) fn open_suggestions(&mut self, field_index: usize) { pub(crate) fn open_suggestions(&mut self, field_index: usize) {
self.suggestions.is_active = true; self.suggestions.is_active = true;
self.suggestions.is_loading = true; self.suggestions.is_loading = true;
self.suggestions.active_field = Some(field_index); self.suggestions.active_field = Some(field_index);
self.suggestions.active_query = None;
self.suggestions.selected_index = None; self.suggestions.selected_index = None;
self.suggestions.completion_text = None; self.suggestions.completion_text = None;
} }
@@ -182,6 +167,7 @@ impl EditorState {
self.suggestions.is_active = false; self.suggestions.is_active = false;
self.suggestions.is_loading = false; self.suggestions.is_loading = false;
self.suggestions.active_field = None; self.suggestions.active_field = None;
self.suggestions.active_query = None;
self.suggestions.selected_index = None; self.suggestions.selected_index = None;
self.suggestions.completion_text = None; self.suggestions.completion_text = None;
} }

View File

@@ -796,33 +796,99 @@ impl<D: DataProvider> FormEditor<D> {
// ASYNC OPERATIONS: Only suggestions need async // ASYNC OPERATIONS: Only suggestions need async
// =================================================================== // ===================================================================
/// Trigger suggestions (async because it fetches data) // NOTE: trigger_suggestions (the async fetch helper) was removed in favor of
pub async fn trigger_suggestions<A>(&mut self, provider: &mut A) -> Result<()> // the non-blocking start_suggestions / apply_suggestions_result API.
where
A: SuggestionsProvider,
{
let field_index = self.ui_state.current_field;
/// Trigger suggestions (async because it fetches data)
/// (Removed - use start_suggestions + apply_suggestions_result instead)
// ===================================================================
// NON-BLOCKING SUGGESTIONS API (ONLY API)
// ===================================================================
/// Begin suggestions loading for a field (UI updates immediately, no fetch)
/// This opens the dropdown with "Loading..." state instantly
///
/// The caller is responsible for fetching suggestions and calling
/// `apply_suggestions_result()` when ready.
pub fn start_suggestions(&mut self, field_index: usize) -> Option<String> {
if !self.data_provider.supports_suggestions(field_index) { if !self.data_provider.supports_suggestions(field_index) {
return Ok(()); return None;
} }
// Activate suggestions UI let query = self.current_text().to_string();
self.ui_state.activate_suggestions(field_index);
// Open suggestions UI immediately - user sees dropdown right away
self.ui_state.open_suggestions(field_index);
// ADD THIS LINE - mark as loading so UI shows "Loading..."
self.ui_state.suggestions.is_loading = true;
// Store the query we're loading for (prevents stale results)
self.ui_state.suggestions.active_query = Some(query.clone());
// Clear any old suggestions
self.suggestions.clear();
// Return the query so caller knows what to fetch
Some(query)
}
// Fetch suggestions from user (no conversion needed!) /// Apply fetched suggestions results
let query = self.current_text(); ///
self.suggestions = provider.fetch_suggestions(field_index, query).await?; /// This will ignore stale results if the field or query has changed since
/// `start_suggestions()` was called.
///
/// Returns `true` if results were applied, `false` if they were stale/ignored.
pub fn apply_suggestions_result(
&mut self,
field_index: usize,
query: &str,
results: Vec<SuggestionItem>,
) -> bool {
// Ignore stale results: wrong field
if self.ui_state.suggestions.active_field != Some(field_index) {
return false;
}
// Update UI state // Ignore stale results: query has changed
if self.ui_state.suggestions.active_query.as_deref() != Some(query) {
return false;
}
// Apply results
self.ui_state.suggestions.is_loading = false; self.ui_state.suggestions.is_loading = false;
self.suggestions = results;
if !self.suggestions.is_empty() { if !self.suggestions.is_empty() {
self.ui_state.suggestions.selected_index = Some(0); self.ui_state.suggestions.selected_index = Some(0);
// Compute initial inline completion from first suggestion
self.update_inline_completion(); self.update_inline_completion();
} else {
self.ui_state.suggestions.selected_index = None;
self.ui_state.suggestions.completion_text = None;
} }
Ok(()) true
}
/// Check if there's an active suggestions query waiting for results
///
/// Returns (field_index, query) if suggestions are loading, None otherwise.
pub fn pending_suggestions_query(&self) -> Option<(usize, String)> {
if self.ui_state.suggestions.is_loading {
if let (Some(field), Some(query)) = (
self.ui_state.suggestions.active_field,
&self.ui_state.suggestions.active_query
) {
return Some((field, query.clone()));
}
}
None
}
/// Cancel any pending suggestions (useful for cleanup)
pub fn cancel_suggestions(&mut self) {
self.close_suggestions();
} }
/// Navigate suggestions /// Navigate suggestions
@@ -857,7 +923,7 @@ impl<D: DataProvider> FormEditor<D> {
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
// Close suggestions // Close suggestions
self.ui_state.deactivate_suggestions(); self.close_suggestions();
self.suggestions.clear(); self.suggestions.clear();
// Validate the new content if validation is enabled // Validate the new content if validation is enabled
@@ -1257,7 +1323,7 @@ impl<D: DataProvider> FormEditor<D> {
self.set_mode(AppMode::ReadOnly); self.set_mode(AppMode::ReadOnly);
// Deactivate suggestions when exiting edit mode // Deactivate suggestions when exiting edit mode
self.ui_state.deactivate_suggestions(); self.close_suggestions();
Ok(()) Ok(())
} }