use super::main::{Zle, ZleChar};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WordStyle {
Emacs,
Vi,
Shell,
BlankDelimited,
}
impl Zle {
pub fn find_word_start(&self, style: WordStyle) -> usize {
let mut pos = self.zlecs;
match style {
WordStyle::Emacs => {
while pos > 0 && !is_emacs_word_char(self.zleline[pos - 1]) {
pos -= 1;
}
while pos > 0 && is_emacs_word_char(self.zleline[pos - 1]) {
pos -= 1;
}
}
WordStyle::Vi => {
while pos > 0 && self.zleline[pos - 1].is_whitespace() {
pos -= 1;
}
if pos > 0 {
let is_word = is_vi_word_char(self.zleline[pos - 1]);
while pos > 0 {
let c = self.zleline[pos - 1];
if c.is_whitespace() || (is_vi_word_char(c) != is_word) {
break;
}
pos -= 1;
}
}
}
WordStyle::Shell => {
pos = shell_word_start_before(&self.zleline[..self.zlell], pos);
}
WordStyle::BlankDelimited => {
while pos > 0 && self.zleline[pos - 1].is_whitespace() {
pos -= 1;
}
while pos > 0 && !self.zleline[pos - 1].is_whitespace() {
pos -= 1;
}
}
}
pos
}
pub fn find_word_end(&self, style: WordStyle) -> usize {
let mut pos = self.zlecs;
match style {
WordStyle::Emacs => {
while pos < self.zlell && !is_emacs_word_char(self.zleline[pos]) {
pos += 1;
}
while pos < self.zlell && is_emacs_word_char(self.zleline[pos]) {
pos += 1;
}
}
WordStyle::Vi => {
if pos < self.zlell {
let is_word = is_vi_word_char(self.zleline[pos]);
while pos < self.zlell {
let c = self.zleline[pos];
if c.is_whitespace() || (is_vi_word_char(c) != is_word) {
break;
}
pos += 1;
}
while pos < self.zlell && self.zleline[pos].is_whitespace() {
pos += 1;
}
}
}
WordStyle::Shell => {
pos = shell_word_end_after(&self.zleline[..self.zlell], pos);
}
WordStyle::BlankDelimited => {
while pos < self.zlell && !self.zleline[pos].is_whitespace() {
pos += 1;
}
while pos < self.zlell && self.zleline[pos].is_whitespace() {
pos += 1;
}
}
}
pos
}
pub fn get_current_word(&self, style: WordStyle) -> &[ZleChar] {
let start = self.find_word_start(style);
let end = self.find_word_end(style);
&self.zleline[start..end]
}
}
fn is_emacs_word_char(c: ZleChar) -> bool {
c.is_alphanumeric() || c == '_'
}
fn is_vi_word_char(c: ZleChar) -> bool {
c.is_alphanumeric() || c == '_'
}
fn shell_words(line: &[ZleChar]) -> Vec<(usize, usize)> {
let mut out = Vec::new();
let mut i = 0;
let n = line.len();
while i < n {
while i < n && line[i].is_whitespace() {
i += 1;
}
if i >= n {
break;
}
let start = i;
let mut in_single = false;
let mut in_double = false;
while i < n {
let c = line[i];
if in_single {
if c == '\'' {
in_single = false;
}
i += 1;
continue;
}
if in_double {
if c == '\\' && i + 1 < n {
i += 2;
continue;
}
if c == '"' {
in_double = false;
}
i += 1;
continue;
}
if c == '\\' && i + 1 < n {
i += 2;
continue;
}
if c == '\'' {
in_single = true;
i += 1;
continue;
}
if c == '"' {
in_double = true;
i += 1;
continue;
}
if c.is_whitespace() {
break;
}
i += 1;
}
out.push((start, i));
}
out
}
pub fn shell_words_for_test(line: &[ZleChar]) -> Vec<(usize, usize)> {
shell_words(line)
}
pub(crate) fn shell_word_start_before(line: &[ZleChar], pos: usize) -> usize {
let words = shell_words(line);
for (s, e) in words.iter().rev() {
if *s <= pos && pos <= *e {
if pos == *s {
continue;
}
return *s;
}
if *e < pos {
return *s;
}
}
0
}
pub(crate) fn shell_word_end_after(line: &[ZleChar], pos: usize) -> usize {
let words = shell_words(line);
for (s, e) in words {
if pos >= s && pos < e {
return e;
}
if pos < s {
return e;
}
}
line.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn chars(s: &str) -> Vec<char> {
s.chars().collect()
}
#[test]
fn shell_words_splits_on_whitespace() {
let line = chars("echo hello world");
assert_eq!(shell_words(&line), vec![(0, 4), (5, 10), (11, 16)]);
}
#[test]
fn shell_words_keeps_double_quoted_run_intact() {
let line = chars(r#"echo "hello world""#);
assert_eq!(shell_words(&line), vec![(0, 4), (5, 18)]);
}
#[test]
fn shell_words_keeps_single_quoted_run_intact() {
let line = chars("a 'b c' d");
assert_eq!(shell_words(&line), vec![(0, 1), (2, 7), (8, 9)]);
}
#[test]
fn shell_words_treats_backslash_escape_as_part_of_word() {
let line = chars(r"foo\ bar baz");
assert_eq!(shell_words(&line), vec![(0, 8), (9, 12)]);
}
#[test]
fn shell_word_end_after_advances_into_next_word() {
let line = chars("aa bb cc");
assert_eq!(shell_word_end_after(&line, 2), 5);
assert_eq!(shell_word_end_after(&line, 0), 2);
}
#[test]
fn shell_word_start_before_returns_word_start() {
let line = chars("aa bb cc");
assert_eq!(shell_word_start_before(&line, 4), 3);
assert_eq!(shell_word_start_before(&line, 3), 0);
assert_eq!(shell_word_start_before(&line, 5), 3);
}
}