editor_demo/
editor_demo.rs

1//! Example demonstrating a complete text editor with syntax highlighting
2//!
3//! Run with: cargo run --example editor_demo
4
5use gpui::*;
6use gpui_editor::*;
7use gpuikit_keymap::KeymapCollection;
8use std::path::Path;
9
10actions!(
11    editor,
12    [
13        MoveUp,
14        MoveDown,
15        MoveLeft,
16        MoveRight,
17        MoveUpWithShift,
18        MoveDownWithShift,
19        MoveLeftWithShift,
20        MoveRightWithShift,
21        Backspace,
22        Delete,
23        InsertNewline,
24        NextTheme,
25        PreviousTheme,
26        NextLanguage,
27        PreviousLanguage,
28        SelectAll,
29        Escape,
30        Copy,
31        Cut,
32        Paste
33    ]
34);
35
36/// A complete editor view with keyboard handling and state management
37struct EditorView {
38    focus_handle: FocusHandle,
39    editor: Editor,
40    current_theme_index: usize,
41    available_themes: Vec<String>,
42    current_language_index: usize,
43    available_languages: Vec<(String, String, String)>, // (name, extension, sample_code)
44}
45
46impl EditorView {
47    fn new(cx: &mut Context<Self>) -> Self {
48        let focus_handle = cx.focus_handle();
49
50        let initial_code = vec![
51            "// Rust sample code".to_string(),
52            "use std::collections::HashMap;".to_string(),
53            "".to_string(),
54            "fn main() {".to_string(),
55            "    let mut count = 0;".to_string(),
56            "    ".to_string(),
57            "    // Count from 1 to 10".to_string(),
58            "    for i in 1..=10 {".to_string(),
59            "        count += i;".to_string(),
60            "    }".to_string(),
61            "    ".to_string(),
62            "    // HashMap example".to_string(),
63            "    let mut scores = HashMap::new();".to_string(),
64            "    scores.insert(\"Blue\", 10);".to_string(),
65            "    scores.insert(\"Yellow\", 50);".to_string(),
66            "    ".to_string(),
67            "    println!(\"Final count: {}\", count);".to_string(),
68            "}".to_string(),
69        ];
70
71        let mut editor = Editor::new("editor", initial_code);
72
73        let highlighter = SyntaxHighlighter::new();
74        let available_themes = highlighter.available_themes();
75
76        let default_theme_index = available_themes
77            .iter()
78            .position(|t| t == "base16-ocean.dark")
79            .unwrap_or(0);
80
81        editor.set_theme(&available_themes[default_theme_index]);
82
83        let available_languages = vec![
84            ("Rust".to_string(), "rs".to_string(), get_rust_sample()),
85            (
86                "Plain Text".to_string(),
87                "txt".to_string(),
88                get_plain_text_sample(),
89            ),
90        ];
91
92        editor.set_language("Rust".to_string());
93
94        Self {
95            focus_handle,
96            editor,
97            current_theme_index: default_theme_index,
98            available_themes,
99            current_language_index: 0,
100            available_languages,
101        }
102    }
103
104    fn get_selected_text(&self) -> String {
105        self.editor.get_selected_text()
106    }
107
108    // Action handlers
109    fn move_up(&mut self, _: &MoveUp, _window: &mut Window, cx: &mut Context<Self>) {
110        self.editor.move_up(false);
111        cx.notify();
112    }
113
114    fn move_down(&mut self, _: &MoveDown, _window: &mut Window, cx: &mut Context<Self>) {
115        self.editor.move_down(false);
116        cx.notify();
117    }
118
119    fn move_left(&mut self, _: &MoveLeft, _window: &mut Window, cx: &mut Context<Self>) {
120        self.editor.move_left(false);
121        cx.notify();
122    }
123
124    fn move_right(&mut self, _: &MoveRight, _window: &mut Window, cx: &mut Context<Self>) {
125        self.editor.move_right(false);
126        cx.notify();
127    }
128
129    fn move_up_with_shift(
130        &mut self,
131        _: &MoveUpWithShift,
132        _window: &mut Window,
133        cx: &mut Context<Self>,
134    ) {
135        self.editor.move_up(true);
136        cx.notify();
137    }
138
139    fn move_down_with_shift(
140        &mut self,
141        _: &MoveDownWithShift,
142        _window: &mut Window,
143        cx: &mut Context<Self>,
144    ) {
145        self.editor.move_down(true);
146        cx.notify();
147    }
148
149    fn move_left_with_shift(
150        &mut self,
151        _: &MoveLeftWithShift,
152        _window: &mut Window,
153        cx: &mut Context<Self>,
154    ) {
155        self.editor.move_left(true);
156        cx.notify();
157    }
158
159    fn move_right_with_shift(
160        &mut self,
161        _: &MoveRightWithShift,
162        _window: &mut Window,
163        cx: &mut Context<Self>,
164    ) {
165        self.editor.move_right(true);
166        cx.notify();
167    }
168
169    fn backspace(&mut self, _: &Backspace, _window: &mut Window, cx: &mut Context<Self>) {
170        self.editor.backspace();
171        cx.notify();
172    }
173
174    fn delete(&mut self, _: &Delete, _window: &mut Window, cx: &mut Context<Self>) {
175        self.editor.delete();
176        cx.notify();
177    }
178
179    fn insert_newline(&mut self, _: &InsertNewline, _window: &mut Window, cx: &mut Context<Self>) {
180        self.editor.insert_newline();
181        cx.notify();
182    }
183
184    fn select_all(&mut self, _: &SelectAll, _window: &mut Window, cx: &mut Context<Self>) {
185        self.editor.select_all();
186        cx.notify();
187    }
188
189    fn escape(&mut self, _: &Escape, _window: &mut Window, cx: &mut Context<Self>) {
190        self.editor.clear_selection();
191        cx.notify();
192    }
193
194    fn copy(&mut self, _: &Copy, _window: &mut Window, cx: &mut Context<Self>) {
195        let selected_text = self.get_selected_text();
196        if !selected_text.is_empty() {
197            cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
198        }
199    }
200
201    fn cut(&mut self, _: &Cut, _window: &mut Window, cx: &mut Context<Self>) {
202        let selected_text = self.get_selected_text();
203        if !selected_text.is_empty() {
204            cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
205            self.editor.delete_selection();
206            cx.notify();
207        }
208    }
209
210    fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
211        if let Some(clipboard) = cx.read_from_clipboard() {
212            if let Some(text) = clipboard.text() {
213                // Delete selection if exists
214                self.editor.delete_selection();
215
216                // Insert text character by character (simplified)
217                for ch in text.chars() {
218                    if ch == '\n' {
219                        self.editor.insert_newline();
220                    } else if ch != '\r' {
221                        self.editor.insert_char(ch);
222                    }
223                }
224                cx.notify();
225            }
226        }
227    }
228
229    fn next_theme(&mut self, _: &NextTheme, _window: &mut Window, cx: &mut Context<Self>) {
230        self.current_theme_index = (self.current_theme_index + 1) % self.available_themes.len();
231        self.editor
232            .set_theme(&self.available_themes[self.current_theme_index]);
233        cx.notify();
234    }
235
236    fn previous_theme(&mut self, _: &PreviousTheme, _window: &mut Window, cx: &mut Context<Self>) {
237        self.current_theme_index = if self.current_theme_index == 0 {
238            self.available_themes.len() - 1
239        } else {
240            self.current_theme_index - 1
241        };
242        self.editor
243            .set_theme(&self.available_themes[self.current_theme_index]);
244        cx.notify();
245    }
246
247    fn next_language(&mut self, _: &NextLanguage, _window: &mut Window, cx: &mut Context<Self>) {
248        self.current_language_index =
249            (self.current_language_index + 1) % self.available_languages.len();
250        let (language, _, sample_code) = &self.available_languages[self.current_language_index];
251        self.editor.set_language(language.clone());
252        self.editor
253            .update_buffer(sample_code.lines().map(|s| s.to_string()).collect());
254        cx.notify();
255    }
256
257    fn previous_language(
258        &mut self,
259        _: &PreviousLanguage,
260        _window: &mut Window,
261        cx: &mut Context<Self>,
262    ) {
263        self.current_language_index = if self.current_language_index == 0 {
264            self.available_languages.len() - 1
265        } else {
266            self.current_language_index - 1
267        };
268        let (language, _, sample_code) = &self.available_languages[self.current_language_index];
269        self.editor.set_language(language.clone());
270        self.editor
271            .update_buffer(sample_code.lines().map(|s| s.to_string()).collect());
272        cx.notify();
273    }
274}
275
276impl Render for EditorView {
277    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
278        let _current_theme = &self.available_themes[self.current_theme_index];
279        let (current_language, _, _) = &self.available_languages[self.current_language_index];
280
281        let language = match current_language.as_str() {
282            "Rust" => Language::Rust,
283            _ => Language::PlainText,
284        };
285
286        let cursor_position = self.editor.cursor_position();
287        let cursor_point = Point::new(cursor_position.col, cursor_position.row);
288
289        let selection = if self.editor.has_selection() {
290            let selected_text = self.get_selected_text();
291            Some(Selection {
292                lines: selected_text.matches('\n').count(),
293                chars: selected_text.len(),
294            })
295        } else {
296            None
297        };
298
299        div()
300            .key_context("editor")
301            .size_full()
302            .flex()
303            .flex_col()
304            .child(
305                div()
306                    .flex_grow()
307                    .track_focus(&self.focus_handle)
308                    .on_action(cx.listener(Self::move_up))
309                    .on_action(cx.listener(Self::move_down))
310                    .on_action(cx.listener(Self::move_left))
311                    .on_action(cx.listener(Self::move_right))
312                    .on_action(cx.listener(Self::move_up_with_shift))
313                    .on_action(cx.listener(Self::move_down_with_shift))
314                    .on_action(cx.listener(Self::move_left_with_shift))
315                    .on_action(cx.listener(Self::move_right_with_shift))
316                    .on_action(cx.listener(Self::backspace))
317                    .on_action(cx.listener(Self::delete))
318                    .on_action(cx.listener(Self::insert_newline))
319                    .on_action(cx.listener(Self::select_all))
320                    .on_action(cx.listener(Self::escape))
321                    .on_action(cx.listener(Self::copy))
322                    .on_action(cx.listener(Self::cut))
323                    .on_action(cx.listener(Self::paste))
324                    .on_action(cx.listener(Self::next_theme))
325                    .on_action(cx.listener(Self::previous_theme))
326                    .on_action(cx.listener(Self::next_language))
327                    .on_action(cx.listener(Self::previous_language))
328                    .on_key_down(cx.listener(
329                        |this: &mut Self, event: &KeyDownEvent, _window, cx| {
330                            // Handle character input
331                            if let Some(text) = &event.keystroke.key_char {
332                                if !event.keystroke.modifiers.platform
333                                    && !event.keystroke.modifiers.control
334                                    && !event.keystroke.modifiers.function
335                                {
336                                    for ch in text.chars() {
337                                        this.editor.insert_char(ch);
338                                    }
339                                    cx.notify();
340                                }
341                            }
342                        },
343                    ))
344                    .child(EditorElement::new(self.editor.clone())),
345            )
346            .child(MetaLine::new(cursor_point, language, selection))
347    }
348}
349
350fn load_keymaps(cx: &mut App) {
351    // Load keymaps from JSON configuration
352    let mut keymap_collection = KeymapCollection::new();
353
354    let keymap_path = Path::new("examples/demo-keymap.json");
355    let loaded_from_file = if keymap_path.exists() {
356        match keymap_collection.load_file(keymap_path) {
357            Ok(_) => {
358                println!("Loaded keymaps from file: {}", keymap_path.display());
359                true
360            }
361            Err(e) => {
362                eprintln!("Failed to load keymap file: {}", e);
363                false
364            }
365        }
366    } else {
367        false
368    };
369
370    if !loaded_from_file {
371        let demo_keymap = include_str!("demo-keymap.json");
372        keymap_collection
373            .load_json(demo_keymap)
374            .expect("Failed to load embedded demo keymaps");
375        println!("Loaded embedded demo keymaps");
376    }
377
378    let specs = keymap_collection.get_binding_specs();
379
380    let mut bindings = Vec::new();
381
382    for spec in specs {
383        if !spec.action_name.starts_with("editor::") {
384            continue;
385        }
386
387        let action_name = spec
388            .action_name
389            .strip_prefix("editor::")
390            .unwrap_or(&spec.action_name);
391        let context = spec.context.as_deref();
392
393        match action_name {
394            "MoveUp" => bindings.push(KeyBinding::new(&spec.keystrokes, MoveUp, context)),
395            "MoveDown" => bindings.push(KeyBinding::new(&spec.keystrokes, MoveDown, context)),
396            "MoveLeft" => bindings.push(KeyBinding::new(&spec.keystrokes, MoveLeft, context)),
397            "MoveRight" => bindings.push(KeyBinding::new(&spec.keystrokes, MoveRight, context)),
398            "MoveUpWithShift" => {
399                bindings.push(KeyBinding::new(&spec.keystrokes, MoveUpWithShift, context))
400            }
401            "MoveDownWithShift" => bindings.push(KeyBinding::new(
402                &spec.keystrokes,
403                MoveDownWithShift,
404                context,
405            )),
406            "MoveLeftWithShift" => bindings.push(KeyBinding::new(
407                &spec.keystrokes,
408                MoveLeftWithShift,
409                context,
410            )),
411            "MoveRightWithShift" => bindings.push(KeyBinding::new(
412                &spec.keystrokes,
413                MoveRightWithShift,
414                context,
415            )),
416            "Backspace" => bindings.push(KeyBinding::new(&spec.keystrokes, Backspace, context)),
417            "Delete" => bindings.push(KeyBinding::new(&spec.keystrokes, Delete, context)),
418            "InsertNewline" => {
419                bindings.push(KeyBinding::new(&spec.keystrokes, InsertNewline, context))
420            }
421            "SelectAll" => bindings.push(KeyBinding::new(&spec.keystrokes, SelectAll, context)),
422            "Escape" => bindings.push(KeyBinding::new(&spec.keystrokes, Escape, context)),
423            "Copy" => bindings.push(KeyBinding::new(&spec.keystrokes, Copy, context)),
424            "Cut" => bindings.push(KeyBinding::new(&spec.keystrokes, Cut, context)),
425            "Paste" => bindings.push(KeyBinding::new(&spec.keystrokes, Paste, context)),
426            "NextTheme" => bindings.push(KeyBinding::new(&spec.keystrokes, NextTheme, context)),
427            "PreviousTheme" => {
428                bindings.push(KeyBinding::new(&spec.keystrokes, PreviousTheme, context))
429            }
430            "NextLanguage" => {
431                bindings.push(KeyBinding::new(&spec.keystrokes, NextLanguage, context))
432            }
433            "PreviousLanguage" => {
434                bindings.push(KeyBinding::new(&spec.keystrokes, PreviousLanguage, context))
435            }
436            unknown => {
437                eprintln!("Unknown editor action: {}", unknown);
438            }
439        }
440    }
441
442    println!(
443        "Registered {} keybindings from configuration",
444        bindings.len()
445    );
446    cx.bind_keys(bindings);
447}
448
449fn main() {
450    Application::new().run(move |cx: &mut App| {
451        load_keymaps(cx);
452
453        cx.open_window(
454            WindowOptions {
455                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
456                    None,
457                    size(px(800.0), px(600.0)),
458                    cx,
459                ))),
460                focus: true,
461                ..Default::default()
462            },
463            |_window, cx| cx.new(EditorView::new),
464        )
465        .unwrap();
466
467        cx.activate(true)
468    });
469}
470
471fn get_rust_sample() -> String {
472    r#"// Rust sample code
473use std::collections::HashMap;
474
475fn main() {
476    let mut count = 0;
477
478    // Count from 1 to 10
479    for i in 1..=10 {
480        count += i;
481    }
482
483    // HashMap example
484    let mut scores = HashMap::new();
485    scores.insert("Blue", 10);
486    scores.insert("Yellow", 50);
487
488    println!("Final count: {}", count);
489}"#
490    .to_string()
491}
492
493fn get_plain_text_sample() -> String {
494    r#"This is a plain text document.
495
496No syntax highlighting is applied to plain text files.
497You can write anything here without worrying about code formatting.
498
499Features of this editor:
500- Syntax highlighting for multiple languages
501- Theme switching with Cmd+[ and Cmd+]
502- Language switching with Cmd+Shift+[ and Cmd+Shift+]
503- Text selection with Shift+Arrow keys
504- Copy, Cut, and Paste support
505- Line numbers
506- Active line highlighting
507
508The editor uses the syntect library for syntax highlighting,
509which provides TextMate-compatible syntax definitions and themes.
510
511Try switching between different languages and themes to see
512how the editor adapts to different file types!"#
513        .to_string()
514}