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
//! 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:
//! cargo run --example suggestions2 --features "gui,cursor-style,suggestions"
@@ -35,7 +39,7 @@ use ratatui::{
use canvas::{
canvas::{
gui::render_canvas_default,
modes::{AppMode, ModeManager, HighlightState},
modes::AppMode,
CursorManager, // This import only exists when cursor-style feature is enabled
},
suggestions::gui::render_suggestions_dropdown,
@@ -45,7 +49,7 @@ use canvas::{
use async_trait::async_trait;
use anyhow::Result;
// Enhanced FormEditor that demonstrates automatic cursor management + SUGGESTIONS
// Enhanced FormEditor that demonstrates professional suggestions architecture
struct AutoCursorFormEditor<D: DataProvider> {
editor: FormEditor<D>,
has_unsaved_changes: bool,
@@ -58,7 +62,7 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
Self {
editor: FormEditor::new(data_provider),
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(),
}
}
@@ -88,19 +92,16 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
// === 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();
}
@@ -132,12 +133,12 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
// === ENHANCED MOVEMENT WITH VISUAL UPDATES ===
fn move_left(&mut self) {
self.editor.move_left();
let _ = self.editor.move_left();
self.update_visual_selection();
}
fn move_right(&mut self) {
self.editor.move_right();
let _ = self.editor.move_right();
self.update_visual_selection();
}
@@ -182,12 +183,12 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
}
fn move_first_line(&mut self) {
self.editor.move_first_line();
let _ = self.editor.move_first_line();
self.update_visual_selection();
}
fn move_last_line(&mut self) {
self.editor.move_last_line();
let _ = self.editor.move_last_line();
self.update_visual_selection();
}
@@ -253,13 +254,50 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
Ok(result?)
}
// === SUGGESTIONS SUPPORT ===
// === PRODUCTION-READY NON-BLOCKING SUGGESTIONS ===
async fn trigger_suggestions<A>(&mut self, provider: &mut A) -> anyhow::Result<()>
where
A: SuggestionsProvider,
{
self.editor.trigger_suggestions(provider).await
/// Trigger suggestions with non-blocking approach (production pattern)
///
/// This method demonstrates the proper way to integrate suggestions with
/// real APIs, databases, or any async data source without blocking the UI.
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) {
@@ -284,16 +322,13 @@ impl<D: DataProvider> AutoCursorFormEditor<D> {
// === 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(())
@@ -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)>,
}
impl MultiFieldDemoData {
impl ApplicationData {
fn new() -> Self {
Self {
fields: vec![
("🍎 Favorite Fruit".to_string(), "".to_string()), // Field 0: Fruits
("💼 Job Role".to_string(), "".to_string()), // Field 1: Jobs
("💻 Programming Language".to_string(), "".to_string()), // Field 2: Languages
("🌍 Country".to_string(), "".to_string()), // Field 3: Countries
("🎨 Favorite Color".to_string(), "".to_string()), // Field 4: Colors
("🍎 Favorite Fruit".to_string(), "".to_string()),
("💼 Job Role".to_string(), "".to_string()),
("💻 Programming Language".to_string(), "".to_string()),
("🌍 Country".to_string(), "".to_string()),
("🎨 Favorite Color".to_string(), "".to_string()),
],
}
}
}
impl DataProvider for MultiFieldDemoData {
impl DataProvider for ApplicationData {
fn field_count(&self) -> usize {
self.fields.len()
}
@@ -388,7 +423,7 @@ impl DataProvider for MultiFieldDemoData {
}
fn supports_suggestions(&self, field_index: usize) -> bool {
// All 5 fields support suggestions - perfect for testing!
// Configure which fields support suggestions
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 {
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)
fn get_fruit_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
/// Get fruit suggestions (replace with your API call)
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![
("Apple", "🍎 Crisp and sweet"),
("Banana", "🍌 Rich in potassium"),
@@ -419,15 +483,22 @@ impl ComprehensiveSuggestionsProvider {
("Fig", "🍇 Sweet Mediterranean fruit"),
("Grape", "🍇 Perfect for wine"),
("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)
fn get_job_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
/// Get job suggestions (replace with your database query)
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![
("Software Engineer", "👨‍💻 Build applications"),
("Product Manager", "📋 Manage product roadmap"),
@@ -439,11 +510,17 @@ impl ComprehensiveSuggestionsProvider {
("Accountant", "💼 Manage finances"),
];
self.filter_suggestions(jobs, query, "role")
Ok(self.filter_suggestions(jobs, query))
}
/// Get programming language suggestions (field 2)
fn get_language_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
/// Get language suggestions (replace with your cache lookup)
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![
("Rust", "🦀 Systems programming"),
("Python", "🐍 Versatile and popular"),
@@ -455,11 +532,18 @@ impl ComprehensiveSuggestionsProvider {
("Swift", "🍎 iOS development"),
];
self.filter_suggestions(languages, query, "language")
Ok(self.filter_suggestions(languages, query))
}
/// Get country suggestions (field 3)
fn get_country_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
/// Get country suggestions (replace with your geographic API)
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![
("United States", "🇺🇸 North America"),
("Canada", "🇨🇦 Great neighbors"),
@@ -471,11 +555,11 @@ impl ComprehensiveSuggestionsProvider {
("Brazil", "🇧🇷 Carnival country"),
];
self.filter_suggestions(countries, query, "country")
Ok(self.filter_suggestions(countries, query))
}
/// Get color suggestions (field 4)
fn get_color_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
/// Get color suggestions (local data)
async fn get_color_suggestions(&self, query: &str) -> Result<Vec<SuggestionItem>> {
let colors = vec![
("Red", "🔴 Bold and energetic"),
("Blue", "🔵 Calm and trustworthy"),
@@ -487,11 +571,11 @@ impl ComprehensiveSuggestionsProvider {
("Black", "⚫ Classic and elegant"),
];
self.filter_suggestions(colors, query, "color")
Ok(self.filter_suggestions(colors, query))
}
/// Generic filtering helper
fn filter_suggestions(&self, items: Vec<(&str, &str)>, query: &str, _category: &str) -> Vec<SuggestionItem> {
/// Generic filtering helper (reusable for any data source)
fn filter_suggestions(&self, items: Vec<(&str, &str)>, query: &str) -> Vec<SuggestionItem> {
let query_lower = query.to_lowercase();
items.iter()
@@ -507,38 +591,26 @@ impl ComprehensiveSuggestionsProvider {
}
#[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>> {
// Simulate different network delays for different fields (realistic!)
let delay_ms = match field_index {
0 => 100, // Fruits: local data
1 => 200, // Jobs: medium API call
2 => 150, // Languages: cached data
3 => 300, // Countries: slow geographic API
4 => 80, // Colors: instant local
_ => 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)
match field_index {
0 => self.get_fruit_suggestions(query).await, // API call
1 => self.get_job_suggestions(query).await, // Database query
2 => self.get_language_suggestions(query).await, // Cache + API
3 => self.get_country_suggestions(query).await, // Geographic API
4 => self.get_color_suggestions(query).await, // Local data
_ => Ok(Vec::new()),
}
}
}
/// Multi-field suggestions demonstration + automatic cursor management
/// Production-ready key handling with non-blocking suggestions
async fn handle_key_press(
key: KeyCode,
modifiers: KeyModifiers,
editor: &mut AutoCursorFormEditor<MultiFieldDemoData>,
suggestions_provider: &mut ComprehensiveSuggestionsProvider,
editor: &mut AutoCursorFormEditor<ApplicationData>,
suggestions_provider: &mut ProductionSuggestionsProvider,
) -> anyhow::Result<bool> {
let mode = editor.mode();
@@ -551,27 +623,16 @@ async fn handle_key_press(
}
match (mode, key, modifiers) {
// === SUGGESTIONS HANDLING ===
// === NON-BLOCKING SUGGESTIONS HANDLING ===
(_, KeyCode::Tab, _) => {
if editor.is_suggestions_active() {
// Cycle through suggestions
editor.suggestions_next();
editor.set_debug_message("📍 Next suggestion".to_string());
} else if editor.data_provider().supports_suggestions(editor.current_field()) {
// Open suggestions explicitly
editor.open_suggestions(editor.current_field());
match editor.trigger_suggestions(suggestions_provider).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));
}
}
// Trigger non-blocking suggestions
let field_index = editor.current_field();
editor.trigger_suggestions_async(suggestions_provider, field_index).await;
} else {
editor.next_field();
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'), _) => {
editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar |
editor.enter_edit_mode();
editor.clear_command_buffer();
// Auto-show suggestions on entering insert mode
if editor.data_provider().supports_suggestions(editor.current_field()) {
let _ = editor.trigger_suggestions(suggestions_provider).await;
editor.update_inline_completion();
}
editor.auto_trigger_suggestions(suggestions_provider).await;
}
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
editor.enter_append_mode();
@@ -632,7 +689,7 @@ async fn handle_key_press(
}
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
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.clear_command_buffer();
}
@@ -762,60 +819,16 @@ async fn handle_key_press(
editor.move_line_end();
}
// === DELETE OPERATIONS ===
// === DELETE OPERATIONS WITH AUTO-SUGGESTIONS ===
(AppMode::Edit, KeyCode::Backspace, _) => {
editor.delete_backward()?;
// Update suggestions after deletion
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();
}
}
// Auto-trigger suggestions after deletion
editor.auto_trigger_suggestions(suggestions_provider).await;
}
(AppMode::Edit, KeyCode::Delete, _) => {
editor.delete_forward()?;
// Update suggestions after deletion
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();
}
}
// Auto-trigger suggestions after deletion
editor.auto_trigger_suggestions(suggestions_provider).await;
}
// Delete operations in normal mode (vim x)
@@ -831,24 +844,8 @@ async fn handle_key_press(
// === CHARACTER INPUT WITH REAL-TIME SUGGESTIONS ===
(AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
editor.insert_char(c)?;
// Auto-trigger suggestions after typing
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));
}
}
}
// Auto-trigger suggestions after typing - this is the magic!
editor.auto_trigger_suggestions(suggestions_provider).await;
}
// === DEBUG/INFO COMMANDS ===
@@ -885,9 +882,9 @@ async fn handle_key_press(
async fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut editor: AutoCursorFormEditor<MultiFieldDemoData>,
mut editor: AutoCursorFormEditor<ApplicationData>,
) -> io::Result<()> {
let mut suggestions_provider = ComprehensiveSuggestionsProvider::new();
let mut suggestions_provider = ProductionSuggestionsProvider::new();
loop {
terminal.draw(|f| ui(f, &editor))?;
@@ -909,7 +906,7 @@ async fn run_app<B: Backend>(
Ok(())
}
fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<MultiFieldDemoData>) {
fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<ApplicationData>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(12)])
@@ -934,7 +931,7 @@ fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<MultiFieldDemoData>) {
fn render_enhanced_canvas(
f: &mut Frame,
area: ratatui::layout::Rect,
editor: &AutoCursorFormEditor<MultiFieldDemoData>,
editor: &AutoCursorFormEditor<ApplicationData>,
) -> Option<ratatui::layout::Rect> {
render_canvas_default(f, area, &editor.editor)
}
@@ -942,7 +939,7 @@ fn render_enhanced_canvas(
fn render_status_and_help(
f: &mut Frame,
area: ratatui::layout::Rect,
editor: &AutoCursorFormEditor<MultiFieldDemoData>,
editor: &AutoCursorFormEditor<ApplicationData>,
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -981,38 +978,37 @@ fn render_status_and_help(
);
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]);
// Comprehensive help text
// Production help text
let help_text = match editor.mode() {
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\
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\
💻 Languages: Rust, Python, JS... | 🌍 Countries: USA, Canada, UK...\n\
🎨 Colors: Red, Blue, Green... | Tab=suggestions, Enter=select\n\
Edge cases to test: empty→suggestions, partial matches, field navigation!"
Integration: Replace data sources with your APIs, databases, caches\n\
Architecture: Non-blocking • Instant UI • Stale protection • Professional UX\n\
Tab=suggestions, Enter=select • Ready for: REST, GraphQL, SQL, Redis, etc."
}
AppMode::Edit => {
"🎯 INSERT MODE - Cursor: | (bar)\n\
Type to filter suggestions! Tab=show/cycle, Enter=select, Esc=normal\n\
Test cases: 'r'→Red/Rust, 's'→Software Engineer/Swift, 'c'→Canada/Cherry...\n\
"🚀 INSERT MODE - Type for instant suggestions!\n\
Real-time search-as-you-type with non-blocking architecture\n\
Perfect for: User search, autocomplete, typeahead, smart suggestions\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 => {
"🎯 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\
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)
.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));
f.render_widget(help, chunks[1]);
@@ -1020,27 +1016,26 @@ fn render_status_and_help(
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Print comprehensive demo information
println!("🎯 Multi-Field Suggestions Demo - Perfect for Testing Edge Cases!");
println!("cursor-style feature: ENABLED");
println!("suggestions feature: ENABLED");
println!("🚀 Automatic cursor management: ACTIVE");
println!("✨ 5 different suggestion types: ACTIVE");
// Print production-ready information
println!("🚀 Production-Ready Non-Blocking Suggestions Demo");
println!("Instant, responsive UI - no blocking on network/database calls");
println!("Professional autocomplete architecture");
println!("✅ Copy this pattern for your production application!");
println!();
println!("📋 Test These 5 Fields:");
println!(" 🍎 Fruits: Apple, Banana, Cherry, Date, Elderberry, Fig, Grape, Honeydew");
println!(" 💼 Jobs: Software Engineer, Product Manager, Data Scientist, UX Designer...");
println!(" 💻 Languages: Rust, Python, JavaScript, TypeScript, Go, Java, C++, Swift");
println!(" 🌍 Countries: USA, Canada, UK, Germany, France, Japan, Australia, Brazil");
println!(" 🎨 Colors: Red, Blue, Green, Yellow, Purple, Orange, Pink, Black");
println!("🏗️ Integration Ready For:");
println!(" 📡 REST APIs (reqwest, hyper)");
println!(" 🗄️ Databases (sqlx, diesel, mongodb)");
println!(" 🔍 Search Engines (elasticsearch, algolia, typesense)");
println!(" 💾 Caches (redis, memcached)");
println!(" 🌐 GraphQL APIs");
println!(" 🔗 gRPC Services");
println!();
println!("🧪 Edge Cases to Test:");
println!("Navigation between suggestion/non-suggestion fields");
println!("Empty field → Tab → see all suggestions");
println!("Partial typing → Tab → filtered suggestions");
println!("Different loading times per field (100-300ms)");
println!("Field switching while suggestions active");
println!(" • Visual mode selections across suggestion fields");
println!("⚡ Key Features:");
println!("Dropdown appears instantly (never waits for network)");
println!("Built-in stale result protection");
println!("Search-as-you-type with real-time filtering");
println!("Professional-grade user experience");
println!("Easy to integrate with any async data source");
println!();
enable_raw_mode()?;
@@ -1049,7 +1044,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let data = MultiFieldDemoData::new();
let data = ApplicationData::new();
let mut editor = AutoCursorFormEditor::new(data);
// 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!("🎯 Multi-field testing complete! Great for finding edge cases!");
println!("🚀 Ready to integrate this architecture into your production app!");
Ok(())
}

View File

@@ -35,6 +35,7 @@ pub struct SuggestionsUIState {
pub(crate) is_loading: bool,
pub(crate) selected_index: Option<usize>,
pub(crate) active_field: Option<usize>,
pub(crate) active_query: Option<String>,
pub(crate) completion_text: Option<String>,
}
@@ -57,6 +58,7 @@ impl EditorState {
is_loading: false,
selected_index: None,
active_field: None,
active_query: None,
completion_text: None,
},
selection: SelectionState::None,
@@ -150,29 +152,12 @@ impl EditorState {
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
pub(crate) fn open_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.active_query = None;
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
}
@@ -182,6 +167,7 @@ impl EditorState {
self.suggestions.is_active = false;
self.suggestions.is_loading = false;
self.suggestions.active_field = None;
self.suggestions.active_query = None;
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
}

View File

@@ -796,33 +796,99 @@ impl<D: DataProvider> FormEditor<D> {
// ASYNC OPERATIONS: Only suggestions need async
// ===================================================================
/// Trigger suggestions (async because it fetches data)
pub async fn trigger_suggestions<A>(&mut self, provider: &mut A) -> Result<()>
where
A: SuggestionsProvider,
{
let field_index = self.ui_state.current_field;
// NOTE: trigger_suggestions (the async fetch helper) was removed in favor of
// the non-blocking start_suggestions / apply_suggestions_result API.
/// 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) {
return Ok(());
return None;
}
// Activate suggestions UI
self.ui_state.activate_suggestions(field_index);
let query = self.current_text().to_string();
// 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!)
let query = self.current_text();
self.suggestions = provider.fetch_suggestions(field_index, query).await?;
/// Apply fetched suggestions results
///
/// 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.suggestions = results;
if !self.suggestions.is_empty() {
self.ui_state.suggestions.selected_index = Some(0);
// Compute initial inline completion from first suggestion
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
@@ -857,7 +923,7 @@ impl<D: DataProvider> FormEditor<D> {
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
// Close suggestions
self.ui_state.deactivate_suggestions();
self.close_suggestions();
self.suggestions.clear();
// Validate the new content if validation is enabled
@@ -1257,7 +1323,7 @@ impl<D: DataProvider> FormEditor<D> {
self.set_mode(AppMode::ReadOnly);
// Deactivate suggestions when exiting edit mode
self.ui_state.deactivate_suggestions();
self.close_suggestions();
Ok(())
}