gpui-component/docs/docs/components/editor.md
Andreas Johansson 7e479aa7f2
input: Add to support CodeEditor as single line mode. (#1696)
## Screenshot

From `input_story`:

<img width="1069" height="122" alt="Screenshot From 2025-11-28 09-14-03"
src="https://github.com/user-attachments/assets/f14ef2fc-4939-4ceb-be3e-bede492f8602"
/>

## Breaking Changes

- `.multi_line()` was changed to `.multi_line(bool)`.  
- Removed pub `InputMode`, this should only for internal.

```diff
InputState::new(window, cx)
-    .multi_line()
+    .multi_line(true)
```

## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)

Use cases,

1) In my API client, I have a tree sitter language for the URL input,

<img width="1001" height="162" alt="Image"
src="https://github.com/user-attachments/assets/9771decf-b0b7-4230-8ed6-784a95b72af1"
/>

2) In my SQL editor I want to be able to allow JSON syntax highlighting
for inline editing of json columns,

<img width="501" height="130" alt="Image"
src="https://github.com/user-attachments/assets/f0f879cc-841f-49d2-a23a-2effc3747f38"
/>

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2025-11-28 19:51:40 +08:00

9.4 KiB

title description
Editor Multi-line text input component with auto-resize, validation, and advanced editing features.

Editor

A powerful multi-line text input component that extends the basic input functionality with support for multiple lines, auto-resizing, syntax highlighting, line numbers, and code editing features. Perfect for forms, code editors, and content editing.

Import

use gpui_component::input::{InputState, Input};

Usage

Textarea

let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .placeholder("Enter your message...")
);

Input::new(&state)

With fixed height Textarea:

let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .rows(10) // Set number of rows
        .placeholder("Enter text here...")
);

Input::new(&state)
    .h(px(320.)) // Set explicit height

AutoGrow

let state = cx.new(|cx|
    InputState::new(window, cx)
        .auto_grow(1, 5) // min_rows: 1, max_rows: 5
        .placeholder("Type here and watch it grow...")
);

Input::new(&state)

CodeEditor

GPUI Component's InputState supports a code editor mode with syntax highlighting, line numbers, and search functionality.

It design for high performance and can handle large files efficiently. We used tree-sitter for syntax highlighting, and ropey for text storage and manipulation.

let state = cx.new(|cx|
    InputState::new(window, cx)
        .code_editor("rust") // Language for syntax highlighting
        .line_number(true) // Show line numbers
        .searchable(true) // Enable search functionality
        .default_value("fn main() {\n    println!(\"Hello, world!\");\n}")
);

Input::new(&state)
    .h_full() // Full height

Single Line Mode

Sometimes you may want to use the code editor features but restrict input to a single line, for example for code snippets or commands.

let state = cx.new(|cx|
    InputState::new(window, cx)
        .code_editor("rust")
        .multi_line(false) // Single line
        .default_value("println!(\"Hello, world!\");")
);

Input::new(&state)

TabSize

use gpui_component::input::TabSize;

let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .tab_size(TabSize {
            tab_size: 4,
            hard_tabs: false, // Use spaces instead of tabs
        })
);

Input::new(&state)

Searchable

The search feature allows for all multi-line inputs to support searching through the content using Ctrl+F (or Cmd+F on Mac).

It provides a search bar with options to navigate between matches and highlight them.

Use searchable method to enable:

let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .searchable(true) // Enable Ctrl+F search
        .rows(15)
        .default_value("Search through this content...")
);

Input::new(&state)

SoftWrap

By default multi-line inputs have soft wrapping enabled, meaning long lines will wrap to fit the width of the textarea.

You can disable soft wrapping to allow horizontal scrolling instead:

// With soft wrap (default)
let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .soft_wrap(true)
        .rows(6)
);

// Without soft wrap (horizontal scrolling)
let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .soft_wrap(false)
        .rows(6)
        .default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.")
);

Text Manipulation

// Insert text at cursor position
state.update(cx, |state, cx| {
    state.insert("inserted text", window, cx);
});

// Replace all content
state.update(cx, |state, cx| {
    state.replace("new content", window, cx);
});

// Set cursor position
state.update(cx, |state, cx| {
    state.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
});

// Get cursor position
let position = state.read(cx).cursor_position();
println!("Line: {}, Column: {}", position.line, position.character);

Validation

let state = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line(true)
        .validate(|text, _| {
            // Validate that content is not empty and under 1000 chars
            !text.trim().is_empty() && text.len() <= 1000
        })
);

Input::new(&state)

Handle Events

cx.subscribe_in(&state, window, |view, state, event, window, cx| {
    match event {
        InputEvent::Change => {
            let content = state.read(cx).value();
            println!("Content changed: {} characters", content.len());
        }
        InputEvent::PressEnter { secondary } => {
            if secondary {
                println!("Shift+Enter pressed - insert line break");
            } else {
                println!("Enter pressed - could submit form");
            }
        }
        InputEvent::Focus => println!("Textarea focused"),
        InputEvent::Blur => println!("Textarea blurred"),
    }
});

Disabled State

Input::new(&state)
    .disabled(true)
    .h(px(200.))

Custom Styling

// Without default appearance
Input::new(&state)
    .appearance(false)
    .h(px(200.))

// Custom container styling
div()
    .bg(cx.theme().background)
    .border_2()
    .border_color(cx.theme().input)
    .rounded_lg()
    .p_4()
    .child(
        Input::new(&state)
            .appearance(false)
            .h(px(150.))
    )

Examples

Comment Box

struct CommentBox {
    state: Entity<InputState>,
    char_limit: usize,
}

impl CommentBox {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let state = cx.new(|cx|
            InputState::new(window, cx)
                .auto_grow(3, 8)
                .placeholder("Write your comment...")
                .validate(|text, _| text.len() <= 500)
        );

        Self {
            state,
            char_limit: 500,
        }
    }
}

impl Render for CommentBox {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let content = self.state.read(cx).value();
        let char_count = content.len();
        let remaining = self.char_limit.saturating_sub(char_count);

        v_flex()
            .gap_2()
            .child(Input::new(&self.state))
            .child(
                h_flex()
                    .justify_between()
                    .child(
                        div()
                            .text_xs()
                            .text_color(cx.theme().muted_foreground)
                            .child(format!("{} characters remaining", remaining))
                    )
                    .child(
                        Button::new("submit")
                            .primary()
                            .disabled(char_count == 0 || char_count > self.char_limit)
                            .label("Post Comment")
                    )
            )
    }
}

Code Editor with Language Selection

struct CodeEditor {
    editor: Entity<InputState>,
    language: String,
}

impl CodeEditor {
    fn set_language(&mut self, language: String, window: &mut Window, cx: &mut Context<Self>) {
        self.language = language.clone();
        self.editor.update(cx, |editor, cx| {
            editor.set_highlighter(language, cx);
        });
    }
}

impl Render for CodeEditor {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_3()
            .child(
                h_flex()
                    .gap_2()
                    .child("Language:")
                    .child(
                        // Language selector dropdown would go here
                        div().child(self.language.clone())
                    )
            )
            .child(
                Input::new(&self.editor)
                    .h(px(400.))
                    .bordered(true)
            )
    }
}

Text Editor with Toolbar

struct TextEditor {
    editor: Entity<InputState>,
}

impl TextEditor {
    fn format_bold(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.editor.update(cx, |editor, cx| {
            if !editor.selected_range.is_empty() {
                let selected = editor.selected_text().to_string();
                editor.replace(&format!("**{}**", selected), window, cx);
            }
        });
    }
}

impl Render for TextEditor {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(
                h_flex()
                    .gap_1()
                    .p_2()
                    .border_b_1()
                    .border_color(cx.theme().border)
                    .child(
                        Button::new("bold")
                            .ghost()
                            .icon(IconName::Bold)
                            .on_click(cx.listener(Self::format_bold))
                    )
                    .child(
                        Button::new("italic")
                            .ghost()
                            .icon(IconName::Italic)
                    )
            )
            .child(
                Input::new(&self.editor)
                    .h(px(300.))
            )
    }
}