autocomplete now autocompleting data not just id
This commit is contained in:
@@ -140,12 +140,12 @@ pub fn render_rich_autocomplete_dropdown(
|
||||
|
||||
let display_part = values.first().cloned().unwrap_or_default(); // Get the first value
|
||||
if display_part.is_empty() {
|
||||
format!("ID: {}", hit.id)
|
||||
format!("{}", hit.id)
|
||||
} else {
|
||||
format!("{} | ID: {}", display_part, hit.id) // ID at the end
|
||||
format!("{} | {}", display_part, hit.id) // ID at the end
|
||||
}
|
||||
} else {
|
||||
format!("ID: {} (parse error)", hit.id)
|
||||
format!("{} (parse error)", hit.id)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// src/components/handlers/canvas.rs
|
||||
|
||||
use ratatui::{
|
||||
widgets::{Paragraph, Block, Borders},
|
||||
layout::{Layout, Constraint, Direction, Rect},
|
||||
style::{Style, Modifier},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
prelude::Alignment,
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::app::highlight::HighlightState; // Ensure correct import path
|
||||
use std::cmp::{min, max};
|
||||
use std::cmp::{max, min};
|
||||
|
||||
pub fn render_canvas(
|
||||
f: &mut Frame,
|
||||
@@ -21,9 +21,8 @@ pub fn render_canvas(
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState, // Using the enum state
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
// ... (setup code remains the same) ...
|
||||
let columns = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
@@ -58,46 +57,47 @@ pub fn render_canvas(
|
||||
|
||||
let mut active_field_input_rect = None;
|
||||
|
||||
// Render labels
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let label = Paragraph::new(Line::from(Span::styled(
|
||||
format!("{}:", field),
|
||||
Style::default().fg(theme.fg)),
|
||||
));
|
||||
f.render_widget(label, Rect {
|
||||
x: columns[0].x,
|
||||
y: input_block.y + 1 + i as u16,
|
||||
width: columns[0].width,
|
||||
height: 1,
|
||||
});
|
||||
Style::default().fg(theme.fg),
|
||||
)));
|
||||
f.render_widget(
|
||||
label,
|
||||
Rect {
|
||||
x: columns[0].x,
|
||||
y: input_block.y + 1 + i as u16,
|
||||
width: columns[0].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Render inputs and cursor
|
||||
for (i, input) in inputs.iter().enumerate() {
|
||||
for (i, _input) in inputs.iter().enumerate() {
|
||||
let is_active = i == *current_field_idx;
|
||||
let current_cursor_pos = form_state.current_cursor_pos();
|
||||
let text = input.as_str();
|
||||
let text_len = text.chars().count();
|
||||
|
||||
// Use the trait method to get display value
|
||||
let text = form_state.get_display_value_for_field(i);
|
||||
let text_len = text.chars().count();
|
||||
let line: Line;
|
||||
|
||||
// --- Use match on the highlight_state enum ---
|
||||
match highlight_state {
|
||||
HighlightState::Off => {
|
||||
// Not in highlight mode, render normally
|
||||
line = Line::from(Span::styled(
|
||||
text,
|
||||
if is_active { Style::default().fg(theme.highlight) } else { Style::default().fg(theme.fg) }
|
||||
if is_active {
|
||||
Style::default().fg(theme.highlight)
|
||||
} else {
|
||||
Style::default().fg(theme.fg)
|
||||
},
|
||||
));
|
||||
}
|
||||
HighlightState::Characterwise { anchor } => {
|
||||
// --- Character-wise Highlight Logic ---
|
||||
let (anchor_field, anchor_char) = *anchor;
|
||||
let start_field = min(anchor_field, *current_field_idx);
|
||||
let end_field = max(anchor_field, *current_field_idx);
|
||||
|
||||
// Use start_char and end_char consistently
|
||||
let (start_char, end_char) = if anchor_field == *current_field_idx {
|
||||
(min(anchor_char, current_cursor_pos), max(anchor_char, current_cursor_pos))
|
||||
} else if anchor_field < *current_field_idx {
|
||||
@@ -111,24 +111,20 @@ pub fn render_canvas(
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
// This line is within the character-wise highlight range
|
||||
if start_field == end_field { // Case 1: Single Line Highlight
|
||||
// Use start_char and end_char here
|
||||
if start_field == end_field {
|
||||
let clamped_start = start_char.min(text_len);
|
||||
let clamped_end = end_char.min(text_len); // Use text_len for slicing logic
|
||||
let clamped_end = end_char.min(text_len);
|
||||
|
||||
let before: String = text.chars().take(clamped_start).collect();
|
||||
let highlighted: String = text.chars().skip(clamped_start).take(clamped_end.saturating_sub(clamped_start) + 1).collect();
|
||||
// Define 'after' here
|
||||
let after: String = text.chars().skip(clamped_end + 1).collect();
|
||||
|
||||
line = Line::from(vec![
|
||||
Span::styled(before, normal_style_in_highlight),
|
||||
Span::styled(highlighted, highlight_style),
|
||||
Span::styled(after, normal_style_in_highlight), // Use defined 'after'
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else if i == start_field { // Case 2: Multi-Line Highlight - Start Line
|
||||
// Use start_char here
|
||||
} else if i == start_field {
|
||||
let safe_start = start_char.min(text_len);
|
||||
let before: String = text.chars().take(safe_start).collect();
|
||||
let highlighted: String = text.chars().skip(safe_start).collect();
|
||||
@@ -136,8 +132,7 @@ pub fn render_canvas(
|
||||
Span::styled(before, normal_style_in_highlight),
|
||||
Span::styled(highlighted, highlight_style),
|
||||
]);
|
||||
} else if i == end_field { // Case 3: Multi-Line Highlight - End Line (Corrected index)
|
||||
// Use end_char here
|
||||
} else if i == end_field {
|
||||
let safe_end_inclusive = if text_len > 0 { end_char.min(text_len - 1) } else { 0 };
|
||||
let highlighted: String = text.chars().take(safe_end_inclusive + 1).collect();
|
||||
let after: String = text.chars().skip(safe_end_inclusive + 1).collect();
|
||||
@@ -145,19 +140,17 @@ pub fn render_canvas(
|
||||
Span::styled(highlighted, highlight_style),
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else { // Case 4: Multi-Line Highlight - Middle Line (Corrected index)
|
||||
line = Line::from(Span::styled(text, highlight_style)); // Highlight whole line
|
||||
} else {
|
||||
line = Line::from(Span::styled(text, highlight_style));
|
||||
}
|
||||
} else { // Case 5: Line Outside Character-wise Highlight Range
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
text,
|
||||
// Use normal styling (active or inactive)
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
}
|
||||
HighlightState::Linewise { anchor_line } => {
|
||||
// --- Linewise Highlight Logic ---
|
||||
let start_field = min(*anchor_line, *current_field_idx);
|
||||
let end_field = max(*anchor_line, *current_field_idx);
|
||||
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
||||
@@ -165,25 +158,31 @@ pub fn render_canvas(
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
// Highlight the entire line
|
||||
line = Line::from(Span::styled(text, highlight_style));
|
||||
} else {
|
||||
// Line outside linewise highlight range
|
||||
line = Line::from(Span::styled(
|
||||
text,
|
||||
// Use normal styling (active or inactive)
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
}
|
||||
} // End match highlight_state
|
||||
}
|
||||
|
||||
let input_display = Paragraph::new(line).alignment(Alignment::Left);
|
||||
f.render_widget(input_display, input_rows[i]);
|
||||
|
||||
if is_active {
|
||||
active_field_input_rect = Some(input_rows[i]);
|
||||
let cursor_x = input_rows[i].x + form_state.current_cursor_pos() as u16;
|
||||
|
||||
// --- CORRECTED CURSOR POSITIONING LOGIC ---
|
||||
// Use the new generic trait method to check for an override.
|
||||
let cursor_x = if form_state.has_display_override(i) {
|
||||
// If an override exists, place the cursor at the end.
|
||||
input_rows[i].x + text.chars().count() as u16
|
||||
} else {
|
||||
// Otherwise, use the real cursor position.
|
||||
input_rows[i].x + form_state.current_cursor_pos() as u16
|
||||
};
|
||||
let cursor_y = input_rows[i].y;
|
||||
f.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
@@ -191,4 +190,3 @@ pub fn render_canvas(
|
||||
|
||||
active_field_input_rect
|
||||
}
|
||||
|
||||
|
||||
@@ -132,13 +132,24 @@ pub async fn handle_edit_event(
|
||||
.get(selected_idx)
|
||||
.cloned()
|
||||
{
|
||||
// --- THIS IS THE CORE LOGIC CHANGE ---
|
||||
|
||||
// 1. Get the friendly display name for the UI
|
||||
let display_name =
|
||||
form_state.get_display_name_for_hit(&selection);
|
||||
|
||||
// 2. Store the REAL ID in the form's values
|
||||
let current_input =
|
||||
form_state.get_current_input_mut();
|
||||
*current_input = selection.id.to_string();
|
||||
let new_cursor_pos = current_input.len();
|
||||
form_state.set_current_cursor_pos(new_cursor_pos);
|
||||
// FIX: Access ideal_cursor_column through event_handler
|
||||
event_handler.ideal_cursor_column = new_cursor_pos;
|
||||
|
||||
// 3. Set the persistent display override in the map
|
||||
form_state.link_display_map.insert(
|
||||
form_state.current_field,
|
||||
display_name,
|
||||
);
|
||||
|
||||
// 4. Finalize state
|
||||
form_state.deactivate_autocomplete();
|
||||
form_state.set_has_unsaved_changes(true);
|
||||
return Ok(EditEventOutcome::Message(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
|
||||
pub trait CanvasState {
|
||||
// --- Existing methods (unchanged) ---
|
||||
fn current_field(&self) -> usize;
|
||||
fn current_cursor_pos(&self) -> usize;
|
||||
fn has_unsaved_changes(&self) -> bool;
|
||||
@@ -10,15 +11,22 @@ pub trait CanvasState {
|
||||
fn get_current_input(&self) -> &str;
|
||||
fn get_current_input_mut(&mut self) -> &mut String;
|
||||
fn fields(&self) -> Vec<&str>;
|
||||
|
||||
fn set_current_field(&mut self, index: usize);
|
||||
fn set_current_cursor_pos(&mut self, pos: usize);
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool);
|
||||
|
||||
// --- Autocomplete Support ---
|
||||
fn get_suggestions(&self) -> Option<&[String]>;
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize>;
|
||||
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
self.inputs()
|
||||
.get(index)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
fn has_display_override(&self, _index: usize) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,20 @@
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use common::proto::multieko2::search::search_response::Hit; // Import Hit
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::Frame;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// A struct to bridge the display name (label) to the data key from the server.
|
||||
fn json_value_to_string(value: &serde_json::Value) -> String {
|
||||
match value {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Number(n) => n.to_string(),
|
||||
serde_json::Value::Bool(b) => b.to_string(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FieldDefinition {
|
||||
pub display_name: String,
|
||||
@@ -29,12 +37,11 @@ pub struct FormState {
|
||||
pub current_field: usize,
|
||||
pub has_unsaved_changes: bool,
|
||||
pub current_cursor_pos: usize,
|
||||
|
||||
// --- MODIFIED AUTOCOMPLETE STATE ---
|
||||
pub autocomplete_active: bool,
|
||||
pub autocomplete_suggestions: Vec<Hit>, // Changed to use the Hit struct
|
||||
pub autocomplete_suggestions: Vec<Hit>,
|
||||
pub selected_suggestion_index: Option<usize>,
|
||||
pub autocomplete_loading: bool, // To show a loading indicator
|
||||
pub autocomplete_loading: bool,
|
||||
pub link_display_map: HashMap<usize, String>,
|
||||
}
|
||||
|
||||
impl FormState {
|
||||
@@ -55,11 +62,48 @@ impl FormState {
|
||||
current_field: 0,
|
||||
has_unsaved_changes: false,
|
||||
current_cursor_pos: 0,
|
||||
// --- INITIALIZE NEW STATE ---
|
||||
autocomplete_active: false,
|
||||
autocomplete_suggestions: Vec::new(),
|
||||
selected_suggestion_index: None,
|
||||
autocomplete_loading: false, // Initialize loading state
|
||||
autocomplete_loading: false,
|
||||
link_display_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_display_name_for_hit(&self, hit: &Hit) -> String {
|
||||
if let Ok(content_map) =
|
||||
serde_json::from_str::<HashMap<String, serde_json::Value>>(
|
||||
&hit.content_json,
|
||||
)
|
||||
{
|
||||
const IGNORED_KEYS: &[&str] = &["id", "deleted", "created_at"];
|
||||
let mut keys: Vec<_> = content_map
|
||||
.keys()
|
||||
.filter(|k| !IGNORED_KEYS.contains(&k.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
keys.sort();
|
||||
|
||||
let values: Vec<_> = keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
content_map
|
||||
.get(key)
|
||||
.map(json_value_to_string)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.take(1)
|
||||
.collect();
|
||||
|
||||
let display_part = values.first().cloned().unwrap_or_default();
|
||||
if display_part.is_empty() {
|
||||
format!("ID: {}", hit.id)
|
||||
} else {
|
||||
format!("{} | ID: {}", display_part, hit.id)
|
||||
}
|
||||
} else {
|
||||
format!("ID: {} (parse error)", hit.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +122,7 @@ impl FormState {
|
||||
crate::components::form::form::render_form(
|
||||
f,
|
||||
area,
|
||||
self, // <--- This now correctly passes the concrete &FormState
|
||||
self,
|
||||
&fields_str_slice,
|
||||
&self.current_field,
|
||||
&values_str_slice,
|
||||
@@ -102,7 +146,8 @@ impl FormState {
|
||||
} else {
|
||||
self.current_position = 1;
|
||||
}
|
||||
self.deactivate_autocomplete(); // Deactivate on reset
|
||||
self.deactivate_autocomplete();
|
||||
self.link_display_map.clear();
|
||||
}
|
||||
|
||||
pub fn get_current_input(&self) -> &str {
|
||||
@@ -113,6 +158,7 @@ impl FormState {
|
||||
}
|
||||
|
||||
pub fn get_current_input_mut(&mut self) -> &mut String {
|
||||
self.link_display_map.remove(&self.current_field);
|
||||
self.values
|
||||
.get_mut(self.current_field)
|
||||
.expect("Invalid current_field index")
|
||||
@@ -159,11 +205,10 @@ impl FormState {
|
||||
self.has_unsaved_changes = false;
|
||||
self.current_field = 0;
|
||||
self.current_cursor_pos = 0;
|
||||
self.deactivate_autocomplete(); // Deactivate on update
|
||||
self.deactivate_autocomplete();
|
||||
self.link_display_map.clear();
|
||||
}
|
||||
|
||||
// --- NEW HELPER METHOD ---
|
||||
/// Deactivates autocomplete and clears its state.
|
||||
pub fn deactivate_autocomplete(&mut self) {
|
||||
self.autocomplete_active = false;
|
||||
self.autocomplete_suggestions.clear();
|
||||
@@ -176,58 +221,42 @@ impl CanvasState for FormState {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.current_cursor_pos
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_unsaved_changes
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
self.values.iter().collect()
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
FormState::get_current_input(self)
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
FormState::get_current_input_mut(self)
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
self.fields
|
||||
.iter()
|
||||
.map(|f| f.display_name.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
if index < self.fields.len() {
|
||||
self.current_field = index;
|
||||
}
|
||||
// Deactivate autocomplete when changing fields
|
||||
self.deactivate_autocomplete();
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.current_cursor_pos = pos;
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_unsaved_changes = changed;
|
||||
}
|
||||
|
||||
// --- MODIFIED: Implement autocomplete trait methods ---
|
||||
|
||||
/// Returns None because this state uses rich suggestions.
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns rich suggestions.
|
||||
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||
if self.autocomplete_active {
|
||||
Some(&self.autocomplete_suggestions)
|
||||
@@ -235,7 +264,6 @@ impl CanvasState for FormState {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
if self.autocomplete_active {
|
||||
self.selected_suggestion_index
|
||||
@@ -243,4 +271,19 @@ impl CanvasState for FormState {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
if let Some(display_text) = self.link_display_map.get(&index) {
|
||||
return display_text.as_str();
|
||||
}
|
||||
self.inputs()
|
||||
.get(index)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
// --- IMPLEMENT THE NEW TRAIT METHOD ---
|
||||
fn has_display_override(&self, index: usize) -> bool {
|
||||
self.link_display_map.contains_key(&index)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user