Line data Source code
1 : //! The editable prompt of the shell.
2 : //!
3 : //! The prompt is a script whose cursor moves by extended grapheme clusters. Grapheme offsets are
4 : //! those of the whole text, which is short and segmented on demand.
5 :
6 : use super::unicode::{grapheme_boundaries, is_valid, next_grapheme, previous_grapheme};
7 : use dashql_core::catalog::Catalog;
8 : use dashql_core::script::Script;
9 :
10 : /// A grapheme position: the byte and code point offsets of a grapheme cluster boundary
11 : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
12 : struct Position {
13 : /// The byte offset of the boundary
14 : bytes: usize,
15 : /// The code point offset of the boundary
16 : codepoints: usize,
17 : /// The index of the grapheme cluster that starts at the boundary
18 : graphemes: usize,
19 : }
20 :
21 : /// The grapheme boundaries of a text
22 : struct Positions {
23 : /// The byte offsets of the boundaries, including the end
24 : bytes: Vec<usize>,
25 : /// The code point offsets of the boundaries, including the end
26 : codepoints: Vec<usize>,
27 : }
28 :
29 : impl Positions {
30 2301 : fn of(text: &str) -> Self {
31 2301 : let bytes = grapheme_boundaries(text);
32 2301 : let mut codepoints = Vec::with_capacity(bytes.len());
33 2301 : let mut count = 0;
34 2301 : let mut previous = 0;
35 62517 : for &b in &bytes {
36 62796 : count += text.as_bytes()[previous..b].iter().filter(|&&c| (c & 0xC0) != 0x80).count();
37 62517 : codepoints.push(count);
38 62517 : previous = b;
39 : }
40 2301 : Self { bytes, codepoints }
41 2301 : }
42 :
43 1962 : fn count(&self) -> usize {
44 1962 : self.bytes.len() - 1
45 1962 : }
46 :
47 2248 : fn position(&self, idx: usize) -> Position {
48 2248 : Position { bytes: self.bytes[idx], codepoints: self.codepoints[idx], graphemes: idx }
49 2248 : }
50 :
51 : /// The start of a grapheme cluster, the end for indexes past the end
52 1824 : fn resolve(&self, idx: usize) -> Position {
53 1824 : self.position(idx.min(self.count()))
54 1824 : }
55 :
56 : /// The boundary at an exact byte offset
57 158 : fn boundary(&self, byte: usize) -> Option<Position> {
58 158 : self.bytes.binary_search(&byte).ok().map(|idx| self.position(idx))
59 158 : }
60 :
61 : /// The first boundary at or after a byte offset
62 273 : fn at_or_after(&self, byte: usize) -> Position {
63 273 : let byte = byte.min(*self.bytes.last().unwrap());
64 1502 : self.position(self.bytes.partition_point(|&b| b < byte))
65 273 : }
66 :
67 : /// The boundary at a byte offset that callers expect to be one. A line break inside a CR LF cluster has no
68 : /// boundary; take the boundary before it.
69 24 : fn unchecked_boundary(&self, byte: usize) -> Position {
70 24 : self.boundary(byte)
71 24 : .unwrap_or_else(|| self.position(self.bytes.partition_point(|&b| b <= byte).saturating_sub(1)))
72 24 : }
73 : }
74 :
75 122 : fn is_word_grapheme(grapheme: &str) -> bool {
76 122 : grapheme.chars().next().is_some_and(|c| !c.is_ascii() || c.is_ascii_alphanumeric() || c == '_')
77 122 : }
78 :
79 : /// The prompt of a shell
80 : pub struct PromptBuffer {
81 : /// The script that holds the prompt text
82 : script: Script,
83 : /// The cursor position in grapheme clusters
84 : cursor_grapheme_offset: usize,
85 : /// The number of text changes
86 : revision: u64,
87 : }
88 :
89 : impl PromptBuffer {
90 129 : pub fn new(catalog: &Catalog) -> Self {
91 129 : Self { script: Script::new(catalog), cursor_grapheme_offset: 0, revision: 0 }
92 129 : }
93 :
94 2282 : fn positions(&self) -> Positions {
95 2282 : Positions::of(&self.script.to_text())
96 2282 : }
97 :
98 : /// The script of the prompt
99 1 : pub fn script(&self) -> &Script {
100 1 : &self.script
101 1 : }
102 :
103 619 : pub fn script_mut(&mut self) -> &mut Script {
104 619 : &mut self.script
105 619 : }
106 :
107 : /// The prompt text
108 1591 : pub fn text(&self) -> String {
109 1591 : self.script.to_text()
110 1591 : }
111 :
112 440 : pub fn revision(&self) -> u64 {
113 440 : self.revision
114 440 : }
115 :
116 : /// The number of grapheme clusters
117 124 : pub fn grapheme_count(&self) -> usize {
118 124 : self.positions().count()
119 124 : }
120 :
121 1542 : pub fn cursor_byte_offset(&self) -> usize {
122 1542 : self.positions().resolve(self.cursor_grapheme_offset).bytes
123 1542 : }
124 :
125 1 : pub fn cursor_codepoint_offset(&self) -> usize {
126 1 : self.positions().resolve(self.cursor_grapheme_offset).codepoints
127 1 : }
128 :
129 : /// Replace the text and move the cursor to the end
130 91 : pub fn set_text(&mut self, text: &[u8]) -> bool {
131 91 : let Ok(text) = std::str::from_utf8(text) else {
132 2 : return false;
133 : };
134 89 : if text != self.script.to_text() {
135 78 : self.script.replace_text(text);
136 78 : self.revision += 1;
137 78 : }
138 89 : self.cursor_grapheme_offset = self.grapheme_count();
139 89 : true
140 91 : }
141 :
142 : /// Insert text at the cursor
143 241 : pub fn insert(&mut self, text: &[u8]) -> bool {
144 241 : if text.is_empty() {
145 3 : return false;
146 238 : }
147 238 : let Ok(text) = std::str::from_utf8(text) else {
148 3 : return false;
149 : };
150 235 : let cursor = self.positions().resolve(self.cursor_grapheme_offset);
151 235 : let desired_byte_offset = cursor.bytes + text.len();
152 235 : self.script.insert_text_at(cursor.codepoints, text);
153 235 : self.revision += 1;
154 235 : self.cursor_grapheme_offset = self.positions().at_or_after(desired_byte_offset).graphemes;
155 235 : true
156 241 : }
157 :
158 37 : pub fn move_left(&mut self) -> bool {
159 37 : if self.cursor_grapheme_offset == 0 {
160 1 : return false;
161 36 : }
162 36 : self.cursor_grapheme_offset -= 1;
163 36 : true
164 37 : }
165 :
166 26 : pub fn move_right(&mut self) -> bool {
167 26 : if self.cursor_grapheme_offset >= self.grapheme_count() {
168 18 : return false;
169 8 : }
170 8 : self.cursor_grapheme_offset += 1;
171 8 : true
172 26 : }
173 :
174 8 : pub fn move_to_start(&mut self) -> bool {
175 8 : if self.cursor_grapheme_offset == 0 {
176 1 : return false;
177 7 : }
178 7 : self.cursor_grapheme_offset = 0;
179 7 : true
180 8 : }
181 :
182 6 : pub fn move_to_end(&mut self) -> bool {
183 6 : let end = self.grapheme_count();
184 6 : if self.cursor_grapheme_offset == end {
185 3 : return false;
186 3 : }
187 3 : self.cursor_grapheme_offset = end;
188 3 : true
189 6 : }
190 :
191 6 : pub fn move_to_line_start(&mut self) -> bool {
192 6 : let text = self.text();
193 6 : let cursor = self.cursor_byte_offset();
194 67 : let begin = text.as_bytes()[..cursor].iter().rposition(|&c| c == b'\n').map_or(0, |i| i + 1);
195 6 : self.move_to_byte_offset(begin)
196 6 : }
197 :
198 4 : pub fn move_to_line_end(&mut self) -> bool {
199 4 : let text = self.text();
200 4 : let cursor = self.cursor_byte_offset();
201 4 : let end = find_from(text.as_bytes(), b'\n', cursor).unwrap_or(text.len());
202 4 : self.move_to_byte_offset(end)
203 4 : }
204 :
205 : /// The byte offset of the word start left of the cursor
206 14 : fn word_left(&self, text: &str, mut cursor: usize) -> usize {
207 19 : while cursor > 0 {
208 17 : let previous = previous_grapheme(text, cursor);
209 17 : if is_word_grapheme(&text[previous..cursor]) {
210 12 : break;
211 5 : }
212 5 : cursor = previous;
213 : }
214 50 : while cursor > 0 {
215 46 : let previous = previous_grapheme(text, cursor);
216 46 : if !is_word_grapheme(&text[previous..cursor]) {
217 10 : break;
218 36 : }
219 36 : cursor = previous;
220 : }
221 14 : cursor
222 14 : }
223 :
224 14 : pub fn move_word_left(&mut self) -> bool {
225 14 : let text = self.text();
226 14 : let original = self.cursor_byte_offset();
227 14 : let cursor = self.word_left(&text, original);
228 14 : cursor != original && self.move_to_byte_offset(cursor)
229 14 : }
230 :
231 12 : pub fn move_word_right(&mut self) -> bool {
232 12 : let text = self.text();
233 12 : let original = self.cursor_byte_offset();
234 12 : let mut cursor = original;
235 13 : while cursor < text.len() && !is_word_grapheme(&text[cursor..next_grapheme(&text, cursor)]) {
236 1 : cursor = next_grapheme(&text, cursor);
237 1 : }
238 50 : while cursor < text.len() && is_word_grapheme(&text[cursor..next_grapheme(&text, cursor)]) {
239 38 : cursor = next_grapheme(&text, cursor);
240 38 : }
241 12 : cursor != original && self.move_to_byte_offset(cursor)
242 12 : }
243 :
244 8 : pub fn move_up(&mut self) -> bool {
245 8 : let text = self.text();
246 8 : let bytes = text.as_bytes();
247 8 : let positions = Positions::of(&text);
248 8 : let cursor = positions.resolve(self.cursor_grapheme_offset).bytes;
249 31 : let Some(line_begin) = bytes[..cursor].iter().rposition(|&c| c == b'\n') else {
250 3 : return false;
251 : };
252 5 : let previous_line_end = line_begin;
253 34 : let previous_line_begin = bytes[..previous_line_end].iter().rposition(|&c| c == b'\n').map_or(0, |i| i + 1);
254 5 : let column = self.cursor_grapheme_offset - positions.unchecked_boundary(line_begin + 1).graphemes;
255 5 : let previous_line_column = positions.unchecked_boundary(previous_line_begin).graphemes;
256 5 : let previous_line_length = positions.unchecked_boundary(previous_line_end).graphemes - previous_line_column;
257 5 : self.cursor_grapheme_offset = previous_line_column + column.min(previous_line_length);
258 5 : true
259 8 : }
260 :
261 4 : pub fn move_down(&mut self) -> bool {
262 4 : let text = self.text();
263 4 : let bytes = text.as_bytes();
264 4 : let positions = Positions::of(&text);
265 4 : let cursor = positions.resolve(self.cursor_grapheme_offset).bytes;
266 4 : let Some(next_line_break) = find_from(bytes, b'\n', cursor) else {
267 1 : return false;
268 : };
269 12 : let line_begin = bytes[..cursor].iter().rposition(|&c| c == b'\n').map_or(0, |i| i + 1);
270 3 : let next_line_begin = next_line_break + 1;
271 3 : let next_line_end = find_from(bytes, b'\n', next_line_begin).unwrap_or(bytes.len());
272 3 : let column = self.cursor_grapheme_offset.wrapping_sub(positions.unchecked_boundary(line_begin).graphemes);
273 3 : let next_line_column = positions.unchecked_boundary(next_line_begin).graphemes;
274 3 : let next_line_length = positions.unchecked_boundary(next_line_end).graphemes - next_line_column;
275 3 : self.cursor_grapheme_offset = next_line_column + column.min(next_line_length);
276 3 : true
277 4 : }
278 :
279 : /// Move the cursor to a grapheme boundary
280 52 : pub fn move_to_byte_offset(&mut self, byte_offset: usize) -> bool {
281 52 : match self.positions().boundary(byte_offset) {
282 51 : Some(position) => {
283 51 : self.cursor_grapheme_offset = position.graphemes;
284 51 : true
285 : }
286 1 : None => false,
287 : }
288 52 : }
289 :
290 8 : pub fn delete_backward(&mut self) -> bool {
291 8 : if self.cursor_grapheme_offset == 0 {
292 1 : return false;
293 7 : }
294 7 : let positions = self.positions();
295 7 : let begin = positions.resolve(self.cursor_grapheme_offset - 1);
296 7 : let end = positions.resolve(self.cursor_grapheme_offset);
297 7 : self.script.erase_text_range(begin.codepoints, end.codepoints - begin.codepoints);
298 7 : self.cursor_grapheme_offset -= 1;
299 7 : self.revision += 1;
300 7 : true
301 8 : }
302 :
303 7 : pub fn delete_forward(&mut self) -> bool {
304 7 : let positions = self.positions();
305 7 : if self.cursor_grapheme_offset >= positions.count() {
306 3 : return false;
307 4 : }
308 4 : let begin = positions.resolve(self.cursor_grapheme_offset);
309 4 : let end = positions.resolve(self.cursor_grapheme_offset + 1);
310 4 : self.script.erase_text_range(begin.codepoints, end.codepoints - begin.codepoints);
311 4 : self.revision += 1;
312 4 : true
313 7 : }
314 :
315 5 : pub fn delete_to_line_start(&mut self, deleted: Option<&mut String>) -> bool {
316 5 : let text = self.text();
317 5 : let end = self.cursor_byte_offset();
318 36 : let begin = text.as_bytes()[..end].iter().rposition(|&c| c == b'\n').map_or(0, |i| i + 1);
319 5 : if begin == end {
320 1 : return false;
321 4 : }
322 4 : if let Some(deleted) = deleted {
323 4 : *deleted = text[begin..end].to_string();
324 4 : }
325 4 : self.replace_byte_range(begin, end - begin, "")
326 5 : }
327 :
328 6 : pub fn delete_to_line_end(&mut self, deleted: Option<&mut String>) -> bool {
329 6 : let text = self.text();
330 6 : let begin = self.cursor_byte_offset();
331 6 : let end = match find_from(text.as_bytes(), b'\n', begin) {
332 3 : Some(line_break) if line_break == begin => line_break + 1,
333 1 : Some(line_break) => line_break,
334 3 : None => text.len(),
335 : };
336 6 : if begin == end {
337 2 : return false;
338 4 : }
339 4 : if let Some(deleted) = deleted {
340 4 : *deleted = text[begin..end].to_string();
341 4 : }
342 4 : self.replace_byte_range(begin, end - begin, "")
343 6 : }
344 :
345 5 : pub fn delete_word_backward(&mut self, deleted: Option<&mut String>) -> bool {
346 5 : let end = self.cursor_byte_offset();
347 5 : if !self.move_word_left() {
348 1 : return false;
349 4 : }
350 4 : let begin = self.cursor_byte_offset();
351 4 : if let Some(deleted) = deleted {
352 4 : *deleted = self.text()[begin..end].to_string();
353 4 : }
354 4 : self.replace_byte_range(begin, end - begin, "")
355 5 : }
356 :
357 5 : pub fn delete_word_forward(&mut self, deleted: Option<&mut String>) -> bool {
358 5 : let begin = self.cursor_byte_offset();
359 5 : if !self.move_word_right() {
360 1 : return false;
361 4 : }
362 4 : let end = self.cursor_byte_offset();
363 4 : if let Some(deleted) = deleted {
364 4 : *deleted = self.text()[begin..end].to_string();
365 4 : }
366 4 : self.replace_byte_range(begin, end - begin, "")
367 5 : }
368 :
369 7 : pub fn transpose_characters(&mut self) -> bool {
370 7 : let text = self.text();
371 7 : let positions = Positions::of(&text);
372 7 : let count = positions.count();
373 7 : if count < 2 || self.cursor_grapheme_offset == 0 {
374 3 : return false;
375 4 : }
376 4 : let left_index = if self.cursor_grapheme_offset == count { count - 2 } else { self.cursor_grapheme_offset - 1 };
377 4 : let right_index = left_index + 1;
378 4 : let begin = positions.resolve(left_index).bytes;
379 4 : let middle = positions.resolve(right_index).bytes;
380 4 : let end = positions.resolve(right_index + 1).bytes;
381 4 : let mut replacement = text[middle..end].to_string();
382 4 : replacement.push_str(&text[begin..middle]);
383 4 : self.replace_byte_range(begin, end - begin, &replacement)
384 7 : }
385 :
386 : /// Replace a byte range on grapheme boundaries and move the cursor behind the replacement
387 41 : pub fn replace_byte_range(&mut self, byte_offset: usize, byte_length: usize, text: &str) -> bool {
388 41 : let positions = self.positions();
389 38 : let (Some(begin), Some(end)) =
390 41 : (positions.boundary(byte_offset), positions.boundary(byte_offset.saturating_add(byte_length)))
391 : else {
392 3 : return false;
393 : };
394 38 : let deleted = end.codepoints - begin.codepoints;
395 38 : if deleted != 0 {
396 35 : self.script.erase_text_range(begin.codepoints, deleted);
397 35 : }
398 38 : if !text.is_empty() {
399 22 : self.script.insert_text_at(begin.codepoints, text);
400 30 : }
401 38 : self.revision += 1;
402 38 : self.cursor_grapheme_offset = self.positions().at_or_after(byte_offset + text.len()).graphemes;
403 38 : true
404 41 : }
405 :
406 : /// Replace a byte range with bytes that must be valid UTF-8
407 2 : pub fn replace_byte_range_bytes(&mut self, byte_offset: usize, byte_length: usize, text: &[u8]) -> bool {
408 2 : if !is_valid(text) {
409 1 : return false;
410 1 : }
411 1 : self.replace_byte_range(byte_offset, byte_length, std::str::from_utf8(text).unwrap())
412 2 : }
413 : }
414 :
415 : /// Find a byte at or after an offset
416 2220 : pub(super) fn find_from(text: &[u8], needle: u8, from: usize) -> Option<usize> {
417 2220 : if from > text.len() {
418 0 : return None;
419 2220 : }
420 21924 : text[from..].iter().position(|&c| c == needle).map(|i| i + from)
421 2220 : }
|