1use 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
36struct 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)>, }
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 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 self.editor.delete_selection();
215
216 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 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 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}