#![allow(clippy::doc_markdown)]
use reovim_driver_session::{SessionExtension, TextInputSink};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CmdlinePrompt {
#[default]
Command,
SearchForward,
SearchBackward,
}
impl CmdlinePrompt {
#[must_use]
pub const fn char(self) -> char {
match self {
Self::Command => ':',
Self::SearchForward => '/',
Self::SearchBackward => '?',
}
}
#[must_use]
pub const fn is_search(self) -> bool {
matches!(self, Self::SearchForward | Self::SearchBackward)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CmdlineMessage {
Error(String),
Info(String),
}
impl CmdlineMessage {
#[must_use]
pub fn text(&self) -> &str {
match self {
Self::Error(s) | Self::Info(s) => s,
}
}
#[must_use]
pub const fn kind(&self) -> &str {
match self {
Self::Error(_) => "error",
Self::Info(_) => "info",
}
}
}
const MAX_HISTORY: usize = 100;
#[derive(Debug, Default)]
pub struct CmdlineState {
active: bool,
prompt: CmdlinePrompt,
cancelled: bool,
input: String,
cursor: usize,
command_history: Vec<String>,
search_history: Vec<String>,
history_index: Option<usize>,
saved_input: String,
completions: Vec<String>,
completion_index: Option<usize>,
completion_prefix: String,
message: Option<CmdlineMessage>,
}
impl SessionExtension for CmdlineState {
fn create() -> Self {
Self::default()
}
fn as_text_input_sink(&mut self) -> Option<&mut dyn TextInputSink> {
Some(self)
}
}
impl TextInputSink for CmdlineState {
fn insert_char(&mut self, ch: char) {
Self::insert_char(self, ch);
}
}
impl CmdlineState {
pub fn enter(&mut self, prompt: CmdlinePrompt) {
self.active = true;
self.prompt = prompt;
self.cancelled = false;
self.input.clear();
self.cursor = 0;
self.history_index = None;
self.saved_input.clear();
self.message = None;
}
pub const fn exit(&mut self) {
self.active = false;
self.cancelled = false;
}
pub fn cancel(&mut self) {
self.active = false;
self.cancelled = true;
self.input.clear();
self.cursor = 0;
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active
}
#[must_use]
pub const fn was_cancelled(&self) -> bool {
self.cancelled
}
#[must_use]
pub const fn prompt(&self) -> CmdlinePrompt {
self.prompt
}
pub fn insert_char(&mut self, ch: char) {
if self.cursor >= self.input.len() {
self.input.push(ch);
} else {
self.input.insert(self.cursor, ch);
}
self.cursor += 1;
self.clear_completions();
}
pub fn backspace(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
self.input.remove(self.cursor);
self.clear_completions();
}
}
#[must_use]
pub fn input(&self) -> &str {
&self.input
}
pub fn take_cmdline_input(&mut self) -> String {
self.cursor = 0;
std::mem::take(&mut self.input)
}
#[must_use]
pub const fn cursor(&self) -> usize {
self.cursor
}
pub const fn move_cursor_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub const fn move_cursor_right(&mut self) {
if self.cursor < self.input.len() {
self.cursor += 1;
}
}
pub const fn move_to_start(&mut self) {
self.cursor = 0;
}
pub const fn move_to_end(&mut self) {
self.cursor = self.input.len();
}
pub fn delete_at_cursor(&mut self) {
if self.cursor < self.input.len() {
self.input.remove(self.cursor);
self.clear_completions();
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn delete_word_back(&mut self) {
if self.cursor == 0 {
return;
}
let mut pos = self.cursor;
while pos > 0 && self.input.as_bytes()[pos - 1] == b' ' {
pos -= 1;
}
while pos > 0 && self.input.as_bytes()[pos - 1] != b' ' {
pos -= 1;
}
self.input.drain(pos..self.cursor);
self.cursor = pos;
self.clear_completions();
}
pub fn delete_to_start(&mut self) {
if self.cursor > 0 {
self.input.drain(..self.cursor);
self.cursor = 0;
self.clear_completions();
}
}
fn current_history(&self) -> &[String] {
if self.prompt.is_search() {
&self.search_history
} else {
&self.command_history
}
}
const fn current_history_mut(&mut self) -> &mut Vec<String> {
if self.prompt.is_search() {
&mut self.search_history
} else {
&mut self.command_history
}
}
pub fn history_up(&mut self) {
let history_len = self.current_history().len();
if history_len == 0 {
return;
}
let idx = match self.history_index {
None => {
self.saved_input.clone_from(&self.input);
history_len - 1
}
Some(0) => return, Some(i) => i - 1,
};
self.history_index = Some(idx);
let entry = self.current_history()[idx].clone();
self.input = entry;
self.cursor = self.input.len();
}
pub fn history_down(&mut self) {
let Some(idx) = self.history_index else {
return; };
let history_len = self.current_history().len();
if idx + 1 >= history_len {
self.history_index = None;
self.input = std::mem::take(&mut self.saved_input);
} else {
let entry = self.current_history()[idx + 1].clone();
self.history_index = Some(idx + 1);
self.input = entry;
}
self.cursor = self.input.len();
}
pub fn push_to_history(&mut self) {
if self.input.is_empty() {
return;
}
let input_clone = self.input.clone();
let history = self.current_history_mut();
history.retain(|entry| *entry != input_clone);
history.push(input_clone);
if history.len() > MAX_HISTORY {
history.remove(0);
}
}
pub fn set_completions(&mut self, prefix: String, candidates: Vec<String>) {
self.completion_prefix = prefix;
self.completions = candidates;
self.completion_index = None;
}
pub fn clear_completions(&mut self) {
self.completions.clear();
self.completion_index = None;
self.completion_prefix.clear();
}
pub fn complete_next(&mut self) -> bool {
if self.completions.is_empty() {
return false;
}
let idx = match self.completion_index {
None => 0,
Some(i) => (i + 1) % self.completions.len(),
};
self.completion_index = Some(idx);
self.apply_completion(idx);
true
}
pub fn complete_prev(&mut self) -> bool {
if self.completions.is_empty() {
return false;
}
let idx = match self.completion_index {
None | Some(0) => self.completions.len() - 1,
Some(i) => i - 1,
};
self.completion_index = Some(idx);
self.apply_completion(idx);
true
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn apply_completion(&mut self, idx: usize) {
if let Some(completion) = self.completions.get(idx) {
self.input = completion.clone();
self.cursor = self.input.len();
}
}
#[must_use]
pub fn completions(&self) -> &[String] {
&self.completions
}
#[must_use]
pub const fn completion_index(&self) -> Option<usize> {
self.completion_index
}
pub fn set_message(&mut self, message: CmdlineMessage) {
self.message = Some(message);
}
pub fn clear_message(&mut self) {
self.message = None;
}
#[must_use]
pub const fn message(&self) -> Option<&CmdlineMessage> {
self.message.as_ref()
}
}
#[cfg(test)]
#[path = "state_tests.rs"]
mod tests;