par_term/copy_mode/
motion.rs1use super::types::CopyModeState;
4use crate::smart_selection::is_word_char;
5
6impl CopyModeState {
7 pub fn move_word_forward(&mut self, line_text: &str, word_chars: &str) {
13 let count = self.effective_count();
14 let chars: Vec<char> = line_text.chars().collect();
15 let mut col = self.cursor_col;
16
17 for _ in 0..count {
18 if col >= chars.len() {
19 break;
20 }
21 while col < chars.len() && is_word_char(chars[col], word_chars) {
23 col += 1;
24 }
25 while col < chars.len() && !is_word_char(chars[col], word_chars) {
27 col += 1;
28 }
29 }
30
31 self.cursor_col = col.min(self.cols.saturating_sub(1));
32 }
33
34 pub fn move_word_backward(&mut self, line_text: &str, word_chars: &str) {
36 let count = self.effective_count();
37 let chars: Vec<char> = line_text.chars().collect();
38 let mut col = self.cursor_col.min(chars.len().saturating_sub(1));
43
44 for _ in 0..count {
45 if col == 0 {
46 break;
47 }
48 col = col.saturating_sub(1);
49 while col > 0 && !is_word_char(chars[col], word_chars) {
51 col -= 1;
52 }
53 while col > 0 && is_word_char(chars[col - 1], word_chars) {
55 col -= 1;
56 }
57 }
58
59 self.cursor_col = col;
60 }
61
62 pub fn move_word_end(&mut self, line_text: &str, word_chars: &str) {
64 let count = self.effective_count();
65 let chars: Vec<char> = line_text.chars().collect();
66 let mut col = self.cursor_col;
67
68 for _ in 0..count {
69 if col >= chars.len().saturating_sub(1) {
70 break;
71 }
72 col += 1;
73 while col < chars.len() && !is_word_char(chars[col], word_chars) {
75 col += 1;
76 }
77 while col < chars.len().saturating_sub(1) && is_word_char(chars[col + 1], word_chars) {
79 col += 1;
80 }
81 }
82
83 self.cursor_col = col.min(self.cols.saturating_sub(1));
84 }
85
86 pub fn move_big_word_forward(&mut self, line_text: &str) {
88 let count = self.effective_count();
89 let chars: Vec<char> = line_text.chars().collect();
90 let mut col = self.cursor_col;
91
92 for _ in 0..count {
93 while col < chars.len() && !chars[col].is_whitespace() {
95 col += 1;
96 }
97 while col < chars.len() && chars[col].is_whitespace() {
99 col += 1;
100 }
101 }
102
103 self.cursor_col = col.min(self.cols.saturating_sub(1));
104 }
105
106 pub fn move_big_word_backward(&mut self, line_text: &str) {
108 let count = self.effective_count();
109 let chars: Vec<char> = line_text.chars().collect();
110 let mut col = self.cursor_col.min(chars.len().saturating_sub(1));
112
113 for _ in 0..count {
114 if col == 0 {
115 break;
116 }
117 col = col.saturating_sub(1);
118 while col > 0 && chars[col].is_whitespace() {
120 col -= 1;
121 }
122 while col > 0 && !chars[col - 1].is_whitespace() {
124 col -= 1;
125 }
126 }
127
128 self.cursor_col = col;
129 }
130
131 pub fn move_big_word_end(&mut self, line_text: &str) {
133 let count = self.effective_count();
134 let chars: Vec<char> = line_text.chars().collect();
135 let mut col = self.cursor_col;
136
137 for _ in 0..count {
138 if col >= chars.len().saturating_sub(1) {
139 break;
140 }
141 col += 1;
142 while col < chars.len() && chars[col].is_whitespace() {
144 col += 1;
145 }
146 while col < chars.len().saturating_sub(1) && !chars[col + 1].is_whitespace() {
148 col += 1;
149 }
150 }
151
152 self.cursor_col = col.min(self.cols.saturating_sub(1));
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::CopyModeState;
159
160 const CJK_LINE: &str = "日本語 test";
163 const ACCENTED_LINE: &str = "café au lait";
164 const EMOJI_LINE: &str = "😀 😁 done";
165
166 fn at_line_end(cols: usize) -> CopyModeState {
167 let mut cm = CopyModeState::new();
168 cm.enter(0, 0, cols, 24, 0);
169 cm.move_to_line_end();
170 cm
171 }
172
173 #[test]
174 fn word_backward_from_line_end_on_non_ascii_line() {
175 for line in [CJK_LINE, ACCENTED_LINE, EMOJI_LINE] {
178 let mut cm = at_line_end(80);
179 cm.move_word_backward(line, "");
180 assert!(
181 cm.cursor_col < line.chars().count(),
182 "{line:?} left cursor at {}",
183 cm.cursor_col
184 );
185
186 let mut cm = at_line_end(80);
187 cm.move_big_word_backward(line);
188 assert!(
189 cm.cursor_col < line.chars().count(),
190 "{line:?} left cursor at {}",
191 cm.cursor_col
192 );
193 }
194 }
195
196 #[test]
197 fn word_backward_lands_on_last_word_start() {
198 let mut cm = at_line_end(80);
199 cm.move_word_backward(CJK_LINE, "");
200 assert_eq!(cm.cursor_col, 4);
202
203 let mut cm = at_line_end(80);
204 cm.move_big_word_backward(ACCENTED_LINE);
205 assert_eq!(cm.cursor_col, 8);
207 }
208
209 #[test]
210 fn word_backward_handles_empty_and_short_lines() {
211 let mut cm = at_line_end(80);
212 cm.move_word_backward("", "");
213 assert_eq!(cm.cursor_col, 0);
214
215 let mut cm = at_line_end(80);
216 cm.move_big_word_backward("é");
217 assert_eq!(cm.cursor_col, 0);
218 }
219}