use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::{
file_picker::{self, FilePaletteEntry, FilePaletteMatches},
palette::ActivePalette,
App,
};
impl App {
pub(super) fn handle_file_palette_key(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
let Some(ActivePalette::File(matches)) = self.active_palette() else {
return Ok(false);
};
match (key.modifiers, key.code) {
(KeyModifiers::NONE, KeyCode::Up) => {
if !matches.is_empty() {
self.input_ui
.set_file_selection(if self.input_ui.file_selection() == 0 {
matches.len() - 1
} else {
self.input_ui.file_selection() - 1
});
}
self.input_ui.clear_paste_burst();
self.ctrl_c_streak = 0;
Ok(true)
}
(KeyModifiers::NONE, KeyCode::Down) => {
if !matches.is_empty() {
self.input_ui
.set_file_selection((self.input_ui.file_selection() + 1) % matches.len());
}
self.input_ui.clear_paste_burst();
self.ctrl_c_streak = 0;
Ok(true)
}
(KeyModifiers::NONE, KeyCode::Tab) | (KeyModifiers::NONE, KeyCode::Enter) => {
if let Some(entry) =
selected_palette_entry(&matches, self.input_ui.file_selection())
{
self.apply_file_palette_selection(&entry)?;
}
self.input_ui.clear_paste_burst();
self.ctrl_c_streak = 0;
Ok(true)
}
(KeyModifiers::NONE, KeyCode::Esc) => {
self.input_ui.set_file_palette_dismissed(true);
self.input_ui.set_file_selection(0);
self.input_ui.clear_paste_burst();
self.ctrl_c_streak = 0;
Ok(true)
}
_ => Ok(false),
}
}
pub(super) fn apply_file_palette_selection(
&mut self,
entry: &FilePaletteEntry,
) -> anyhow::Result<()> {
match entry {
FilePaletteEntry::WorkspaceFile(path) => self.insert_selected_file_path(path),
FilePaletteEntry::McpResource(resource) if resource.templated => {
if self.insert_file_mention_text(&resource.uri) {
self.set_status("resource template inserted; fill in the placeholders");
}
}
FilePaletteEntry::McpResource(resource) => self.start_mcp_resource_attach(resource)?,
}
Ok(())
}
pub(super) fn insert_selected_file_path(&mut self, path: &str) {
if self.insert_file_mention_text(path) {
self.set_status("file path inserted");
}
}
fn insert_file_mention_text(&mut self, text: &str) -> bool {
let Some(mention) =
file_picker::active_file_mention(self.input_ui.text(), self.input_ui.cursor())
else {
return false;
};
let insertion = if self
.input_ui
.text()
.chars()
.nth(mention.end)
.is_some_and(char::is_whitespace)
{
format!("@{text}")
} else {
format!("@{text} ")
};
self.replace_input_range(mention.start, mention.end, &insertion);
self.input_ui.set_file_palette_dismissed(true);
self.input_ui.set_file_selection(0);
true
}
pub(super) fn clear_active_file_mention(&mut self) {
let Some(mention) =
file_picker::active_file_mention(self.input_ui.text(), self.input_ui.cursor())
else {
return;
};
self.replace_input_range(mention.start, mention.end, "");
self.input_ui.set_file_palette_dismissed(true);
self.input_ui.set_file_selection(0);
}
pub(super) fn file_match_list(&mut self) -> FilePaletteMatches {
let Some(mention) =
file_picker::active_file_mention(self.input_ui.text(), self.input_ui.cursor())
else {
self.palette_caches.clear_file();
return FilePaletteMatches::empty();
};
if let Some(matches) = self
.palette_caches
.fresh_file(&mention.query, super::palette::PALETTE_CACHE_TTL)
{
return matches;
}
let discovered = self.discover_file_palette_matches(&mention.query);
self.palette_caches
.store_file(mention.query, discovered.clone());
discovered
}
fn discover_file_palette_matches(&mut self, query: &str) -> FilePaletteMatches {
let resources = if self.mcp_catalog.is_empty() {
Vec::new()
} else {
self.mcp_catalog.resources()
};
let cwd = self.info.runtime.cwd.clone();
file_picker::file_palette_matches(
file_picker::matching_file_paths_cached(
&cwd,
query,
self.palette_caches.workspace_mut(),
),
&resources,
query,
)
}
pub(super) fn clamp_file_selection(&mut self) {
let query = file_picker::active_file_mention(self.input_ui.text(), self.input_ui.cursor())
.map(|mention| mention.query);
if self.input_ui.file_query() != query.as_deref() {
self.input_ui.set_file_query(query);
self.input_ui.set_file_selection(0);
}
let match_count = self.file_match_list().len();
if match_count == 0 {
self.input_ui.set_file_selection(0);
} else if self.input_ui.file_selection() >= match_count {
self.input_ui.set_file_selection(match_count - 1);
}
}
}
fn selected_palette_entry(
matches: &FilePaletteMatches,
selection: usize,
) -> Option<FilePaletteEntry> {
matches.get(selection.min(matches.len().saturating_sub(1)))
}
#[cfg(test)]
#[path = "file_palette_tests.rs"]
mod tests;