Files
komp_ac/canvas/examples/suggestions2.rs
2025-08-10 16:10:45 +02:00

1082 lines
39 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// examples/suggestions2.rs
//! Demonstrates automatic cursor management + MULTIPLE SUGGESTION FIELDS
//!
//! This example REQUIRES the `cursor-style` feature to compile.
//!
//! Run with:
//! cargo run --example suggestions2 --features "gui,cursor-style,suggestions"
// REQUIRE cursor-style feature - example won't compile without it
#[cfg(not(feature = "cursor-style"))]
compile_error!(
"This example requires the 'cursor-style' feature. \
Run with: cargo run --example suggestions2 --features \"gui,cursor-style,suggestions\""
);
use std::io;
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use canvas::{
canvas::{
gui::render_canvas_default,
modes::{AppMode, ModeManager, HighlightState},
CursorManager, // This import only exists when cursor-style feature is enabled
},
suggestions::gui::render_suggestions_dropdown,
DataProvider, FormEditor, SuggestionsProvider, SuggestionItem,
};
use async_trait::async_trait;
use anyhow::Result;
// Enhanced FormEditor that demonstrates automatic cursor management + SUGGESTIONS
struct AutoCursorFormEditor<D: DataProvider> {
editor: FormEditor<D>,
has_unsaved_changes: bool,
debug_message: String,
command_buffer: String, // For multi-key vim commands like "gg"
}
impl<D: DataProvider> AutoCursorFormEditor<D> {
fn new(data_provider: D) -> Self {
Self {
editor: FormEditor::new(data_provider),
has_unsaved_changes: false,
debug_message: "🎯 Multi-Field Suggestions Demo - 5 fields with different suggestions!".to_string(),
command_buffer: String::new(),
}
}
fn close_suggestions(&mut self) {
self.editor.close_suggestions();
}
// === COMMAND BUFFER HANDLING ===
fn clear_command_buffer(&mut self) {
self.command_buffer.clear();
}
fn add_to_command_buffer(&mut self, ch: char) {
self.command_buffer.push(ch);
}
fn get_command_buffer(&self) -> &str {
&self.command_buffer
}
fn has_pending_command(&self) -> bool {
!self.command_buffer.is_empty()
}
// === VISUAL/HIGHLIGHT MODE SUPPORT ===
fn enter_visual_mode(&mut self) {
// Use the library method instead of manual state setting
self.editor.enter_highlight_mode();
self.debug_message = "🔥 VISUAL MODE - Cursor: Blinking Block █".to_string();
}
fn enter_visual_line_mode(&mut self) {
// Use the library method instead of manual state setting
self.editor.enter_highlight_line_mode();
self.debug_message = "🔥 VISUAL LINE MODE - Cursor: Blinking Block █".to_string();
}
fn exit_visual_mode(&mut self) {
// Use the library method
self.editor.exit_highlight_mode();
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
}
fn update_visual_selection(&mut self) {
if self.editor.is_highlight_mode() {
use canvas::canvas::state::SelectionState;
match self.editor.selection_state() {
SelectionState::Characterwise { anchor } => {
self.debug_message = format!(
"🎯 Visual selection: anchor=({},{}) current=({},{}) - Cursor: Blinking Block █",
anchor.0, anchor.1,
self.editor.current_field(),
self.editor.cursor_position()
);
}
SelectionState::Linewise { anchor_field } => {
self.debug_message = format!(
"🎯 Visual LINE selection: anchor={} current={} - Cursor: Blinking Block █",
anchor_field,
self.editor.current_field()
);
}
_ => {}
}
}
}
// === ENHANCED MOVEMENT WITH VISUAL UPDATES ===
fn move_left(&mut self) {
self.editor.move_left();
self.update_visual_selection();
}
fn move_right(&mut self) {
self.editor.move_right();
self.update_visual_selection();
}
fn move_up(&mut self) {
let _ = self.editor.move_up();
self.update_visual_selection();
}
fn move_down(&mut self) {
let _ = self.editor.move_down();
self.update_visual_selection();
}
fn move_word_next(&mut self) {
self.editor.move_word_next();
self.update_visual_selection();
}
fn move_word_prev(&mut self) {
self.editor.move_word_prev();
self.update_visual_selection();
}
fn move_word_end(&mut self) {
self.editor.move_word_end();
self.update_visual_selection();
}
fn move_word_end_prev(&mut self) {
self.editor.move_word_end_prev();
self.update_visual_selection();
}
fn move_line_start(&mut self) {
self.editor.move_line_start();
self.update_visual_selection();
}
fn move_line_end(&mut self) {
self.editor.move_line_end();
self.update_visual_selection();
}
fn move_first_line(&mut self) {
self.editor.move_first_line();
self.update_visual_selection();
}
fn move_last_line(&mut self) {
self.editor.move_last_line();
self.update_visual_selection();
}
fn prev_field(&mut self) {
let _ = self.editor.prev_field();
self.update_visual_selection();
}
fn next_field(&mut self) {
let _ = self.editor.next_field();
self.update_visual_selection();
}
// === DELETE OPERATIONS ===
fn delete_backward(&mut self) -> anyhow::Result<()> {
let result = self.editor.delete_backward();
if result.is_ok() {
self.has_unsaved_changes = true;
self.debug_message = "⌫ Deleted character backward".to_string();
}
Ok(result?)
}
fn delete_forward(&mut self) -> anyhow::Result<()> {
let result = self.editor.delete_forward();
if result.is_ok() {
self.has_unsaved_changes = true;
self.debug_message = "⌦ Deleted character forward".to_string();
}
Ok(result?)
}
// === SUGGESTIONS CONTROL WRAPPERS ===
fn open_suggestions(&mut self, field_index: usize) {
self.editor.open_suggestions(field_index);
}
// === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT ===
fn enter_edit_mode(&mut self) {
self.editor.enter_edit_mode(); // 🎯 Library automatically sets cursor to bar |
self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar |".to_string();
}
fn enter_append_mode(&mut self) {
self.editor.enter_append_mode(); // 🎯 Library automatically positions cursor and sets mode
self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar |".to_string();
}
fn exit_edit_mode(&mut self) {
let _ = self.editor.exit_edit_mode(); // 🎯 Library automatically sets cursor to block █
self.exit_visual_mode();
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
}
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
let result = self.editor.insert_char(ch);
if result.is_ok() {
self.has_unsaved_changes = true;
}
Ok(result?)
}
// === SUGGESTIONS SUPPORT ===
async fn trigger_suggestions<A>(&mut self, provider: &mut A) -> anyhow::Result<()>
where
A: SuggestionsProvider,
{
self.editor.trigger_suggestions(provider).await
}
fn suggestions_next(&mut self) {
self.editor.suggestions_next();
}
fn apply_suggestion(&mut self) -> Option<String> {
self.editor.apply_suggestion()
}
fn is_suggestions_active(&self) -> bool {
self.editor.is_suggestions_active()
}
fn suggestions(&self) -> &[SuggestionItem] {
self.editor.suggestions()
}
pub fn update_inline_completion(&mut self) {
self.editor.update_inline_completion();
}
// === MANUAL CURSOR OVERRIDE DEMONSTRATION ===
/// Demonstrate manual cursor control (for advanced users)
fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> {
// Users can still manually control cursor if needed
CursorManager::update_for_mode(AppMode::Command)?;
self.debug_message = "🔧 Manual override: Command cursor _".to_string();
Ok(())
}
fn restore_automatic_cursor(&mut self) -> std::io::Result<()> {
// Restore automatic cursor based on current mode
CursorManager::update_for_mode(self.editor.mode())?;
self.debug_message = "🎯 Restored automatic cursor management".to_string();
Ok(())
}
// === DELEGATE TO ORIGINAL EDITOR ===
fn current_field(&self) -> usize {
self.editor.current_field()
}
fn cursor_position(&self) -> usize {
self.editor.cursor_position()
}
fn mode(&self) -> AppMode {
self.editor.mode()
}
fn current_text(&self) -> &str {
let field_index = self.editor.current_field();
self.editor.data_provider().field_value(field_index)
}
fn data_provider(&self) -> &D {
self.editor.data_provider()
}
fn ui_state(&self) -> &canvas::EditorState {
self.editor.ui_state()
}
fn set_mode(&mut self, mode: AppMode) {
self.editor.set_mode(mode); // 🎯 Library automatically updates cursor
if mode != AppMode::Highlight {
self.exit_visual_mode();
}
}
// === STATUS AND DEBUG ===
fn set_debug_message(&mut self, msg: String) {
self.debug_message = msg;
}
fn debug_message(&self) -> &str {
&self.debug_message
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
}
// ===================================================================
// MULTI-FIELD DEMO DATA - 5 different types of suggestion fields
// ===================================================================
struct MultiFieldDemoData {
fields: Vec<(String, String)>,
}
impl MultiFieldDemoData {
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
],
}
}
}
impl DataProvider for MultiFieldDemoData {
fn field_count(&self) -> usize {
self.fields.len()
}
fn field_name(&self, index: usize) -> &str {
&self.fields[index].0
}
fn field_value(&self, index: usize) -> &str {
&self.fields[index].1
}
fn set_field_value(&mut self, index: usize, value: String) {
self.fields[index].1 = value;
}
fn supports_suggestions(&self, field_index: usize) -> bool {
// All 5 fields support suggestions - perfect for testing!
field_index < 5
}
fn display_value(&self, _index: usize) -> Option<&str> {
None
}
}
// ===================================================================
// COMPREHENSIVE SUGGESTIONS PROVIDER - 5 different suggestion types!
// ===================================================================
struct ComprehensiveSuggestionsProvider;
impl ComprehensiveSuggestionsProvider {
fn new() -> Self {
Self
}
/// Get fruit suggestions (field 0)
fn get_fruit_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
let fruits = vec![
("Apple", "🍎 Crisp and sweet"),
("Banana", "🍌 Rich in potassium"),
("Cherry", "🍒 Small and tart"),
("Date", "📅 Sweet and chewy"),
("Elderberry", "🫐 Dark purple berry"),
("Fig", "🍇 Sweet Mediterranean fruit"),
("Grape", "🍇 Perfect for wine"),
("Honeydew", "🍈 Sweet melon"),
("Ananas", "🍎 Crisp and sweet"),
("Avocado", "🍈 Sweet melon"),
];
self.filter_suggestions(fruits, query, "fruit")
}
/// Get job role suggestions (field 1)
fn get_job_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
let jobs = vec![
("Software Engineer", "👨‍💻 Build applications"),
("Product Manager", "📋 Manage product roadmap"),
("Data Scientist", "📊 Analyze data patterns"),
("UX Designer", "🎨 Design user experiences"),
("DevOps Engineer", "⚙️ Manage infrastructure"),
("Marketing Manager", "📢 Drive growth"),
("Sales Representative", "💰 Generate revenue"),
("Accountant", "💼 Manage finances"),
];
self.filter_suggestions(jobs, query, "role")
}
/// Get programming language suggestions (field 2)
fn get_language_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
let languages = vec![
("Rust", "🦀 Systems programming"),
("Python", "🐍 Versatile and popular"),
("JavaScript", "⚡ Web development"),
("TypeScript", "🔷 Typed JavaScript"),
("Go", "🏃 Fast and simple"),
("Java", "☕ Enterprise favorite"),
("C++", "⚡ High performance"),
("Swift", "🍎 iOS development"),
];
self.filter_suggestions(languages, query, "language")
}
/// Get country suggestions (field 3)
fn get_country_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
let countries = vec![
("United States", "🇺🇸 North America"),
("Canada", "🇨🇦 Great neighbors"),
("United Kingdom", "🇬🇧 Tea and crumpets"),
("Germany", "🇩🇪 Engineering excellence"),
("France", "🇫🇷 Art and cuisine"),
("Japan", "🇯🇵 Technology hub"),
("Australia", "🇦🇺 Down under"),
("Brazil", "🇧🇷 Carnival country"),
];
self.filter_suggestions(countries, query, "country")
}
/// Get color suggestions (field 4)
fn get_color_suggestions(&self, query: &str) -> Vec<SuggestionItem> {
let colors = vec![
("Red", "🔴 Bold and energetic"),
("Blue", "🔵 Calm and trustworthy"),
("Green", "🟢 Natural and fresh"),
("Yellow", "🟡 Bright and cheerful"),
("Purple", "🟣 Royal and mysterious"),
("Orange", "🟠 Warm and vibrant"),
("Pink", "🩷 Soft and gentle"),
("Black", "⚫ Classic and elegant"),
];
self.filter_suggestions(colors, query, "color")
}
/// Generic filtering helper
fn filter_suggestions(&self, items: Vec<(&str, &str)>, query: &str, _category: &str) -> Vec<SuggestionItem> {
let query_lower = query.to_lowercase();
items.iter()
.filter(|(item, _)| {
query.is_empty() || item.to_lowercase().starts_with(&query_lower)
})
.map(|(item, description)| SuggestionItem {
display_text: format!("{} - {}", item, description),
value_to_store: item.to_string(),
})
.collect()
}
}
#[async_trait]
impl SuggestionsProvider for ComprehensiveSuggestionsProvider {
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)
}
}
/// Multi-field suggestions demonstration + automatic cursor management
async fn handle_key_press(
key: KeyCode,
modifiers: KeyModifiers,
editor: &mut AutoCursorFormEditor<MultiFieldDemoData>,
suggestions_provider: &mut ComprehensiveSuggestionsProvider,
) -> anyhow::Result<bool> {
let mode = editor.mode();
// Quit handling
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL))
|| (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL))
|| key == KeyCode::F(10)
{
return Ok(false);
}
match (mode, key, modifiers) {
// === SUGGESTIONS HANDLING ===
(_, 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));
}
}
} else {
editor.next_field();
editor.set_debug_message("Tab: next field".to_string());
}
}
// Enter: Apply suggestion or move to next field
(_, KeyCode::Enter, _) => {
if editor.is_suggestions_active() {
if let Some(applied) = editor.apply_suggestion() {
editor.set_debug_message(format!("✅ Selected: {}", applied));
} else {
editor.set_debug_message("❌ No suggestion selected".to_string());
}
} else {
editor.next_field();
let field_names = ["Fruit", "Job", "Language", "Country", "Color"];
let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field");
editor.set_debug_message(format!("Enter: moved to {} field", field_name));
}
}
// Escape: Close suggestions or exit mode
(_, KeyCode::Esc, _) => {
if editor.is_suggestions_active() {
editor.close_suggestions();
editor.set_debug_message("❌ Suggestions closed".to_string());
} else {
match mode {
AppMode::Edit => {
editor.exit_edit_mode();
}
AppMode::Highlight => {
editor.exit_visual_mode();
}
_ => {
editor.clear_command_buffer();
}
}
}
}
// === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT ===
(AppMode::ReadOnly, KeyCode::Char('i'), _) => {
editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar |
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();
}
}
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
editor.enter_append_mode();
editor.set_debug_message("✏️ INSERT (append) - Cursor: Steady Bar |".to_string());
editor.clear_command_buffer();
}
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
editor.move_line_end();
editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar |
editor.set_debug_message("✏️ INSERT (end of line) - Cursor: Steady Bar |".to_string());
editor.clear_command_buffer();
}
// From Normal Mode: Enter visual modes
(AppMode::ReadOnly, KeyCode::Char('v'), _) => {
editor.enter_visual_mode();
editor.clear_command_buffer();
}
(AppMode::ReadOnly, KeyCode::Char('V'), _) => {
editor.enter_visual_line_mode();
editor.clear_command_buffer();
}
// === CURSOR MANAGEMENT DEMONSTRATION ===
(AppMode::ReadOnly, KeyCode::F(1), _) => {
editor.demo_manual_cursor_control()?;
}
(AppMode::ReadOnly, KeyCode::F(2), _) => {
editor.restore_automatic_cursor()?;
}
// === MOVEMENT: VIM-STYLE NAVIGATION ===
// Basic movement (hjkl and arrows)
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('h'), _)
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => {
editor.move_left();
editor.set_debug_message("← left".to_string());
editor.clear_command_buffer();
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _)
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => {
editor.move_right();
editor.set_debug_message("→ right".to_string());
editor.clear_command_buffer();
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _)
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => {
editor.move_down();
let field_names = ["Fruit", "Job", "Language", "Country", "Color"];
let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field");
editor.set_debug_message(format!("↓ moved to {} field", field_name));
editor.clear_command_buffer();
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _)
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => {
editor.move_up();
let field_names = ["Fruit", "Job", "Language", "Country", "Color"];
let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field");
editor.set_debug_message(format!("↑ moved to {} field", field_name));
editor.clear_command_buffer();
}
// Word movement
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => {
editor.move_word_next();
editor.set_debug_message("w: next word start".to_string());
editor.clear_command_buffer();
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => {
editor.move_word_prev();
editor.set_debug_message("b: previous word start".to_string());
editor.clear_command_buffer();
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => {
editor.move_word_end();
editor.set_debug_message("e: word end".to_string());
editor.clear_command_buffer();
}
// Line movement
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _)
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => {
editor.move_line_start();
editor.set_debug_message("0: line start".to_string());
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('$'), _)
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::End, _) => {
editor.move_line_end();
editor.set_debug_message("$: line end".to_string());
}
// Document movement
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => {
if editor.get_command_buffer() == "g" {
editor.move_first_line();
editor.set_debug_message("gg: first field (Fruit)".to_string());
editor.clear_command_buffer();
} else {
editor.clear_command_buffer();
editor.add_to_command_buffer('g');
editor.set_debug_message("g".to_string());
}
}
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => {
editor.move_last_line();
editor.set_debug_message("G: last field (Color)".to_string());
editor.clear_command_buffer();
}
// === EDIT MODE MOVEMENT ===
(AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => {
editor.move_word_prev();
editor.set_debug_message("Ctrl+← word back".to_string());
}
(AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => {
editor.move_word_next();
editor.set_debug_message("Ctrl+→ word forward".to_string());
}
(AppMode::Edit, KeyCode::Left, _) => {
editor.move_left();
}
(AppMode::Edit, KeyCode::Right, _) => {
editor.move_right();
}
(AppMode::Edit, KeyCode::Up, _) => {
editor.move_up();
}
(AppMode::Edit, KeyCode::Down, _) => {
editor.move_down();
}
(AppMode::Edit, KeyCode::Home, _) => {
editor.move_line_start();
}
(AppMode::Edit, KeyCode::End, _) => {
editor.move_line_end();
}
// === DELETE OPERATIONS ===
(AppMode::Edit, KeyCode::Backspace, _) => {
editor.delete_backward()?;
// 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();
}
}
}
(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();
}
}
}
// Delete operations in normal mode (vim x)
(AppMode::ReadOnly, KeyCode::Char('x'), _) => {
editor.delete_forward()?;
editor.set_debug_message("x: deleted character".to_string());
}
(AppMode::ReadOnly, KeyCode::Char('X'), _) => {
editor.delete_backward()?;
editor.set_debug_message("X: deleted character backward".to_string());
}
// === CHARACTER INPUT 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));
}
}
}
}
// === DEBUG/INFO COMMANDS ===
(AppMode::ReadOnly, KeyCode::Char('?'), _) => {
let field_names = ["Fruit🍎", "Job💼", "Language💻", "Country🌍", "Color🎨"];
let current_field_name = field_names.get(editor.current_field()).unwrap_or(&"Unknown");
editor.set_debug_message(format!(
"Field: {} ({}/{}), Pos: {}, Mode: {:?}",
current_field_name,
editor.current_field() + 1,
editor.data_provider().field_count(),
editor.cursor_position(),
editor.mode()
));
}
_ => {
if editor.has_pending_command() {
editor.clear_command_buffer();
editor.set_debug_message("Invalid command sequence".to_string());
} else {
let field_names = ["Fruit", "Job", "Language", "Country", "Color"];
let current_field = field_names.get(editor.current_field()).unwrap_or(&"Field");
editor.set_debug_message(format!(
"{} field - Try: i=insert, Tab=suggestions, j/k=move. Key: {:?}",
current_field, key
));
}
}
}
Ok(true)
}
async fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut editor: AutoCursorFormEditor<MultiFieldDemoData>,
) -> io::Result<()> {
let mut suggestions_provider = ComprehensiveSuggestionsProvider::new();
loop {
terminal.draw(|f| ui(f, &editor))?;
if let Event::Key(key) = event::read()? {
match handle_key_press(key.code, key.modifiers, &mut editor, &mut suggestions_provider).await {
Ok(should_continue) => {
if !should_continue {
break;
}
}
Err(e) => {
editor.set_debug_message(format!("Error: {}", e));
}
}
}
}
Ok(())
}
fn ui(f: &mut Frame, editor: &AutoCursorFormEditor<MultiFieldDemoData>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(12)])
.split(f.area());
let active_field_rect = render_enhanced_canvas(f, chunks[0], editor);
// Render suggestions dropdown if active
if let Some(input_rect) = active_field_rect {
render_suggestions_dropdown(
f,
chunks[0],
input_rect,
&canvas::canvas::theme::DefaultCanvasTheme::default(),
&editor.editor,
);
}
render_status_and_help(f, chunks[1], editor);
}
fn render_enhanced_canvas(
f: &mut Frame,
area: ratatui::layout::Rect,
editor: &AutoCursorFormEditor<MultiFieldDemoData>,
) -> Option<ratatui::layout::Rect> {
render_canvas_default(f, area, &editor.editor)
}
fn render_status_and_help(
f: &mut Frame,
area: ratatui::layout::Rect,
editor: &AutoCursorFormEditor<MultiFieldDemoData>,
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(9)])
.split(area);
// Status bar with current field and cursor information
let field_names = ["Fruit🍎", "Job💼", "Language💻", "Country🌍", "Color🎨"];
let current_field_name = field_names.get(editor.current_field()).unwrap_or(&"Unknown");
let mode_text = match editor.mode() {
AppMode::Edit => "INSERT | (bar cursor)",
AppMode::ReadOnly => "NORMAL █ (block cursor)",
AppMode::Highlight => "VISUAL █ (blinking block)",
_ => "NORMAL █ (block cursor)",
};
let suggestions_info = if editor.is_suggestions_active() {
if editor.editor.ui_state().is_suggestions_loading() {
" | ⏳ Loading suggestions...".to_string()
} else if !editor.suggestions().is_empty() {
format!(" | ✨ {} suggestions", editor.suggestions().len())
} else {
" | 🔍 No matches".to_string()
}
} else {
"".to_string()
};
let status_text = format!(
"-- {} -- {} | Field: {}{}",
mode_text,
editor.debug_message(),
current_field_name,
suggestions_info
);
let status = Paragraph::new(Line::from(Span::raw(status_text)))
.block(Block::default().borders(Borders::ALL).title("🎯 Multi-Field Suggestions Demo"));
f.render_widget(status, chunks[0]);
// Comprehensive help text
let help_text = match editor.mode() {
AppMode::ReadOnly => {
"🎯 MULTI-FIELD SUGGESTIONS DEMO: Normal █ | Insert | | Visual █\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!"
}
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\
Navigation: arrows=move, Ctrl+arrows=words, Home/End=line edges\n\
Try different fields for different suggestion behaviors and timing!"
}
AppMode::Highlight => {
"🎯 VISUAL MODE - Cursor: █ (blinking block)\n\
Selection: hjkl/arrows=extend, w/b/e=word selection, Esc=normal\n\
Test multi-character selections across different suggestion field types!"
}
_ => "🎯 Multi-field suggestions! 5 fields × 8 suggestions each = lots of testing!"
};
let help = Paragraph::new(help_text)
.block(Block::default().borders(Borders::ALL).title("🚀 Comprehensive Testing Guide"))
.style(Style::default().fg(Color::Gray));
f.render_widget(help, chunks[1]);
}
#[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");
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!();
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!();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let data = MultiFieldDemoData::new();
let mut editor = AutoCursorFormEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor
editor.set_mode(AppMode::ReadOnly);
// Demonstrate that CursorManager is available and working
CursorManager::update_for_mode(AppMode::ReadOnly)?;
let res = run_app(&mut terminal, editor).await;
// Library automatically resets cursor on FormEditor::drop()
// But we can also manually reset if needed
CursorManager::reset()?;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err);
}
println!("🎯 Multi-field testing complete! Great for finding edge cases!");
Ok(())
}