use super::*;
pub(super) fn refresh_autocomplete(state: &Arc<TuiState>, editor: &Arc<Editor>) {
let text = editor.get_text();
let cursor = editor_cursor_offset(editor, &text);
let suggestions = state.autocomplete.get_suggestions(&text, cursor);
render_autocomplete(state, suggestions);
}
pub(super) fn editor_cursor_offset(editor: &Editor, text: &str) -> usize {
let (row, col) = editor.cursor_position();
let mut offset = 0;
for (index, line) in text.split('\n').enumerate() {
if index == row {
return (offset + col.min(line.len())).min(text.len());
}
offset = offset.saturating_add(line.len() + 1);
}
text.len()
}
pub(super) fn set_editor_cursor_offset(editor: &Editor, text: &str, offset: usize) {
let offset = offset.min(text.len());
let mut consumed = 0;
for (row, line) in text.split('\n').enumerate() {
let end = consumed + line.len();
if offset <= end {
editor.set_cursor(row, offset - consumed);
return;
}
consumed = end + 1;
}
let last_row = text.bytes().filter(|byte| *byte == b'\n').count();
editor.set_cursor(
last_row,
text.rsplit('\n').next().map(str::len).unwrap_or(0),
);
}
pub(super) fn render_autocomplete(
state: &Arc<TuiState>,
suggestions: Option<AutocompleteSuggestions>,
) {
state.autocomplete_container.clear();
let Some(sugg) = suggestions else {
*state.autocomplete_selection.lock().unwrap() = 0;
return;
};
if sugg.items.is_empty() {
*state.autocomplete_selection.lock().unwrap() = 0;
return;
}
let max_visible = state.autocomplete_max_visible;
let total = sugg.items.len();
let selection = {
let mut sel = state.autocomplete_selection.lock().unwrap();
*sel = (*sel).min(total.saturating_sub(1));
*sel
};
let window_start = if total <= max_visible {
0
} else {
selection
.saturating_sub(max_visible / 2)
.min(total - max_visible)
};
let window_end = (window_start + max_visible).min(total);
let accent = state.theme_manager.get().colors.accent;
let muted = state.theme_manager.get().colors.muted;
for (i, item) in sugg
.items
.iter()
.enumerate()
.skip(window_start)
.take(window_end - window_start)
{
let prefix = if i == selection { "→ " } else { " " };
let label = item.display_text();
let line = if i == selection {
format!(
"{prefix}{} {}",
accent.fg(label),
muted.fg(item.description.as_deref().unwrap_or(""))
)
} else {
format!(
"{prefix}{} {}",
muted.fg(label),
muted.fg(item.description.as_deref().unwrap_or(""))
)
};
state
.autocomplete_container
.add_child(Arc::new(Text::new(line, 1, 0)));
}
}
pub(super) fn accept_top_suggestion(state: &Arc<TuiState>, editor: &Arc<Editor>) -> bool {
let text = editor.get_text();
let cursor = editor_cursor_offset(editor, &text);
let Some(sugg) = state.autocomplete.get_suggestions(&text, cursor) else {
return false;
};
let index = {
let sel = *state.autocomplete_selection.lock().unwrap();
sel.min(sugg.items.len().saturating_sub(1))
};
let Some(top) = sugg.items.get(index) else {
return false;
};
let start = sugg.start.min(text.len());
let end = sugg.end.min(text.len());
let mut replaced = String::with_capacity(text.len() + top.text.len());
replaced.push_str(&text[..start]);
replaced.push_str(&top.text);
replaced.push_str(&text[end..]);
if top.insert_space && !replaced.ends_with('/') {
replaced.push(' ');
}
let new_cursor = replaced.len().min(
start
+ top.text.len()
+ if top.insert_space && !top.text.ends_with('/') {
1
} else {
0
},
);
editor.set_text(&replaced);
set_editor_cursor_offset(editor, &replaced, new_cursor);
state.autocomplete_container.clear();
*state.autocomplete_selection.lock().unwrap() = 0;
true
}
pub(super) fn navigate_autocomplete(
state: &Arc<TuiState>,
editor: &Arc<Editor>,
direction: i32,
) -> bool {
let text = editor.get_text();
let cursor = editor_cursor_offset(editor, &text);
let Some(sugg) = state.autocomplete.get_suggestions(&text, cursor) else {
return false;
};
if sugg.items.is_empty() {
return false;
}
let start = sugg.start.min(text.len());
let explicit = text[start..].starts_with('/') || text[..start].ends_with('@');
if !explicit {
return false;
}
let total = sugg.items.len();
{
let mut sel = state.autocomplete_selection.lock().unwrap();
if direction < 0 {
*sel = sel.saturating_sub(1);
} else {
*sel = (*sel + 1).min(total.saturating_sub(1));
}
}
render_autocomplete(state, Some(sugg));
true
}