use crate::domain::model::Config;
use crate::domain::shell::Shell;
pub(crate) use crate::domain::hook::HookAction;
pub(crate) fn run<F>(
config: &Config,
shell: Shell,
line: &str,
cursor: usize,
command_exists: F,
) -> HookAction
where
F: Fn(&str) -> bool,
{
crate::domain::hook::hook(config, shell, line, cursor, command_exists)
}
pub(crate) fn render(shell: Shell, action: &HookAction) -> String {
let (line, byte_cursor) = match action {
HookAction::Replace { line, cursor } | HookAction::InsertSpace { line, cursor } => {
(line, *cursor)
}
};
let shell_cursor = match shell {
Shell::Zsh => byte_cursor,
Shell::Pwsh => byte_cursor_to_utf16(line, byte_cursor),
Shell::Bash | Shell::Clink | Shell::Nu => byte_cursor_to_char(line, byte_cursor),
};
let shell_action = match action {
HookAction::Replace { line, .. } => HookAction::Replace {
line: line.clone(),
cursor: shell_cursor,
},
HookAction::InsertSpace { line, .. } => HookAction::InsertSpace {
line: line.clone(),
cursor: shell_cursor,
},
};
crate::domain::hook::render_action(shell, &shell_action)
}
pub(crate) fn shell_cursor_to_byte(shell: Shell, line: &str, cursor: usize) -> usize {
match shell {
Shell::Pwsh => utf16_cursor_to_byte(line, cursor),
Shell::Bash | Shell::Zsh | Shell::Clink | Shell::Nu => {
char_cursor_to_byte(line, cursor)
}
}
}
pub(crate) fn insert_space_action(line: &str, byte_cursor: usize) -> HookAction {
let cursor = byte_cursor.min(line.len());
let mut s = String::with_capacity(line.len() + 1);
s.push_str(&line[..cursor]);
s.push(' ');
s.push_str(&line[cursor..]);
HookAction::InsertSpace {
line: s,
cursor: cursor + 1,
}
}
pub(crate) fn char_cursor_to_byte(line: &str, char_cursor: usize) -> usize {
line.char_indices()
.nth(char_cursor)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or(line.len())
}
pub(crate) fn byte_cursor_to_char(line: &str, byte_cursor: usize) -> usize {
if byte_cursor >= line.len() {
return line.chars().count();
}
line.char_indices()
.enumerate()
.find_map(|(char_idx, (byte_idx, ch))| {
let next_byte = byte_idx + ch.len_utf8();
if byte_cursor < next_byte {
Some(char_idx)
} else {
None
}
})
.unwrap_or_else(|| line.chars().count())
}
pub(crate) fn utf16_cursor_to_byte(line: &str, utf16_cursor: usize) -> usize {
let mut consumed_utf16 = 0usize;
for (byte_idx, ch) in line.char_indices() {
if consumed_utf16 == utf16_cursor {
return byte_idx;
}
let next_utf16 = consumed_utf16 + ch.len_utf16();
if next_utf16 > utf16_cursor {
return byte_idx;
}
consumed_utf16 = next_utf16;
}
line.len()
}
pub(crate) fn byte_cursor_to_utf16(line: &str, byte_cursor: usize) -> usize {
if byte_cursor >= line.len() {
return line.chars().map(|c| c.len_utf16()).sum();
}
let mut utf16 = 0usize;
for (byte_idx, ch) in line.char_indices() {
if byte_idx >= byte_cursor {
return utf16;
}
utf16 += ch.len_utf16();
}
utf16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_cursor_to_byte_ascii_is_identity() {
assert_eq!(char_cursor_to_byte("lsd ./test1", 0), 0);
assert_eq!(char_cursor_to_byte("lsd ./test1", 4), 4);
assert_eq!(char_cursor_to_byte("lsd ./test1", 11), 11);
}
#[test]
fn char_cursor_to_byte_japanese_three_byte_chars() {
let line = "lsd ./おはよう ./test1";
assert_eq!(char_cursor_to_byte(line, 6), 6, "before お");
assert_eq!(char_cursor_to_byte(line, 7), 9, "before は");
assert_eq!(char_cursor_to_byte(line, 8), 12, "before よ");
assert_eq!(char_cursor_to_byte(line, 9), 15, "before う");
assert_eq!(char_cursor_to_byte(line, 10), 18, "after う / before first space");
}
#[test]
fn char_cursor_to_byte_at_line_end_returns_line_len() {
let line = "lsd ./おはよう";
assert_eq!(char_cursor_to_byte(line, 10), line.len());
}
#[test]
fn char_cursor_to_byte_past_line_end_clamps_to_line_len() {
let line = "abc";
assert_eq!(char_cursor_to_byte(line, 10), 3);
assert_eq!(char_cursor_to_byte(line, usize::MAX), 3);
}
#[test]
fn char_cursor_to_byte_empty_line() {
assert_eq!(char_cursor_to_byte("", 0), 0);
assert_eq!(char_cursor_to_byte("", 5), 0);
}
#[test]
fn char_cursor_to_byte_emoji_four_byte_chars() {
let line = "gst🎯end";
assert_eq!(char_cursor_to_byte(line, 0), 0);
assert_eq!(char_cursor_to_byte(line, 3), 3, "before 🎯");
assert_eq!(char_cursor_to_byte(line, 4), 7, "after 🎯 (1 char, 4 byte)");
assert_eq!(char_cursor_to_byte(line, 5), 8, "before n in 'end'");
}
#[test]
fn char_cursor_to_byte_combining_marks_are_separate_chars() {
let line = "a\u{0301}b";
assert_eq!(char_cursor_to_byte(line, 0), 0);
assert_eq!(char_cursor_to_byte(line, 1), 1, "between a and combining");
assert_eq!(char_cursor_to_byte(line, 2), 3, "between combining and b");
assert_eq!(char_cursor_to_byte(line, 3), 4);
}
#[test]
fn char_cursor_to_byte_rtl_chars_are_normal_codepoints() {
let line = "a\u{202E}b";
assert_eq!(char_cursor_to_byte(line, 0), 0);
assert_eq!(char_cursor_to_byte(line, 1), 1, "before RLO");
assert_eq!(char_cursor_to_byte(line, 2), 4, "after RLO (3 byte)");
assert_eq!(char_cursor_to_byte(line, 3), 5);
}
#[test]
fn char_cursor_to_byte_handles_nul_byte_without_panic() {
let line = "a\0b";
assert_eq!(char_cursor_to_byte(line, 0), 0);
assert_eq!(char_cursor_to_byte(line, 1), 1);
assert_eq!(char_cursor_to_byte(line, 2), 2);
assert_eq!(char_cursor_to_byte(line, 3), 3);
}
#[test]
fn byte_cursor_to_char_ascii_is_identity() {
assert_eq!(byte_cursor_to_char("lsd ./test1", 0), 0);
assert_eq!(byte_cursor_to_char("lsd ./test1", 4), 4);
assert_eq!(byte_cursor_to_char("lsd ./test1", 11), 11);
}
#[test]
fn byte_cursor_to_char_japanese_three_byte_chars() {
let line = "lsd ./おはよう ./test1";
assert_eq!(byte_cursor_to_char(line, 6), 6, "before お");
assert_eq!(byte_cursor_to_char(line, 9), 7, "before は");
assert_eq!(byte_cursor_to_char(line, 12), 8, "before よ");
assert_eq!(byte_cursor_to_char(line, 15), 9, "before う");
assert_eq!(byte_cursor_to_char(line, 18), 10, "after う / before first space");
}
#[test]
fn byte_cursor_to_char_at_line_end_returns_char_count() {
let line = "lsd ./おはよう";
assert_eq!(byte_cursor_to_char(line, line.len()), 10);
}
#[test]
fn byte_cursor_to_char_past_line_end_clamps_to_char_count() {
let line = "abc";
assert_eq!(byte_cursor_to_char(line, 10), 3);
assert_eq!(byte_cursor_to_char(line, usize::MAX), 3);
}
#[test]
fn byte_cursor_to_char_empty_line() {
assert_eq!(byte_cursor_to_char("", 0), 0);
assert_eq!(byte_cursor_to_char("", 5), 0);
}
#[test]
fn byte_cursor_to_char_emoji_four_byte_chars() {
let line = "gst🎯end";
assert_eq!(byte_cursor_to_char(line, 0), 0);
assert_eq!(byte_cursor_to_char(line, 3), 3, "before 🎯");
assert_eq!(byte_cursor_to_char(line, 7), 4, "after 🎯");
assert_eq!(byte_cursor_to_char(line, 8), 5);
}
#[test]
fn byte_cursor_to_char_round_trips_with_char_cursor_to_byte() {
let lines = [
"lsd ./おはよう ./test1",
"gst🎯end",
"a\u{0301}b",
"abc",
"",
];
for line in lines {
let char_count = line.chars().count();
for ch in 0..=char_count {
let byte = char_cursor_to_byte(line, ch);
let back = byte_cursor_to_char(line, byte);
assert_eq!(
back, ch,
"round-trip failed for line={line:?} char_cursor={ch}: byte={byte} back={back}"
);
}
}
}
#[test]
fn byte_cursor_to_char_combining_marks_are_separate_chars() {
let line = "a\u{0301}b";
assert_eq!(byte_cursor_to_char(line, 0), 0);
assert_eq!(byte_cursor_to_char(line, 1), 1, "between a and combining");
assert_eq!(byte_cursor_to_char(line, 3), 2, "between combining and b");
assert_eq!(byte_cursor_to_char(line, 4), 3);
}
#[test]
fn byte_cursor_to_char_non_char_boundary_rounds_down() {
let line = "lsd ./おはよう";
assert_eq!(byte_cursor_to_char(line, 7), 6);
assert_eq!(byte_cursor_to_char(line, 8), 6);
}
#[test]
fn utf16_cursor_to_byte_ascii_is_identity() {
assert_eq!(utf16_cursor_to_byte("hello", 0), 0);
assert_eq!(utf16_cursor_to_byte("hello", 3), 3);
assert_eq!(utf16_cursor_to_byte("hello", 5), 5);
}
#[test]
fn utf16_cursor_to_byte_bmp_chars_one_code_unit_each() {
let line = "lsd ./おはよう";
assert_eq!(utf16_cursor_to_byte(line, 6), 6, "before お");
assert_eq!(utf16_cursor_to_byte(line, 7), 9, "before は (1 UTF-16 unit, 3 bytes)");
assert_eq!(utf16_cursor_to_byte(line, 8), 12, "before よ");
assert_eq!(utf16_cursor_to_byte(line, 10), 18, "after う");
}
#[test]
fn utf16_cursor_to_byte_handles_surrogate_pair_for_emoji() {
let line = "🎯end";
assert_eq!(utf16_cursor_to_byte(line, 0), 0, "before 🎯");
assert_eq!(utf16_cursor_to_byte(line, 2), 4, "after 🎯 (2 UTF-16 units, 4 bytes)");
assert_eq!(utf16_cursor_to_byte(line, 3), 5, "after e");
assert_eq!(utf16_cursor_to_byte(line, 5), 7);
}
#[test]
fn utf16_cursor_to_byte_mid_surrogate_rounds_down() {
let line = "🎯end";
assert_eq!(utf16_cursor_to_byte(line, 1), 0);
}
#[test]
fn utf16_cursor_to_byte_past_line_end_clamps() {
let line = "abc";
assert_eq!(utf16_cursor_to_byte(line, 10), 3);
assert_eq!(utf16_cursor_to_byte(line, usize::MAX), 3);
}
#[test]
fn utf16_cursor_to_byte_empty_line() {
assert_eq!(utf16_cursor_to_byte("", 0), 0);
assert_eq!(utf16_cursor_to_byte("", 5), 0);
}
#[test]
fn byte_cursor_to_utf16_ascii_is_identity() {
assert_eq!(byte_cursor_to_utf16("hello", 0), 0);
assert_eq!(byte_cursor_to_utf16("hello", 3), 3);
assert_eq!(byte_cursor_to_utf16("hello", 5), 5);
}
#[test]
fn byte_cursor_to_utf16_japanese_three_byte_chars() {
let line = "lsd ./おはよう";
assert_eq!(byte_cursor_to_utf16(line, 6), 6, "before お");
assert_eq!(byte_cursor_to_utf16(line, 9), 7, "before は");
assert_eq!(byte_cursor_to_utf16(line, 12), 8, "before よ");
assert_eq!(byte_cursor_to_utf16(line, 18), 10, "after う");
}
#[test]
fn byte_cursor_to_utf16_emoji_surrogate_pair() {
let line = "🎯end";
assert_eq!(byte_cursor_to_utf16(line, 0), 0, "before 🎯");
assert_eq!(byte_cursor_to_utf16(line, 4), 2, "after 🎯");
assert_eq!(byte_cursor_to_utf16(line, 5), 3, "after e");
assert_eq!(byte_cursor_to_utf16(line, 7), 5);
}
#[test]
fn byte_cursor_to_utf16_round_trips_with_utf16_cursor_to_byte() {
let lines = [
"🎯end",
"lsd ./おはよう",
"abc",
"",
];
for line in lines {
let utf16_len: usize = line.chars().map(|c| c.len_utf16()).sum();
let mut utf16_pos = 0usize;
for ch in line.chars() {
let byte = utf16_cursor_to_byte(line, utf16_pos);
let back = byte_cursor_to_utf16(line, byte);
assert_eq!(
back, utf16_pos,
"round-trip failed for line={line:?} utf16_pos={utf16_pos}"
);
utf16_pos += ch.len_utf16();
}
assert_eq!(
byte_cursor_to_utf16(line, line.len()),
utf16_len,
"end-of-line round-trip failed for {line:?}"
);
}
}
}