use std::{collections::BTreeSet, sync::Arc};
use promkit_widgets::{
core::{crossterm::event::Event, grapheme::StyledGraphemes, Widget},
listbox::{self, Listbox},
};
use tokio::{
sync::{mpsc, Mutex, RwLock},
task::{self, JoinHandle},
};
use crate::{
config::CompletionKeybinds,
context::{Index, SharedContext},
guide::{GuideAction, GuideMessage},
json,
query_editor::QueryEditorAction,
};
#[derive(Clone, Default)]
pub struct SuggestionLoadProgress {
pub is_complete: bool,
pub loaded_path_count: usize,
}
struct SuggestionStore {
paths: BTreeSet<String>,
progress: SuggestionLoadProgress,
}
#[derive(Clone)]
pub struct SharedSuggestionStore(Arc<Mutex<SuggestionStore>>);
impl SharedSuggestionStore {
pub async fn collect_matches(&self, prefix: &str) -> (Vec<String>, SuggestionLoadProgress) {
let store = self.0.lock().await;
let items = store
.paths
.iter()
.filter(|p| p.starts_with(prefix))
.cloned()
.collect::<Vec<_>>();
(items, store.progress.clone())
}
}
pub fn spawn_initialize(
input: &'static str,
max_streams: Option<usize>,
chunk_size: usize,
) -> (SharedSuggestionStore, JoinHandle<()>) {
let shared = SharedSuggestionStore(Arc::new(Mutex::new(SuggestionStore {
paths: BTreeSet::new(),
progress: SuggestionLoadProgress::default(),
})));
let shared_for_loading = shared.clone();
let loader_task = task::spawn(async move {
let iter = match json::get_all_paths(input, max_streams).await {
Ok(iter) => iter,
Err(_) => {
let mut store = shared_for_loading.0.lock().await;
store.progress.is_complete = true;
return;
}
};
let mut batch = Vec::with_capacity(chunk_size);
for path in iter {
batch.push(path);
if batch.len() >= chunk_size {
let loaded = batch.len();
let mut store = shared_for_loading.0.lock().await;
for item in batch.drain(..) {
store.paths.insert(item);
}
store.progress.loaded_path_count += loaded;
}
}
let remaining = batch.len();
let mut store = shared_for_loading.0.lock().await;
for item in batch {
store.paths.insert(item);
}
store.progress.loaded_path_count += remaining;
store.progress.is_complete = true;
});
(shared, loader_task)
}
pub struct CompletionNavigator {
shared_suggestions: SharedSuggestionStore,
state: listbox::State,
search_result_chunk_size: usize,
remaining_items: Vec<String>,
}
impl CompletionNavigator {
pub fn new(
shared_suggestions: SharedSuggestionStore,
state: listbox::State,
search_result_chunk_size: usize,
) -> Self {
Self {
shared_suggestions,
state,
search_result_chunk_size,
remaining_items: Default::default(),
}
}
fn get_current_item(&self) -> String {
self.state.listbox.get().to_string()
}
pub fn create_graphemes(&self, width: u16, height: u16) -> StyledGraphemes {
self.state.create_graphemes(width, height)
}
fn is_near_visible_tail(&self) -> bool {
self.state
.listbox
.len()
.saturating_sub(self.state.listbox.position())
< self.state.config.lines.unwrap_or(1)
}
fn move_down(&mut self) {
self.state.listbox.forward();
if self.is_near_visible_tail() {
self.append_next_chunk_if_needed();
}
}
fn append_next_chunk_if_needed(&mut self) {
if self.remaining_items.is_empty() {
return;
}
let items = self.remaining_items.drain(
..self
.search_result_chunk_size
.min(self.remaining_items.len()),
);
for item in items {
self.state.listbox.push_string(item);
}
}
fn handle_user_event(
&mut self,
event: &Event,
completion_keybinds: &CompletionKeybinds,
) -> Option<String> {
if self.state.listbox.is_empty() {
return None;
}
if completion_keybinds.up.contains(event) {
self.state.listbox.backward();
return Some(self.get_current_item());
}
if completion_keybinds.down.contains(event) {
self.move_down();
return Some(self.get_current_item());
}
None
}
async fn enter(&mut self, prefix: &str) -> (Option<String>, SuggestionLoadProgress) {
let (items, progress) = self.shared_suggestions.collect_matches(prefix).await;
let head_item = self.initialize_session_items(items);
(head_item, progress)
}
fn initialize_session_items(&mut self, mut items: Vec<String>) -> Option<String> {
self.clear_session_state();
if items.is_empty() {
return None;
}
let used = items
.drain(..self.search_result_chunk_size.min(items.len()))
.collect::<Vec<_>>();
self.remaining_items = items;
self.state.listbox = Listbox::from(used);
Some(self.state.listbox.get().to_string())
}
fn clear_session_state(&mut self) {
self.state.listbox = Listbox::from(Vec::<String>::new());
self.remaining_items.clear();
}
}
pub enum CompletionAction {
Enter { prefix: String },
Leave,
UserEvent(Event),
}
pub fn start_completion_task(
mut action_rx: mpsc::Receiver<CompletionAction>,
shared_ctx: SharedContext,
shared_completion: Arc<RwLock<CompletionNavigator>>,
shared_renderer: promkit_widgets::core::render::SharedRenderer<Index>,
query_editor_action_tx: mpsc::Sender<QueryEditorAction>,
guide_action_tx: mpsc::Sender<GuideAction>,
completion_keybinds: CompletionKeybinds,
) -> JoinHandle<anyhow::Result<()>> {
tokio::spawn(async move {
loop {
tokio::select! {
Some(action) = action_rx.recv() => {
let area = shared_ctx.area().await;
let completion_view = {
let mut completion = shared_completion.write().await;
match action {
CompletionAction::Enter { prefix } => {
let (head_item, load_progress) = completion.enter(&prefix).await;
match head_item {
Some(head) => {
let message = if load_progress.is_complete {
GuideMessage::LoadedAllSuggestions(load_progress.loaded_path_count)
} else {
GuideMessage::LoadedPartiallySuggestions(load_progress.loaded_path_count)
};
guide_action_tx.send(GuideAction::Show(message)).await?;
query_editor_action_tx
.send(QueryEditorAction::ReplaceText(head))
.await?;
}
None => {
guide_action_tx
.send(GuideAction::Show(GuideMessage::NoSuggestionFound(prefix)))
.await?;
shared_ctx.set_active_index(Index::QueryEditor).await;
completion.clear_session_state();
}
}
}
CompletionAction::UserEvent(event) => {
if let Some(text) = completion.handle_user_event(&event, &completion_keybinds) {
query_editor_action_tx
.send(QueryEditorAction::ReplaceText(text))
.await?;
} else {
shared_ctx.set_active_index(Index::QueryEditor).await;
completion.clear_session_state();
query_editor_action_tx
.send(QueryEditorAction::UserEvent(event))
.await?;
}
}
CompletionAction::Leave => {
completion.clear_session_state();
}
}
completion.create_graphemes(area.0, area.1)
};
shared_renderer
.update([(Index::Completion, completion_view)])
.render()
.await?;
}
else => break,
}
}
Ok(())
})
}