use std::path::PathBuf;
use tokio::sync::mpsc;
use crate::app::AppEvent;
use crate::domain::input::InputState;
use crate::infra::file_index::{self, FileEntry};
use crate::ui::state::prompt::PromptAtMentionState;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum AtMentionSyncAction {
Activate,
Dismiss,
KeepOpen,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AtMentionSelection {
pub cursor: usize,
pub text: String,
pub at_start: usize,
}
pub(crate) fn sync_action(
input: &InputState,
at_mention_state: Option<&PromptAtMentionState>,
) -> AtMentionSyncAction {
match (
input.at_mention_query().is_some(),
at_mention_state.is_some(),
) {
(true, true) => AtMentionSyncAction::KeepOpen,
(true, false) => AtMentionSyncAction::Activate,
(false, _) => AtMentionSyncAction::Dismiss,
}
}
pub(crate) fn start_loading_entries(
event_tx: mpsc::UnboundedSender<AppEvent>,
session_folder: PathBuf,
session_id: String,
) {
tokio::spawn(async move {
let entries = tokio::task::spawn_blocking(move || file_index::list_files(&session_folder))
.await
.unwrap_or_default();
let _ = event_tx.send(AppEvent::AtMentionEntriesLoaded {
entries,
session_id,
});
});
}
pub(crate) fn dismiss(at_mention_state: &mut Option<PromptAtMentionState>) {
*at_mention_state = None;
}
pub(crate) fn reset_selection(at_mention_state: &mut PromptAtMentionState) {
at_mention_state.selected_index = 0;
}
pub(crate) fn move_selection_up(at_mention_state: &mut PromptAtMentionState) {
at_mention_state.selected_index = at_mention_state.selected_index.saturating_sub(1);
}
pub(crate) fn move_selection_down(input: &InputState, at_mention_state: &mut PromptAtMentionState) {
let filtered_count =
filtered_entries(input, at_mention_state).map_or(0_usize, |entries| entries.len());
let max_index = filtered_count.saturating_sub(1);
at_mention_state.selected_index = (at_mention_state.selected_index + 1).min(max_index);
}
pub(crate) fn selected_replacement(
input: &InputState,
at_mention_state: &PromptAtMentionState,
) -> Option<AtMentionSelection> {
let (at_start, query) = input.at_mention_query()?;
let filtered = file_index::filter_entries(&at_mention_state.all_entries, &query);
let clamped_index = at_mention_state
.selected_index
.min(filtered.len().saturating_sub(1));
filtered.get(clamped_index).map(|entry| AtMentionSelection {
at_start,
cursor: input.cursor,
text: format_mention_text(entry),
})
}
fn filtered_entries<'a>(
input: &InputState,
at_mention_state: &'a PromptAtMentionState,
) -> Option<Vec<&'a FileEntry>> {
let (_, query) = input.at_mention_query()?;
Some(file_index::filter_entries(
&at_mention_state.all_entries,
&query,
))
}
fn format_mention_text(entry: &FileEntry) -> String {
if entry.is_dir {
return format!("@{}/ ", entry.path);
}
format!("@{} ", entry.path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_action_requests_activation_for_new_query() {
let input = InputState::with_text("@src".to_string());
let action = sync_action(&input, None);
assert_eq!(action, AtMentionSyncAction::Activate);
}
#[test]
fn test_move_selection_down_clamps_to_last_filtered_entry() {
let input = InputState::with_text("@src".to_string());
let mut at_mention_state = PromptAtMentionState::new(vec![
FileEntry {
is_dir: true,
path: "src".to_string(),
},
FileEntry {
is_dir: false,
path: "src/lib.rs".to_string(),
},
]);
at_mention_state.selected_index = 99;
move_selection_down(&input, &mut at_mention_state);
assert_eq!(at_mention_state.selected_index, 1);
}
#[test]
fn test_selected_replacement_formats_directory_with_trailing_slash() {
let input = InputState::with_text("@src".to_string());
let at_mention_state = PromptAtMentionState::new(vec![FileEntry {
is_dir: true,
path: "src".to_string(),
}]);
let selection =
selected_replacement(&input, &at_mention_state).expect("expected directory selection");
assert_eq!(
selection,
AtMentionSelection {
at_start: 0,
cursor: 4,
text: "@src/ ".to_string(),
}
);
}
}