vtcode_ui/tui/core_tui/session/
editing.rs1use super::{InlinePromptSuggestionSource, Session};
2use crate::tui::config::constants::ui;
3use crate::vim::{next_char_boundary, prev_char_boundary};
4use unicode_segmentation::UnicodeSegmentation;
13
14const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
15
16fn is_word_separator(ch: char) -> bool {
17 WORD_SEPARATORS.contains(ch)
18}
19
20fn sanitize_pasted_text(text: &str) -> String {
23 text.chars().filter(|&ch| ch != '\r' && ch != '\u{7f}').collect()
24}
25
26fn is_separator_piece(piece: &str) -> bool {
27 piece.chars().all(is_word_separator)
28}
29
30fn split_word_pieces(run: &str) -> Vec<(usize, &str)> {
31 let mut pieces = Vec::new();
32 for (segment_start, segment) in run.split_word_bound_indices() {
33 let mut piece_start = 0;
34 let mut chars = segment.char_indices();
35 let Some((_, first_char)) = chars.next() else {
36 continue;
37 };
38 let mut in_separator = is_word_separator(first_char);
39
40 for (idx, ch) in chars {
41 let is_separator = is_word_separator(ch);
42 if is_separator == in_separator {
43 continue;
44 }
45
46 pieces.push((segment_start + piece_start, &segment[piece_start..idx]));
47 piece_start = idx;
48 in_separator = is_separator;
49 }
50
51 pieces.push((segment_start + piece_start, &segment[piece_start..]));
52 }
53
54 pieces
55}
56
57fn previous_word_boundary(content: &str, cursor: usize) -> usize {
58 if cursor == 0 {
59 return 0;
60 }
61
62 let prefix = &content[..cursor];
63 let Some((first_non_ws_idx, ch)) = prefix.char_indices().rev().find(|&(_, ch)| !ch.is_whitespace()) else {
64 return 0;
65 };
66
67 let run_start = prefix[..first_non_ws_idx]
68 .char_indices()
69 .rev()
70 .find(|&(_, ch)| ch.is_whitespace())
71 .map_or(0, |(idx, ch)| idx + ch.len_utf8());
72 let run_end = first_non_ws_idx + ch.len_utf8();
73 let pieces = split_word_pieces(&prefix[run_start..run_end]);
74 let mut pieces = pieces.into_iter().rev().peekable();
75 let Some((piece_start, piece)) = pieces.next() else {
76 return run_start;
77 };
78 let mut start = run_start + piece_start;
79
80 if is_separator_piece(piece) {
81 while let Some((idx, piece)) = pieces.peek() {
82 if !is_separator_piece(piece) {
83 break;
84 }
85 start = run_start + *idx;
86 pieces.next();
87 }
88 }
89
90 start
91}
92
93fn next_word_boundary(content: &str, cursor: usize) -> usize {
94 if cursor >= content.len() {
95 return content.len();
96 }
97
98 let suffix = &content[cursor..];
99 let Some(first_non_ws) = suffix.find(|ch: char| !ch.is_whitespace()) else {
100 return content.len();
101 };
102
103 let run = &suffix[first_non_ws..];
104 let run = &run[..run.find(char::is_whitespace).unwrap_or(run.len())];
105 let mut pieces = split_word_pieces(run).into_iter().peekable();
106 let Some((start, piece)) = pieces.next() else {
107 return cursor + first_non_ws;
108 };
109
110 let word_start = cursor + first_non_ws + start;
111 let mut end = word_start + piece.len();
112 if is_separator_piece(piece) {
113 while let Some((idx, piece)) = pieces.peek() {
114 if !is_separator_piece(piece) {
115 break;
116 }
117 end = cursor + first_non_ws + *idx + piece.len();
118 pieces.next();
119 }
120 }
121
122 end
123}
124
125impl Session {
126 pub(crate) fn refresh_input_edit_state(&mut self) {
127 self.clear_suggested_prompt_state();
128 self.clear_inline_prompt_suggestion();
129 self.input_compact_mode = self.input_compact_placeholder().is_some();
130 self.invalidate_header_cache();
131 }
132
133 pub(crate) fn set_inline_prompt_suggestion(&mut self, suggestion: String, llm_generated: bool) {
134 let trimmed = suggestion.trim();
135 if trimmed.is_empty() {
136 self.clear_inline_prompt_suggestion();
137 return;
138 }
139
140 self.inline_prompt_suggestion.suggestion = Some(trimmed.to_string());
141 self.inline_prompt_suggestion.source = Some(if llm_generated {
142 InlinePromptSuggestionSource::Llm
143 } else {
144 InlinePromptSuggestionSource::Local
145 });
146 self.mark_dirty();
147 }
148
149 pub(crate) fn accept_inline_prompt_suggestion(&mut self) -> bool {
150 let Some(suffix) = self.visible_inline_prompt_suggestion_suffix() else {
151 return false;
152 };
153
154 self.input_manager.insert_text(&suffix);
155 self.clear_inline_prompt_suggestion();
156 self.mark_dirty();
157 true
158 }
159
160 pub(crate) fn insert_char(&mut self, ch: char) {
162 if ch == '\u{7f}' {
163 return;
164 }
165 if ch == '\n' && !self.can_insert_newline() {
166 return;
167 }
168 self.input_manager.insert_char(ch);
169 self.refresh_input_edit_state();
170 }
171
172 pub(crate) fn insert_paste_text(&mut self, text: &str) {
182 let sanitized = sanitize_pasted_text(text);
183
184 if sanitized.is_empty() {
185 return;
186 }
187
188 let was_collapsed = self.input_compact_mode;
189 let paste_start = self
190 .input_manager
191 .selection_range()
192 .map_or_else(|| self.input_manager.cursor(), |(start, _)| start);
193 let paste_end = paste_start.saturating_add(sanitized.len());
194 let should_collapse = super::input::should_track_compact_paste(&sanitized);
195 self.input_manager.insert_text(&sanitized);
196 if should_collapse {
197 self.input_manager.set_compact_paste_range(paste_start..paste_end);
198 }
199 self.refresh_input_edit_state();
200 if was_collapsed && self.input_compact_placeholder().is_some() {
201 self.input_compact_mode = false;
202 }
203 }
204
205 pub(crate) fn insert_raw_paste_text(&mut self, text: &str) {
211 let sanitized = sanitize_pasted_text(text);
212
213 if sanitized.is_empty() {
214 return;
215 }
216
217 self.input_manager.insert_text(&sanitized);
218 self.refresh_input_edit_state();
219 self.input_compact_mode = false;
220 }
221
222 pub(crate) fn apply_suggested_prompt(&mut self, text: String) {
223 let trimmed = text.trim();
224 if trimmed.is_empty() {
225 return;
226 }
227
228 let merged = if self.input_manager.content().trim().is_empty() {
229 trimmed.to_string()
230 } else {
231 let trimmed_end = self.input_manager.content().trim_end();
232 let cap = trimmed_end.len() + 2 + trimmed.len();
233 let mut s = String::with_capacity(cap);
234 s.push_str(trimmed_end);
235 s.push_str("\n\n");
236 s.push_str(trimmed);
237 s
238 };
239
240 self.input_manager.set_content(merged);
241 self.input_manager.set_cursor(self.input_manager.content().len());
242 self.suggested_prompt_state.active = true;
243 self.input_compact_mode = self.input_compact_placeholder().is_some();
244 self.mark_dirty();
245 }
246
247 fn remaining_newline_capacity(&self) -> usize {
249 let content = self.input_manager.content();
250 let mut newline_count = content.matches('\n').count();
251 if let Some(range) = self.input_manager.compact_paste_range()
252 && range.end <= content.len()
253 && content.is_char_boundary(range.start)
254 && content.is_char_boundary(range.end)
255 {
256 newline_count = newline_count.saturating_sub(content[range].matches('\n').count());
257 }
258
259 ui::INLINE_INPUT_MAX_LINES.saturating_sub(1).saturating_sub(newline_count)
260 }
261
262 fn can_insert_newline(&self) -> bool {
264 self.remaining_newline_capacity() > 0
265 }
266
267 pub(crate) fn delete_char(&mut self) {
269 if self.input_compact_mode
276 && let Some(range) = self.input_manager.compact_paste_range()
277 && range.end > range.start
278 && self.input_manager.selection_range().is_none()
279 && self.input_manager.cursor() == range.end
280 {
281 let content = self.input_manager.content();
282 if content.is_char_boundary(range.start) && content.is_char_boundary(range.end) {
283 self.input_manager.replace_range(range.start, range.end, "");
284 self.refresh_input_edit_state();
285 return;
286 }
287 }
288 self.input_manager.backspace();
289 self.refresh_input_edit_state();
290 }
291
292 pub(crate) fn delete_char_forward(&mut self) {
294 self.input_manager.delete();
295 self.refresh_input_edit_state();
296 }
297
298 pub(crate) fn delete_word_backward(&mut self) {
300 if self.input_manager.delete_selection() {
301 self.refresh_input_edit_state();
302 return;
303 }
304 let cursor = self.input_manager.cursor();
305 if cursor == 0 {
306 return;
307 }
308
309 let delete_start = previous_word_boundary(self.input_manager.content(), cursor);
310
311 if delete_start < cursor {
312 self.input_manager.replace_range(delete_start, cursor, "");
313 self.input_manager.set_cursor(delete_start);
314 self.refresh_input_edit_state();
315 }
316 }
317
318 pub(crate) fn delete_word_forward(&mut self) {
319 self.input_manager.delete_word_forward();
320 self.refresh_input_edit_state();
321 }
322
323 pub(crate) fn delete_whitespace_around_cursor(&mut self) {
325 self.input_manager.delete_whitespace_around_cursor();
326 self.refresh_input_edit_state();
327 }
328
329 pub(crate) fn transpose_chars(&mut self) {
331 self.input_manager.transpose_chars();
332 self.refresh_input_edit_state();
333 }
334
335 pub(crate) fn transpose_words(&mut self) {
337 self.input_manager.transpose_words();
338 self.refresh_input_edit_state();
339 }
340
341 pub(crate) fn uppercase_word(&mut self) {
343 self.input_manager.uppercase_word();
344 self.refresh_input_edit_state();
345 }
346
347 pub(crate) fn lowercase_word(&mut self) {
349 self.input_manager.lowercase_word();
350 self.refresh_input_edit_state();
351 }
352
353 pub(crate) fn capitalize_word(&mut self) {
355 self.input_manager.capitalize_word();
356 self.refresh_input_edit_state();
357 }
358
359 pub(crate) fn delete_to_end_of_line(&mut self) {
361 if self.input_manager.delete_selection() {
362 self.refresh_input_edit_state();
363 return;
364 }
365 let content = self.input_manager.content();
366 let cursor = self.input_manager.cursor();
367
368 let rest = &content[cursor..];
369 let delete_len = if let Some(newline_pos) = rest.find('\n') {
370 newline_pos
371 } else {
372 rest.len()
373 };
374
375 if delete_len > 0 {
376 self.input_manager.replace_range(cursor, cursor + delete_len, "");
377 self.refresh_input_edit_state();
378 }
379 }
380
381 pub(crate) fn move_left(&mut self) {
383 self.input_manager.move_cursor_left();
384 }
385
386 pub(crate) fn move_right(&mut self) {
388 self.input_manager.move_cursor_right();
389 }
390
391 pub(crate) fn select_left(&mut self) {
392 let cursor = self.input_manager.cursor();
393 let content = self.input_manager.content();
394 let pos = prev_char_boundary(content, cursor);
395 self.input_manager.set_cursor_with_selection(pos);
396 }
397
398 pub(crate) fn select_right(&mut self) {
399 let cursor = self.input_manager.cursor();
400 let content = self.input_manager.content();
401 let pos = next_char_boundary(content, cursor);
402 self.input_manager.set_cursor_with_selection(pos);
403 }
404
405 pub(crate) fn move_left_word(&mut self) {
407 let cursor = previous_word_boundary(self.input_manager.content(), self.input_manager.cursor());
408 self.input_manager.set_cursor(cursor);
409 }
410 pub(crate) fn move_right_word(&mut self) {
412 let cursor = next_word_boundary(self.input_manager.content(), self.input_manager.cursor());
413 self.input_manager.set_cursor(cursor);
414 }
415 pub(crate) fn move_to_start(&mut self) {
417 self.input_manager.move_cursor_to_start();
418 }
419
420 pub(crate) fn move_to_end(&mut self) {
422 self.input_manager.move_cursor_to_end();
423 }
424
425 pub(crate) fn move_to_start_of_line(&mut self) {
430 self.input_manager.move_cursor_to_start_of_line();
431 }
432
433 pub(crate) fn move_to_end_of_line(&mut self) {
438 self.input_manager.move_cursor_to_end_of_line();
439 }
440
441 pub(crate) fn select_to_start(&mut self) {
442 self.input_manager.set_cursor_with_selection(0);
443 }
444
445 pub(crate) fn select_to_end(&mut self) {
446 self.input_manager.set_cursor_with_selection(self.input_manager.content().len());
447 }
448
449 pub(crate) fn select_to_start_of_line(&mut self) {
451 let (line_start, _) = self.input_manager.current_line_byte_range();
452 self.input_manager.set_cursor_with_selection(line_start);
453 }
454
455 pub(crate) fn select_to_end_of_line(&mut self) {
457 let (_, line_end) = self.input_manager.current_line_byte_range();
458 self.input_manager.set_cursor_with_selection(line_end);
459 }
460
461 pub(crate) fn clear_current_line_or_all(&mut self) {
473 if self.input_manager.delete_selection() {
474 self.refresh_input_edit_state();
475 return;
476 }
477 let content_len = self.input_manager.content().len();
478 if content_len == 0 {
479 return;
480 }
481 if self.input_manager.is_single_line() {
482 self.input_manager.clear();
483 self.refresh_input_edit_state();
484 return;
485 }
486
487 let cursor = self.input_manager.cursor().min(content_len);
488 let (line_start, line_end) = self.input_manager.current_line_byte_range();
489 let mut clear_start = line_start.min(content_len);
490 let mut clear_end = line_end.min(content_len);
491
492 if self.input_compact_mode
493 && let Some(range) = self.input_manager.compact_paste_range()
494 && range.start < range.end
495 {
496 let range_start = range.start.min(content_len);
498 let range_end = range.end.min(content_len);
499 if range_start < range_end {
500 let overlaps_line = range_start < clear_end && clear_start < range_end;
501 let cursor_inside = cursor >= range_start && cursor <= range_end;
502 if overlaps_line || cursor_inside {
503 clear_start = clear_start.min(range_start);
504 clear_end = clear_end.max(range_end);
505 }
506 }
507 }
508
509 let content = self.input_manager.content();
511 while clear_start > 0 && !content.is_char_boundary(clear_start) {
512 clear_start -= 1;
513 }
514 while clear_end < content.len() && !content.is_char_boundary(clear_end) {
515 clear_end += 1;
516 }
517 if clear_start >= clear_end {
518 return;
519 }
520 self.input_manager.replace_range(clear_start, clear_end, "");
521 self.input_manager.set_cursor(clear_start);
522 self.refresh_input_edit_state();
523 }
524
525 pub(crate) fn remember_submitted_input(&mut self, submitted: super::input_manager::InputHistoryEntry) {
527 self.input_manager.add_to_history(submitted);
528 }
529
530 pub(crate) fn navigate_history_previous(&mut self) -> bool {
532 if let Some(previous) = self.input_manager.go_to_previous_history() {
533 self.input_manager.apply_history_entry(previous);
534 self.input_compact_mode = self.input_compact_placeholder().is_some();
535 true
536 } else {
537 false
538 }
539 }
540
541 pub(crate) fn navigate_history_next(&mut self) -> bool {
543 if let Some(next) = self.input_manager.go_to_next_history() {
544 self.input_manager.apply_history_entry(next);
545 self.input_compact_mode = self.input_compact_placeholder().is_some();
546 true
547 } else {
548 false
549 }
550 }
551
552 pub(crate) fn move_cursor_up_for_history(&mut self) -> bool {
561 if !self.input_enabled {
562 return false;
563 }
564 self.clear_inline_prompt_suggestion();
565 self.input_manager.move_cursor_up()
566 }
567
568 pub(crate) fn move_cursor_down_for_history(&mut self) -> bool {
577 if !self.input_enabled {
578 return false;
579 }
580 self.clear_inline_prompt_suggestion();
581 self.input_manager.move_cursor_down()
582 }
583
584 pub(crate) fn move_up_within_composer(&mut self) -> bool {
591 if !self.input_enabled {
592 return false;
593 }
594 self.clear_inline_prompt_suggestion();
595 if self.input_visual_geometry().is_some() {
596 self.move_cursor_up_within_visual()
597 } else {
598 self.input_manager.move_cursor_up()
599 }
600 }
601
602 pub(crate) fn move_down_within_composer(&mut self) -> bool {
609 if !self.input_enabled {
610 return false;
611 }
612 self.clear_inline_prompt_suggestion();
613 if self.input_visual_geometry().is_some() {
614 self.move_cursor_down_within_visual()
615 } else {
616 self.input_manager.move_cursor_down()
617 }
618 }
619
620 pub fn history_position(&self) -> Option<(usize, usize)> {
623 self.input_manager.history_index().map(|idx| {
624 let total = self.input_manager.history().len();
625 (total - idx, total)
626 })
627 }
628}