gpui_editor/
editor.rs

1use crate::buffer::{GapBuffer, TextBuffer};
2use crate::syntax_highlighter::SyntaxHighlighter;
3use gpui::*;
4
5#[derive(Clone)]
6pub struct EditorConfig {
7    pub line_height: Pixels,
8    pub font_size: Pixels,
9    pub gutter_width: Pixels,
10    pub gutter_padding: Pixels,
11    pub text_color: Rgba,
12    pub line_number_color: Rgba,
13    pub gutter_bg_color: Rgba,
14    pub editor_bg_color: Rgba,
15    pub active_line_bg_color: Rgba,
16    pub font_family: SharedString,
17}
18
19impl Default for EditorConfig {
20    fn default() -> Self {
21        Self {
22            line_height: px(20.0),
23            font_size: px(14.0),
24            gutter_width: px(50.0),
25            gutter_padding: px(10.0),
26            text_color: rgb(0xcccccc),
27            line_number_color: rgb(0x666666),
28            gutter_bg_color: rgb(0x252525),
29            editor_bg_color: rgb(0x1e1e1e),
30            active_line_bg_color: rgb(0x2a2a2a),
31            font_family: "Monaco".into(),
32        }
33    }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct CursorPosition {
38    pub row: usize,
39    pub col: usize,
40}
41
42impl CursorPosition {
43    pub fn new(row: usize, col: usize) -> Self {
44        Self { row, col }
45    }
46}
47
48#[derive(Clone)]
49pub struct Editor {
50    id: ElementId,
51    buffer: GapBuffer,
52    config: EditorConfig,
53    cursor_position: CursorPosition,
54    goal_column: Option<usize>,
55    selection_anchor: Option<CursorPosition>,
56    syntax_highlighter: SyntaxHighlighter,
57    language: String,
58    current_theme: String,
59}
60
61impl Editor {
62    pub fn new(id: impl Into<ElementId>, lines: Vec<String>) -> Self {
63        let id = id.into();
64        let syntax_highlighter = SyntaxHighlighter::new();
65
66        // Auto-detect language from content
67        let full_text = lines.join("\n");
68        let language = syntax_highlighter
69            .detect_language(&full_text, Some("rs"))
70            .unwrap_or_else(|| "Rust".to_string());
71
72        Self {
73            id,
74            buffer: GapBuffer::from_lines(lines),
75            config: EditorConfig::default(),
76            cursor_position: CursorPosition { row: 0, col: 0 },
77            goal_column: None,
78            selection_anchor: None,
79            syntax_highlighter,
80            language,
81            current_theme: String::new(),
82        }
83    }
84
85    pub fn id(&self) -> &ElementId {
86        &self.id
87    }
88
89    pub fn config(&self) -> &EditorConfig {
90        &self.config
91    }
92
93    pub fn config_mut(&mut self) -> &mut EditorConfig {
94        &mut self.config
95    }
96
97    pub fn set_config(&mut self, config: EditorConfig) {
98        self.config = config;
99    }
100
101    pub fn cursor_position(&self) -> CursorPosition {
102        self.cursor_position
103    }
104
105    pub fn set_cursor_position(&mut self, position: CursorPosition) {
106        self.cursor_position = position;
107        // Reset goal column when cursor position is explicitly set
108        self.goal_column = None;
109    }
110
111    pub fn get_cursor_position(&self) -> CursorPosition {
112        self.cursor_position
113    }
114
115    pub fn clear_selection(&mut self) {
116        self.selection_anchor = None;
117        // Reset goal column when clearing selection
118        self.goal_column = None;
119    }
120
121    pub fn get_buffer(&self) -> &GapBuffer {
122        &self.buffer
123    }
124
125    pub fn get_buffer_mut(&mut self) -> &mut GapBuffer {
126        &mut self.buffer
127    }
128
129    pub fn language(&self) -> &str {
130        &self.language
131    }
132
133    pub fn set_language(&mut self, language: String) {
134        self.language = language;
135    }
136
137    pub fn current_theme(&self) -> &str {
138        &self.current_theme
139    }
140
141    pub fn set_theme(&mut self, theme: &str) {
142        self.current_theme = theme.to_string();
143        self.syntax_highlighter.set_theme(theme);
144        // Update colors from theme
145        self.config.editor_bg_color = self.syntax_highlighter.get_theme_background().into();
146        self.config.text_color = self.syntax_highlighter.get_theme_foreground().into();
147        self.config.gutter_bg_color = self.syntax_highlighter.get_theme_gutter_background().into();
148        self.config.active_line_bg_color =
149            self.syntax_highlighter.get_theme_line_highlight().into();
150    }
151
152    pub fn update_buffer(&mut self, lines: Vec<String>) {
153        self.buffer = GapBuffer::from_lines(lines);
154        // Reset highlighting state to force complete re-highlighting
155        self.syntax_highlighter.reset_state();
156    }
157
158    /// Update buffer content at a specific line (for future incremental updates)
159    pub fn update_line(&mut self, line_index: usize, new_content: String) {
160        // Delete the old line and insert the new content
161        if line_index < self.buffer.line_count() {
162            // Get the current line to find its length
163            let line_len = self.buffer.line_len(line_index);
164
165            // Delete the entire line content
166            let start_pos = self.buffer.cursor_to_position(line_index, 0);
167            let end_pos = self.buffer.cursor_to_position(line_index, line_len);
168            self.buffer.delete_range(start_pos, end_pos);
169
170            // Insert the new content
171            self.buffer.insert_at(line_index, 0, &new_content);
172
173            // Clear highlighting state from this line onward
174            self.syntax_highlighter
175                .clear_state_from_line(line_index, &self.language);
176        }
177    }
178
179    /// Get syntax highlighting for a line
180    pub fn highlight_line(
181        &mut self,
182        line: &str,
183        line_index: usize,
184        font_family: SharedString,
185        font_size: f32,
186    ) -> Vec<TextRun> {
187        self.syntax_highlighter.highlight_line(
188            line,
189            &self.language,
190            line_index,
191            font_family,
192            font_size,
193        )
194    }
195
196    // Movement methods
197    pub fn move_left(&mut self, shift_held: bool) {
198        if shift_held && self.selection_anchor.is_none() {
199            self.selection_anchor = Some(self.cursor_position);
200        } else if !shift_held {
201            self.selection_anchor = None;
202        }
203
204        // Reset goal column when moving horizontally
205        self.goal_column = None;
206
207        if self.cursor_position.col > 0 {
208            self.cursor_position.col -= 1;
209        } else if self.cursor_position.row > 0 {
210            self.cursor_position.row -= 1;
211            self.cursor_position.col = self.buffer.line_len(self.cursor_position.row);
212        }
213    }
214
215    pub fn move_right(&mut self, shift_held: bool) {
216        if shift_held && self.selection_anchor.is_none() {
217            self.selection_anchor = Some(self.cursor_position);
218        } else if !shift_held {
219            self.selection_anchor = None;
220        }
221
222        // Reset goal column when moving horizontally
223        self.goal_column = None;
224
225        let current_line_len = self.buffer.line_len(self.cursor_position.row);
226
227        if self.cursor_position.col < current_line_len {
228            self.cursor_position.col += 1;
229        } else if self.cursor_position.row < self.buffer.line_count().saturating_sub(1) {
230            // Move to start of next line
231            self.cursor_position.row += 1;
232            self.cursor_position.col = 0;
233        }
234    }
235
236    pub fn move_up(&mut self, shift_held: bool) {
237        if shift_held && self.selection_anchor.is_none() {
238            self.selection_anchor = Some(self.cursor_position);
239        } else if !shift_held {
240            self.selection_anchor = None;
241        }
242
243        if self.cursor_position.row > 0 {
244            // Set goal column if not already set
245            if self.goal_column.is_none() {
246                self.goal_column = Some(self.cursor_position.col);
247            }
248
249            self.cursor_position.row -= 1;
250
251            // Try to use goal column, but clamp to line length
252            let line_len = self.buffer.line_len(self.cursor_position.row);
253            self.cursor_position.col = self
254                .goal_column
255                .unwrap_or(self.cursor_position.col)
256                .min(line_len);
257        }
258    }
259
260    pub fn move_down(&mut self, shift_held: bool) {
261        if shift_held && self.selection_anchor.is_none() {
262            self.selection_anchor = Some(self.cursor_position);
263        } else if !shift_held {
264            self.selection_anchor = None;
265        }
266
267        if self.cursor_position.row < self.buffer.line_count().saturating_sub(1) {
268            // Set goal column if not already set
269            if self.goal_column.is_none() {
270                self.goal_column = Some(self.cursor_position.col);
271            }
272
273            self.cursor_position.row += 1;
274
275            // Try to use goal column, but clamp to line length
276            let line_len = self.buffer.line_len(self.cursor_position.row);
277            self.cursor_position.col = self
278                .goal_column
279                .unwrap_or(self.cursor_position.col)
280                .min(line_len);
281        }
282    }
283
284    pub fn select_all(&mut self) {
285        // Reset goal column when selecting all
286        self.goal_column = None;
287
288        // Set anchor at beginning
289        self.selection_anchor = Some(CursorPosition { row: 0, col: 0 });
290
291        // Move cursor to end
292        let last_row = self.buffer.line_count().saturating_sub(1);
293        let last_col = self.buffer.line_len(last_row);
294        self.cursor_position = CursorPosition {
295            row: last_row,
296            col: last_col,
297        };
298    }
299
300    pub fn has_selection(&self) -> bool {
301        self.selection_anchor.is_some()
302    }
303
304    pub fn get_selection_range(&self) -> Option<(CursorPosition, CursorPosition)> {
305        self.selection_anchor.map(|anchor| {
306            // Return (start, end) positions in document order
307            if anchor.row < self.cursor_position.row
308                || (anchor.row == self.cursor_position.row && anchor.col < self.cursor_position.col)
309            {
310                (anchor, self.cursor_position)
311            } else {
312                (self.cursor_position, anchor)
313            }
314        })
315    }
316
317    pub fn delete_selection(&mut self) -> bool {
318        if let Some((start, end)) = self.get_selection_range() {
319            // Convert cursor positions to buffer positions
320            let start_pos = self.buffer.cursor_to_position(start.row, start.col);
321            let end_pos = self.buffer.cursor_to_position(end.row, end.col);
322
323            // Delete the range
324            self.buffer.delete_range(start_pos, end_pos);
325
326            // Update cursor position
327            self.cursor_position = start;
328            self.selection_anchor = None;
329            self.goal_column = None;
330
331            // Reset highlighting state from the changed line onward
332            self.syntax_highlighter
333                .clear_state_from_line(start.row, &self.language);
334
335            true
336        } else {
337            false
338        }
339    }
340
341    pub fn get_selected_text(&self) -> String {
342        if let Some((start, end)) = self.get_selection_range() {
343            // Convert cursor positions to buffer positions
344            let start_pos = self.buffer.cursor_to_position(start.row, start.col);
345            let end_pos = self.buffer.cursor_to_position(end.row, end.col);
346
347            // Get the full text and extract the selection
348            let text = self.buffer.to_string();
349
350            // The positions are character indices, so we can slice the chars directly
351            let chars: Vec<char> = text.chars().collect();
352            if start_pos <= chars.len() && end_pos <= chars.len() && start_pos <= end_pos {
353                chars[start_pos..end_pos].iter().collect()
354            } else {
355                String::new()
356            }
357        } else {
358            String::new()
359        }
360    }
361
362    pub fn insert_char(&mut self, ch: char) {
363        // Delete selection first if there is one
364        self.delete_selection();
365
366        // Use the buffer's insert_at method directly
367        self.buffer.insert_at(
368            self.cursor_position.row,
369            self.cursor_position.col,
370            &ch.to_string(),
371        );
372        self.cursor_position.col += 1;
373        self.goal_column = None;
374
375        // Clear highlighting state from this line onward
376        self.syntax_highlighter
377            .clear_state_from_line(self.cursor_position.row, &self.language);
378    }
379
380    pub fn insert_newline(&mut self) {
381        // Delete selection first if there is one
382        self.delete_selection();
383
384        // Use the buffer's insert_at method directly
385        self.buffer
386            .insert_at(self.cursor_position.row, self.cursor_position.col, "\n");
387        self.cursor_position.row += 1;
388        self.cursor_position.col = 0;
389        self.goal_column = None;
390
391        // Clear highlighting state from this line onward
392        self.syntax_highlighter
393            .clear_state_from_line(self.cursor_position.row - 1, &self.language);
394    }
395
396    pub fn backspace(&mut self) {
397        // If there's a selection, delete it instead
398        if self.selection_anchor.is_some() {
399            self.delete_selection();
400            return;
401        }
402
403        // Use the buffer's backspace_at method directly
404        self.buffer
405            .backspace_at(self.cursor_position.row, self.cursor_position.col);
406
407        if self.cursor_position.col > 0 {
408            self.cursor_position.col -= 1;
409            // Clear highlighting state from this line onward
410            self.syntax_highlighter
411                .clear_state_from_line(self.cursor_position.row, &self.language);
412        } else if self.cursor_position.row > 0 {
413            // Move to end of previous line
414            self.cursor_position.row -= 1;
415            let line_len = self.buffer.line_len(self.cursor_position.row);
416            self.cursor_position.col = line_len;
417            // Clear highlighting state from the previous line onward
418            self.syntax_highlighter
419                .clear_state_from_line(self.cursor_position.row, &self.language);
420        }
421
422        self.goal_column = None;
423    }
424
425    pub fn delete(&mut self) {
426        // If there's a selection, delete it instead
427        if self.selection_anchor.is_some() {
428            self.delete_selection();
429            return;
430        }
431
432        // Use the buffer's delete_at method directly
433        self.buffer
434            .delete_at(self.cursor_position.row, self.cursor_position.col);
435
436        // Clear highlighting state from this line onward
437        self.syntax_highlighter
438            .clear_state_from_line(self.cursor_position.row, &self.language);
439
440        self.goal_column = None;
441    }
442}