Skip to main content

zsh/zle/
word.rs

1//! ZLE word operations
2//!
3//! Direct port from zsh/Src/Zle/zle_word.c
4
5use super::main::{Zle, ZleChar};
6
7/// Word style for movement
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum WordStyle {
10    /// Emacs-style words (alphanumeric + underscore)
11    Emacs,
12    /// Vi-style words (separated by whitespace and punctuation)
13    Vi,
14    /// Shell words (quoted strings, etc.)
15    Shell,
16    /// Whitespace-separated "WORDS"
17    BlankDelimited,
18}
19
20impl Zle {
21    /// Find the start of the current/previous word
22    pub fn find_word_start(&self, style: WordStyle) -> usize {
23        let mut pos = self.zlecs;
24
25        match style {
26            WordStyle::Emacs => {
27                // Skip non-word characters
28                while pos > 0 && !is_emacs_word_char(self.zleline[pos - 1]) {
29                    pos -= 1;
30                }
31                // Skip word characters
32                while pos > 0 && is_emacs_word_char(self.zleline[pos - 1]) {
33                    pos -= 1;
34                }
35            }
36            WordStyle::Vi => {
37                // Skip whitespace
38                while pos > 0 && self.zleline[pos - 1].is_whitespace() {
39                    pos -= 1;
40                }
41                if pos > 0 {
42                    let is_word = is_vi_word_char(self.zleline[pos - 1]);
43                    // Skip same class of characters
44                    while pos > 0 {
45                        let c = self.zleline[pos - 1];
46                        if c.is_whitespace() || (is_vi_word_char(c) != is_word) {
47                            break;
48                        }
49                        pos -= 1;
50                    }
51                }
52            }
53            WordStyle::Shell => {
54                // TODO: implement shell word boundaries
55                while pos > 0 && !self.zleline[pos - 1].is_whitespace() {
56                    pos -= 1;
57                }
58            }
59            WordStyle::BlankDelimited => {
60                // Skip whitespace
61                while pos > 0 && self.zleline[pos - 1].is_whitespace() {
62                    pos -= 1;
63                }
64                // Skip non-whitespace
65                while pos > 0 && !self.zleline[pos - 1].is_whitespace() {
66                    pos -= 1;
67                }
68            }
69        }
70
71        pos
72    }
73
74    /// Find the end of the current/next word
75    pub fn find_word_end(&self, style: WordStyle) -> usize {
76        let mut pos = self.zlecs;
77
78        match style {
79            WordStyle::Emacs => {
80                // Skip non-word characters
81                while pos < self.zlell && !is_emacs_word_char(self.zleline[pos]) {
82                    pos += 1;
83                }
84                // Skip word characters
85                while pos < self.zlell && is_emacs_word_char(self.zleline[pos]) {
86                    pos += 1;
87                }
88            }
89            WordStyle::Vi => {
90                if pos < self.zlell {
91                    let is_word = is_vi_word_char(self.zleline[pos]);
92                    // Skip same class of characters
93                    while pos < self.zlell {
94                        let c = self.zleline[pos];
95                        if c.is_whitespace() || (is_vi_word_char(c) != is_word) {
96                            break;
97                        }
98                        pos += 1;
99                    }
100                    // Skip whitespace
101                    while pos < self.zlell && self.zleline[pos].is_whitespace() {
102                        pos += 1;
103                    }
104                }
105            }
106            WordStyle::Shell => {
107                // TODO: implement shell word boundaries
108                while pos < self.zlell && !self.zleline[pos].is_whitespace() {
109                    pos += 1;
110                }
111                while pos < self.zlell && self.zleline[pos].is_whitespace() {
112                    pos += 1;
113                }
114            }
115            WordStyle::BlankDelimited => {
116                // Skip non-whitespace
117                while pos < self.zlell && !self.zleline[pos].is_whitespace() {
118                    pos += 1;
119                }
120                // Skip whitespace
121                while pos < self.zlell && self.zleline[pos].is_whitespace() {
122                    pos += 1;
123                }
124            }
125        }
126
127        pos
128    }
129
130    /// Get the current word
131    pub fn get_current_word(&self, style: WordStyle) -> &[ZleChar] {
132        let start = self.find_word_start(style);
133        let end = self.find_word_end(style);
134        &self.zleline[start..end]
135    }
136}
137
138/// Check if character is an emacs word character
139fn is_emacs_word_char(c: ZleChar) -> bool {
140    c.is_alphanumeric() || c == '_'
141}
142
143/// Check if character is a vi word character (alphanumeric)
144fn is_vi_word_char(c: ZleChar) -> bool {
145    c.is_alphanumeric() || c == '_'
146}