Skip to main content

vtcode_tui/core_tui/session/
editing.rs

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