Skip to main content

ui/components/
code_editor.rs

1use gpui::{AnyElement, Context, Entity, Render, rgb};
2
3use crate::prelude::*;
4use crate::TextInput;
5
6/// The mockup's fixed line-number gutter width.
7const GUTTER_WIDTH: Pixels = px(44.);
8/// Matches the mockup's `font-family:'IBM Plex Mono'`.
9const CODE_FONT_FAMILY: &str = "IBM Plex Mono";
10/// Matches the mockup's `font-size:12.5px`.
11const CODE_FONT_SIZE: Pixels = px(12.5);
12/// Matches the mockup's `line-height:1.7`.
13const CODE_LINE_HEIGHT: f32 = 1.7;
14
15/// A multi-line code area with a line-number gutter, composed from a
16/// multiline `TextInput` (no syntax highlighting — see Phase 02 ADR
17/// Rationale: highlighting is a materially larger, separately-scoped
18/// effort).
19///
20/// Stateful view — create with `cx.new(|cx| CodeEditor::new(cx))` and store
21/// the resulting `Entity<CodeEditor>`.
22pub struct CodeEditor {
23    input: Entity<TextInput>,
24}
25
26impl CodeEditor {
27    pub fn new(cx: &mut Context<Self>) -> Self {
28        let input = cx.new(|cx| TextInput::new(cx).multiline(true));
29        cx.observe(&input, |_, _, cx| cx.notify()).detach();
30        Self { input }
31    }
32
33    /// Toggles read-only mode (used by the Scripts tab's read-only code
34    /// preview, which reuses this component rather than a second one).
35    /// Forwards the flag to the wrapped `TextInput` — no key-handling logic
36    /// is duplicated here; the `TextInput`'s own `read_only` flag remains the
37    /// single source of truth.
38    pub fn read_only(mut self, cx: &mut Context<Self>, read_only: bool) -> Self {
39        self.set_read_only(read_only, cx);
40        self
41    }
42
43    /// Dynamically toggles read-only mode after construction.
44    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
45        self.input.update(cx, |input, cx| input.set_read_only(read_only, cx));
46        cx.notify();
47    }
48
49    /// The current code content.
50    pub fn text(&self, cx: &App) -> String {
51        self.input.read(cx).text().to_string()
52    }
53
54    /// Programmatically sets the code content (e.g. loading a mock script
55    /// into a read-only preview).
56    pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
57        self.input.update(cx, |input, cx| input.set_text(text, cx));
58        cx.notify();
59    }
60}
61
62impl Render for CodeEditor {
63    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
64        let line_count = self.input.read(cx).text().matches('\n').count() + 1;
65
66        let gutter = v_flex()
67            .flex_none()
68            .w(GUTTER_WIDTH)
69            .pr(px(16.))
70            .text_right()
71            // Gutter line-number text color from the mockup (`#3A424E`).
72            .text_color(rgb(0x3A424E))
73            .font_family(CODE_FONT_FAMILY)
74            .text_size(CODE_FONT_SIZE)
75            .line_height(relative(CODE_LINE_HEIGHT))
76            .children((1..=line_count).map(|line| div().child(line.to_string())));
77
78        h_flex()
79            .id(("code-editor", cx.entity_id()))
80            .w_full()
81            .items_start()
82            .overflow_y_scroll()
83            .child(gutter)
84            .child(
85                div()
86                    .flex_1()
87                    .min_w_0()
88                    // Code text color from the mockup (`#B7BEC7`).
89                    .text_color(rgb(0xB7BEC7))
90                    .font_family(CODE_FONT_FAMILY)
91                    .text_size(CODE_FONT_SIZE)
92                    .line_height(relative(CODE_LINE_HEIGHT))
93                    .child(self.input.clone()),
94            )
95    }
96}
97
98/// Standalone gallery preview for `CodeEditor` (not registered in the
99/// `Component` catalog since it is a stateful `Entity`, matching
100/// `SearchInput`'s existing convention in this crate).
101pub fn code_editor_preview(_window: &mut Window, cx: &mut App) -> AnyElement {
102    v_flex()
103        .gap_4()
104        .child(cx.new(|cx| CodeEditor::new(cx)))
105        .into_any_element()
106}