sugggestions are agnostic
This commit is contained in:
@@ -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;
|
||||
|
||||
impl ComprehensiveSuggestionsProvider {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Get fruit suggestions (field 0)
|
||||
fn get_fruit_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
|
||||
impl ProductionSuggestionsProvider {
|
||||
fn new() -> 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 (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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
// Fetch suggestions from user (no conversion needed!)
|
||||
let query = self.current_text();
|
||||
self.suggestions = provider.fetch_suggestions(field_index, query).await?;
|
||||
// Open suggestions UI immediately - user sees dropdown right away
|
||||
self.ui_state.open_suggestions(field_index);
|
||||
|
||||
// Update UI state
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user