Skip to main content

vtcode_tui/core_tui/session/
editing.rs

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