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