Skip to main content

vtcode_tui/core_tui/session/
editing.rs

1use super::{InlinePromptSuggestionSource, Session};
2use crate::config::constants::ui;
3/// Text editing and cursor movement operations for Session
4///
5/// This module handles all text manipulation and cursor navigation including:
6/// - Character insertion and deletion
7/// - Word and sentence-level editing
8/// - Cursor movement (character, word, line boundaries)
9/// - Input history navigation
10/// - Newline handling with capacity limits
11use unicode_segmentation::UnicodeSegmentation;
12
13impl Session {
14    pub(crate) fn refresh_input_edit_state(&mut self) {
15        self.clear_suggested_prompt_state();
16        self.clear_inline_prompt_suggestion();
17        self.input_compact_mode = self.input_compact_placeholder().is_some();
18    }
19
20    pub(crate) fn set_inline_prompt_suggestion(&mut self, suggestion: String, llm_generated: bool) {
21        let trimmed = suggestion.trim();
22        if trimmed.is_empty() {
23            self.clear_inline_prompt_suggestion();
24            return;
25        }
26
27        self.inline_prompt_suggestion.suggestion = Some(trimmed.to_string());
28        self.inline_prompt_suggestion.source = Some(if llm_generated {
29            InlinePromptSuggestionSource::Llm
30        } else {
31            InlinePromptSuggestionSource::Local
32        });
33        self.mark_dirty();
34    }
35
36    pub(crate) fn accept_inline_prompt_suggestion(&mut self) -> bool {
37        let Some(suffix) = self.visible_inline_prompt_suggestion_suffix() else {
38            return false;
39        };
40
41        self.input_manager.insert_text(&suffix);
42        self.clear_inline_prompt_suggestion();
43        self.mark_dirty();
44        true
45    }
46
47    /// Insert a character at the current cursor position
48    pub(crate) fn insert_char(&mut self, ch: char) {
49        if ch == '\u{7f}' {
50            return;
51        }
52        if ch == '\n' && !self.can_insert_newline() {
53            return;
54        }
55        self.input_manager.insert_char(ch);
56        self.refresh_input_edit_state();
57    }
58
59    /// Insert pasted text without enforcing the inline newline cap.
60    ///
61    /// This preserves the full block (including large multi-line pastes) so the
62    /// agent receives the exact content instead of dropping line breaks after
63    /// hitting the interactive input's visual limit.
64    pub fn insert_paste_text(&mut self, text: &str) {
65        let sanitized: String = text
66            .chars()
67            .filter(|&ch| ch != '\r' && ch != '\u{7f}')
68            .collect();
69
70        if sanitized.is_empty() {
71            return;
72        }
73
74        self.input_manager.insert_text(&sanitized);
75        self.refresh_input_edit_state();
76    }
77
78    pub(crate) fn apply_suggested_prompt(&mut self, text: String) {
79        let trimmed = text.trim();
80        if trimmed.is_empty() {
81            return;
82        }
83
84        let merged = if self.input_manager.content().trim().is_empty() {
85            trimmed.to_string()
86        } else {
87            format!("{}\n\n{}", self.input_manager.content().trim_end(), trimmed)
88        };
89
90        self.input_manager.set_content(merged);
91        self.input_manager
92            .set_cursor(self.input_manager.content().len());
93        self.suggested_prompt_state.active = true;
94        self.input_compact_mode = self.input_compact_placeholder().is_some();
95        self.mark_dirty();
96    }
97
98    /// Calculate remaining newline capacity in the input field
99    pub(crate) fn remaining_newline_capacity(&self) -> usize {
100        ui::INLINE_INPUT_MAX_LINES
101            .saturating_sub(1)
102            .saturating_sub(self.input_manager.content().matches('\n').count())
103    }
104
105    /// Check if a newline can be inserted
106    pub(crate) fn can_insert_newline(&self) -> bool {
107        self.remaining_newline_capacity() > 0
108    }
109
110    /// Delete the character before the cursor (backspace)
111    pub(crate) fn delete_char(&mut self) {
112        self.input_manager.backspace();
113        self.refresh_input_edit_state();
114    }
115
116    /// Delete the character at the cursor (forward delete)
117    pub(crate) fn delete_char_forward(&mut self) {
118        self.input_manager.delete();
119        self.refresh_input_edit_state();
120    }
121
122    /// Delete the word before the cursor
123    pub(crate) fn delete_word_backward(&mut self) {
124        if self.input_manager.delete_selection() {
125            self.refresh_input_edit_state();
126            return;
127        }
128        if self.input_manager.cursor() == 0 {
129            return;
130        }
131
132        // Find the start of the current word by moving backward
133        let graphemes: Vec<(usize, &str)> = self
134            .input_manager
135            .content()
136            .grapheme_indices(true)
137            .take_while(|(idx, _)| *idx < self.input_manager.cursor())
138            .collect();
139
140        if graphemes.is_empty() {
141            return;
142        }
143
144        let mut index = graphemes.len();
145
146        // Skip any trailing whitespace
147        while index > 0 {
148            let (_, grapheme) = graphemes[index - 1];
149            if !grapheme.chars().all(char::is_whitespace) {
150                break;
151            }
152            index -= 1;
153        }
154
155        // Move backwards until we find whitespace (start of the word)
156        while index > 0 {
157            let (_, grapheme) = graphemes[index - 1];
158            if grapheme.chars().all(char::is_whitespace) {
159                break;
160            }
161            index -= 1;
162        }
163
164        // Calculate the position to delete from
165        let delete_start = if index < graphemes.len() {
166            graphemes[index].0
167        } else {
168            self.input_manager.cursor()
169        };
170
171        // Delete from delete_start to cursor
172        if delete_start < self.input_manager.cursor() {
173            let before = &self.input_manager.content()[..delete_start];
174            let after = &self.input_manager.content()[self.input_manager.cursor()..];
175            let new_content = format!("{}{}", before, after);
176
177            self.input_manager.set_content(new_content);
178            self.input_manager.set_cursor(delete_start);
179            self.refresh_input_edit_state();
180        }
181    }
182
183    #[allow(dead_code)]
184    pub(crate) fn delete_word_forward(&mut self) {
185        self.input_manager.delete_word_forward();
186        self.refresh_input_edit_state();
187    }
188    /// Delete from cursor to start of current line (Command+Backspace on macOS)
189    pub(crate) fn delete_to_start_of_line(&mut self) {
190        if self.input_manager.delete_selection() {
191            self.refresh_input_edit_state();
192            return;
193        }
194        let content = self.input_manager.content();
195        let cursor = self.input_manager.cursor();
196
197        // Find the previous newline or start of string
198        let before = &content[..cursor];
199        let delete_start = if let Some(newline_pos) = before.rfind('\n') {
200            newline_pos + 1 // Delete after the newline
201        } else {
202            0 // Delete from start
203        };
204
205        if delete_start < cursor {
206            let new_content = format!("{}{}", &content[..delete_start], &content[cursor..]);
207            self.input_manager.set_content(new_content);
208            self.input_manager.set_cursor(delete_start);
209            self.refresh_input_edit_state();
210        }
211    }
212
213    /// Delete from cursor to end of current line (Command+Delete on macOS)
214    pub(crate) fn delete_to_end_of_line(&mut self) {
215        if self.input_manager.delete_selection() {
216            self.refresh_input_edit_state();
217            return;
218        }
219        let content = self.input_manager.content();
220        let cursor = self.input_manager.cursor();
221
222        // Find the next newline or end of string
223        let rest = &content[cursor..];
224        let delete_len = if let Some(newline_pos) = rest.find('\n') {
225            newline_pos
226        } else {
227            rest.len()
228        };
229
230        if delete_len > 0 {
231            let new_content = format!("{}{}", &content[..cursor], &content[cursor + delete_len..]);
232            self.input_manager.set_content(new_content);
233            self.refresh_input_edit_state();
234        }
235    }
236
237    /// Move cursor left by one character
238    pub(crate) fn move_left(&mut self) {
239        self.input_manager.move_cursor_left();
240    }
241
242    /// Move cursor right by one character
243    pub(crate) fn move_right(&mut self) {
244        self.input_manager.move_cursor_right();
245    }
246
247    pub(crate) fn select_left(&mut self) {
248        let cursor = self.input_manager.cursor();
249        if cursor == 0 {
250            self.input_manager.set_cursor_with_selection(0);
251            return;
252        }
253
254        let mut pos = cursor - 1;
255        let content = self.input_manager.content();
256        while pos > 0 && !content.is_char_boundary(pos) {
257            pos -= 1;
258        }
259        self.input_manager.set_cursor_with_selection(pos);
260    }
261
262    pub(crate) fn select_right(&mut self) {
263        let cursor = self.input_manager.cursor();
264        let content = self.input_manager.content();
265        if cursor >= content.len() {
266            self.input_manager.set_cursor_with_selection(content.len());
267            return;
268        }
269
270        let mut pos = cursor + 1;
271        while pos < content.len() && !content.is_char_boundary(pos) {
272            pos += 1;
273        }
274        self.input_manager.set_cursor_with_selection(pos);
275    }
276
277    /// Move cursor left to the start of the previous word
278    pub(crate) fn move_left_word(&mut self) {
279        if self.input_manager.cursor() == 0 {
280            return;
281        }
282
283        let graphemes: Vec<(usize, &str)> = self
284            .input_manager
285            .content()
286            .grapheme_indices(true)
287            .take_while(|(idx, _)| *idx < self.input_manager.cursor())
288            .collect();
289
290        if graphemes.is_empty() {
291            return;
292        }
293
294        let mut index = graphemes.len();
295
296        // Skip trailing whitespace
297        while index > 0 {
298            let (_, grapheme) = graphemes[index - 1];
299            if !grapheme.chars().all(char::is_whitespace) {
300                break;
301            }
302            index -= 1;
303        }
304
305        // Move to start of word
306        while index > 0 {
307            let (_, grapheme) = graphemes[index - 1];
308            if grapheme.chars().all(char::is_whitespace) {
309                break;
310            }
311            index -= 1;
312        }
313
314        if index < graphemes.len() {
315            self.input_manager.set_cursor(graphemes[index].0);
316        } else {
317            self.input_manager.set_cursor(0);
318        }
319    }
320    /// Move cursor right to the start of the next word
321    pub(crate) fn move_right_word(&mut self) {
322        if self.input_manager.cursor() >= self.input_manager.content().len() {
323            return;
324        }
325
326        let graphemes: Vec<(usize, &str)> = self
327            .input_manager
328            .content()
329            .grapheme_indices(true)
330            .skip_while(|(idx, _)| *idx < self.input_manager.cursor())
331            .collect();
332
333        if graphemes.is_empty() {
334            self.input_manager.move_cursor_to_end();
335            return;
336        }
337
338        let mut index = 0;
339        let mut skipped_whitespace = false;
340
341        // Skip current whitespace
342        while index < graphemes.len() {
343            let (_, grapheme) = graphemes[index];
344            if !grapheme.chars().all(char::is_whitespace) {
345                break;
346            }
347            skipped_whitespace = true;
348            index += 1;
349        }
350
351        if index >= graphemes.len() {
352            self.input_manager.move_cursor_to_end();
353            return;
354        }
355
356        // If we skipped whitespace, we're at the start of a word
357        if skipped_whitespace {
358            self.input_manager.set_cursor(graphemes[index].0);
359            return;
360        }
361
362        // Otherwise, skip to end of current word
363        while index < graphemes.len() {
364            let (_, grapheme) = graphemes[index];
365            if grapheme.chars().all(char::is_whitespace) {
366                break;
367            }
368            index += 1;
369        }
370
371        if index < graphemes.len() {
372            self.input_manager.set_cursor(graphemes[index].0);
373        } else {
374            self.input_manager.move_cursor_to_end();
375        }
376    }
377    /// Move cursor to the start of the line
378    pub(crate) fn move_to_start(&mut self) {
379        self.input_manager.move_cursor_to_start();
380    }
381
382    /// Move cursor to the end of the line
383    pub(crate) fn move_to_end(&mut self) {
384        self.input_manager.move_cursor_to_end();
385    }
386
387    pub(crate) fn select_to_start(&mut self) {
388        self.input_manager.set_cursor_with_selection(0);
389    }
390
391    pub(crate) fn select_to_end(&mut self) {
392        self.input_manager
393            .set_cursor_with_selection(self.input_manager.content().len());
394    }
395
396    /// Remember submitted input in history
397    pub(crate) fn remember_submitted_input(
398        &mut self,
399        submitted: super::input_manager::InputHistoryEntry,
400    ) {
401        self.input_manager.add_to_history(submitted);
402    }
403
404    /// Navigate to previous history entry (disabled to prevent cursor flickering)
405    #[allow(dead_code)]
406    pub(crate) fn navigate_history_previous(&mut self) -> bool {
407        if let Some(previous) = self.input_manager.go_to_previous_history() {
408            self.input_manager.apply_history_entry(previous);
409            true
410        } else {
411            false
412        }
413    }
414
415    /// Navigate to next history entry (disabled to prevent cursor flickering)
416    #[allow(dead_code)]
417    pub(crate) fn navigate_history_next(&mut self) -> bool {
418        if let Some(next) = self.input_manager.go_to_next_history() {
419            self.input_manager.apply_history_entry(next);
420            true
421        } else {
422            false
423        }
424    }
425
426    /// Returns the current history position for status bar display
427    /// Returns (current_index, total_entries) or None if not navigating history
428    pub fn history_position(&self) -> Option<(usize, usize)> {
429        self.input_manager.history_index().map(|idx| {
430            let total = self.input_manager.history().len();
431            (total - idx, total)
432        })
433    }
434}