use escriba_core::Mode;
use escriba_search::MatchCount;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptKind {
None,
SearchForward,
SearchBackward,
Ex,
}
impl PromptKind {
#[must_use]
pub const fn sigil(self) -> Option<char> {
match self {
Self::None => None,
Self::SearchForward => Some('/'),
Self::SearchBackward => Some('?'),
Self::Ex => Some(':'),
}
}
#[must_use]
pub const fn is_search(self) -> bool {
matches!(self, Self::SearchForward | Self::SearchBackward)
}
}
#[derive(Debug, Clone, Copy)]
pub struct StatusModel<'a> {
pub mode: Mode,
pub line: usize,
pub column: usize,
pub prompt: PromptKind,
pub prompt_text: &'a str,
pub prompt_caret: usize,
pub count: MatchCount,
pub message: Option<&'a str>,
}
impl StatusModel<'_> {
pub fn render_prompt_into(self, out: &mut String) {
if let Some(sigil) = self.prompt.sigil() {
out.push(sigil);
out.push_str(self.prompt_text);
}
}
#[must_use]
pub fn render(self) -> String {
let mut out = String::with_capacity(64);
out.push_str(self.mode.as_str());
out.push_str(" ");
push_usize(&mut out, self.line);
out.push(':');
push_usize(&mut out, self.column);
if !self.count.is_idle() {
out.push_str(" ");
self.count.render_into(&mut out);
}
if self.prompt.sigil().is_some() {
out.push_str(" ");
self.render_prompt_into(&mut out);
}
if let Some(msg) = self.message {
out.push_str(" ");
out.push_str(msg);
}
out
}
}
fn push_usize(out: &mut String, mut n: usize) {
if n == 0 {
out.push('0');
return;
}
let mut buf = [0u8; 20];
let mut i = buf.len();
while n > 0 {
i -= 1;
buf[i] = b'0' + u8::try_from(n % 10).unwrap_or(0);
n /= 10;
}
out.push_str(core::str::from_utf8(&buf[i..]).unwrap_or("?"));
}
#[cfg(test)]
mod tests {
use super::*;
fn model<'a>(
prompt: PromptKind,
text: &'a str,
count: MatchCount,
msg: Option<&'a str>,
) -> StatusModel<'a> {
StatusModel {
mode: Mode::Normal,
line: 3,
column: 1,
prompt,
prompt_text: text,
prompt_caret: text.chars().count(),
count,
message: msg,
}
}
#[test]
fn every_prompt_kind_has_the_sigil_a_user_expects() {
assert_eq!(PromptKind::None.sigil(), None);
assert_eq!(PromptKind::SearchForward.sigil(), Some('/'));
assert_eq!(PromptKind::SearchBackward.sigil(), Some('?'));
assert_eq!(PromptKind::Ex.sigil(), Some(':'));
assert!(PromptKind::SearchForward.is_search());
assert!(PromptKind::SearchBackward.is_search());
assert!(!PromptKind::Ex.is_search());
}
#[test]
fn a_search_prompt_renders_its_pattern() {
let s = model(PromptKind::SearchForward, "foo", MatchCount::Idle, None).render();
assert!(s.contains("/foo"), "prompt missing from {s:?}");
let mut only = String::new();
model(PromptKind::SearchBackward, "bar", MatchCount::Idle, None)
.render_prompt_into(&mut only);
assert_eq!(only, "?bar");
}
#[test]
fn no_prompt_renders_no_sigil() {
let mut out = String::new();
model(PromptKind::None, "", MatchCount::Idle, None).render_prompt_into(&mut out);
assert!(out.is_empty(), "got {out:?}");
}
#[test]
fn the_count_is_shown_and_idle_is_silent() {
let s = model(
PromptKind::None,
"",
MatchCount::Exact {
current: 2,
total: 7,
},
None,
)
.render();
assert!(s.contains("[2/7]"), "{s}");
let idle = model(PromptKind::None, "", MatchCount::Idle, None).render();
assert!(!idle.contains('['), "idle must draw nothing: {idle}");
}
#[test]
fn zero_matches_says_so_rather_than_going_quiet() {
let s = model(PromptKind::SearchForward, "zzz", MatchCount::None, None).render();
assert!(s.contains("[0/0]"), "{s}");
}
#[test]
fn a_capped_count_is_marked_as_capped() {
let s = model(
PromptKind::None,
"",
MatchCount::Capped { current: 5 },
None,
)
.render();
assert!(s.contains("[5/>99]"), "{s}");
}
#[test]
fn the_newest_message_reaches_the_line() {
let s = model(
PromptKind::None,
"",
MatchCount::Idle,
Some("E486: Pattern not found: zzz"),
)
.render();
assert!(s.contains("E486"), "{s}");
}
#[test]
fn mode_and_position_are_one_based() {
let s = model(PromptKind::None, "", MatchCount::Idle, None).render();
assert!(s.starts_with("NORMAL"), "{s}");
assert!(s.contains("3:1"), "{s}");
}
#[test]
fn large_numbers_render_correctly_without_format() {
let m = StatusModel {
mode: Mode::Insert,
line: 12_345,
column: 678,
prompt: PromptKind::None,
prompt_text: "",
prompt_caret: 0,
count: MatchCount::Exact {
current: 10,
total: 99,
},
message: None,
};
let s = m.render();
assert!(s.contains("12345:678"), "{s}");
assert!(s.contains("[10/99]"), "{s}");
}
}