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>
This commit is contained in:
Andreas Johansson 2025-11-28 12:51:40 +01:00 committed by GitHub
parent 1ce09cc1d1
commit 7e479aa7f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 302 additions and 240 deletions

View file

@ -22,7 +22,7 @@ impl Example {
let editor = cx.new(|cx| { let editor = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.tab_size(TabSize { .tab_size(TabSize {
tab_size: 4, tab_size: 4,
hard_tabs: false, hard_tabs: false,

View file

@ -23,6 +23,7 @@ pub struct InputStory {
mask_input2: Entity<InputState>, mask_input2: Entity<InputState>,
currency_input: Entity<InputState>, currency_input: Entity<InputState>,
custom_input: Entity<InputState>, custom_input: Entity<InputState>,
code_input: Entity<InputState>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -87,6 +88,13 @@ impl InputStory {
let custom_input = let custom_input =
cx.new(|cx| InputState::new(window, cx).placeholder("here is a custom input")); cx.new(|cx| InputState::new(window, cx).placeholder("here is a custom input"));
let code_input = cx.new(|cx| {
InputState::new(window, cx)
.code_editor("json")
.multi_line(false)
.default_value(r#"{"single_line":"code editor"}"#)
});
let _subscriptions = vec![ let _subscriptions = vec![
cx.subscribe_in(&input1, window, Self::on_input_event), cx.subscribe_in(&input1, window, Self::on_input_event),
cx.subscribe_in(&input2, window, Self::on_input_event), cx.subscribe_in(&input2, window, Self::on_input_event),
@ -113,6 +121,7 @@ impl InputStory {
mask_input2, mask_input2,
currency_input, currency_input,
custom_input, custom_input,
code_input,
_subscriptions, _subscriptions,
} }
} }
@ -250,5 +259,10 @@ impl Render for InputStory {
.child(Input::new(&self.custom_input).appearance(false)), .child(Input::new(&self.custom_input).appearance(false)),
), ),
) )
.child(
section("Single line code editor")
.max_w_md()
.child(Input::new(&self.code_input)),
)
} }
} }

View file

@ -47,7 +47,7 @@ impl TextareaStory {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let textarea = cx.new(|cx| { let textarea = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.rows(10) .rows(10)
.placeholder("Enter text here...") .placeholder("Enter text here...")
.searchable(true) .searchable(true)
@ -77,7 +77,7 @@ impl TextareaStory {
let textarea_no_wrap = cx.new(|cx| { let textarea_no_wrap = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.rows(6) .rows(6)
.soft_wrap(false) .soft_wrap(false)
.default_value("This is a very long line of text to test if the horizontal scrolling function is working properly, and it should not wrap automatically but display a horizontal scrollbar.\nThe second line is also very long text, used to test the horizontal scrolling effect under multiple lines, and you can input more content to test.\nThe third line: Here you can input other long text content that requires horizontal scrolling.\n") .default_value("This is a very long line of text to test if the horizontal scrolling function is working properly, and it should not wrap automatically but display a horizontal scrollbar.\nThe second line is also very long text, used to test the horizontal scrolling effect under multiple lines, and you can input more content to test.\nThe third line: Here you can input other long text content that requires horizontal scrolling.\n")

View file

@ -567,12 +567,12 @@ impl TextElement {
bg_segments: &[(Range<usize>, Hsla)], bg_segments: &[(Range<usize>, Hsla)],
window: &mut Window, window: &mut Window,
) -> Vec<LineLayout> { ) -> Vec<LineLayout> {
let is_multi_line = state.mode.is_multi_line(); let is_single_line = state.mode.is_single_line();
let text_wrapper = &state.text_wrapper; let text_wrapper = &state.text_wrapper;
let visible_range = &last_layout.visible_range; let visible_range = &last_layout.visible_range;
let visible_range_offset = &last_layout.visible_range_offset; let visible_range_offset = &last_layout.visible_range_offset;
if !is_multi_line { if is_single_line {
let shaped_line = window.text_system().shape_line( let shaped_line = window.text_system().shape_line(
display_text.to_string().into(), display_text.to_string().into(),
font_size, font_size,
@ -657,6 +657,7 @@ impl TextElement {
) -> Option<Vec<(Range<usize>, HighlightStyle)>> { ) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
let state = self.state.read(cx); let state = self.state.read(cx);
let text = &state.text; let text = &state.text;
let is_multi_line = state.mode.is_multi_line();
let (highlighter, diagnostics) = match &state.mode { let (highlighter, diagnostics) = match &state.mode {
InputMode::CodeEditor { InputMode::CodeEditor {
@ -676,8 +677,13 @@ impl TextElement {
.skip(visible_range.start) .skip(visible_range.start)
.take(visible_range.len()) .take(visible_range.len())
{ {
// +1 for `\n` let line_len = if is_multi_line {
let line_len = line.len() + 1; // +1 for `\n`
line.len() + 1
} else {
line.len()
};
let range = offset..offset + line_len; let range = offset..offset + line_len;
let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme); let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme);
styles = gpui::combine_highlights(styles, line_styles).collect(); styles = gpui::combine_highlights(styles, line_styles).collect();

View file

@ -1,15 +1,15 @@
use gpui::{ use gpui::{
point, px, Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
SharedString, TextRun, TextStyle, Window, TextRun, TextStyle, Window, point, px,
}; };
use ropey::RopeSlice; use ropey::RopeSlice;
use crate::{ use crate::{
input::{
element::TextElement, mode::InputMode, Indent, IndentInline, InputState, LastLayout,
Outdent, OutdentInline,
},
RopeExt, RopeExt,
input::{
Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, element::TextElement,
mode::InputMode,
},
}; };
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -56,16 +56,22 @@ impl TabSize {
impl InputMode { impl InputMode {
#[inline] #[inline]
pub(super) fn is_indentable(&self) -> bool { pub(super) fn is_indentable(&self) -> bool {
matches!( match self {
self, InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } *multi_line
) }
_ => false,
}
} }
#[inline] #[inline]
pub(super) fn has_indent_guides(&self) -> bool { pub(super) fn has_indent_guides(&self) -> bool {
match self { match self {
InputMode::CodeEditor { indent_guides, .. } => *indent_guides, InputMode::CodeEditor {
indent_guides,
multi_line,
..
} => *indent_guides && *multi_line,
_ => false, _ => false,
} }
} }
@ -73,7 +79,7 @@ impl InputMode {
#[inline] #[inline]
pub(super) fn tab_size(&self) -> TabSize { pub(super) fn tab_size(&self) -> TabSize {
match self { match self {
InputMode::MultiLine { tab, .. } => *tab, InputMode::PlainText { tab, .. } => *tab,
InputMode::CodeEditor { tab, .. } => *tab, InputMode::CodeEditor { tab, .. } => *tab,
_ => TabSize::default(), _ => TabSize::default(),
} }
@ -168,7 +174,7 @@ impl InputState {
/// ///
/// Only for [`InputMode::CodeEditor`] mode. /// Only for [`InputMode::CodeEditor`] mode.
pub fn indent_guides(mut self, indent_guides: bool) -> Self { pub fn indent_guides(mut self, indent_guides: bool) -> Self {
debug_assert!(self.mode.is_code_editor()); debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { if let InputMode::CodeEditor {
indent_guides: l, .. indent_guides: l, ..
} = &mut self.mode } = &mut self.mode
@ -199,11 +205,11 @@ impl InputState {
/// Set the tab size for the input. /// Set the tab size for the input.
/// ///
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode. /// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line.
pub fn tab_size(mut self, tab: TabSize) -> Self { pub fn tab_size(mut self, tab: TabSize) -> Self {
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor()); debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
match &mut self.mode { match &mut self.mode {
InputMode::MultiLine { tab: t, .. } => *t = tab, InputMode::PlainText { tab: t, .. } => *t = tab,
InputMode::CodeEditor { tab: t, .. } => *t = tab, InputMode::CodeEditor { tab: t, .. } => *t = tab,
_ => {} _ => {}
} }

View file

@ -10,20 +10,23 @@ use crate::highlighter::DiagnosticSet;
use crate::highlighter::SyntaxHighlighter; use crate::highlighter::SyntaxHighlighter;
use crate::input::{RopeExt as _, TabSize}; use crate::input::{RopeExt as _, TabSize};
#[derive(Default, Clone)] #[derive(Clone)]
pub enum InputMode { pub(crate) enum InputMode {
#[default] /// A plain text input mode.
SingleLine, PlainText {
MultiLine { multi_line: bool,
tab: TabSize, tab: TabSize,
rows: usize, rows: usize,
}, },
/// An auto grow input mode.
AutoGrow { AutoGrow {
rows: usize, rows: usize,
min_rows: usize, min_rows: usize,
max_rows: usize, max_rows: usize,
}, },
/// A code editor input mode.
CodeEditor { CodeEditor {
multi_line: bool,
tab: TabSize, tab: TabSize,
rows: usize, rows: usize,
/// Show line number /// Show line number
@ -35,11 +38,58 @@ pub enum InputMode {
}, },
} }
impl Default for InputMode {
fn default() -> Self {
InputMode::plain_text()
}
}
#[allow(unused)] #[allow(unused)]
impl InputMode { impl InputMode {
/// Create a plain input mode with default settings.
pub(super) fn plain_text() -> Self {
InputMode::PlainText {
multi_line: false,
tab: TabSize::default(),
rows: 1,
}
}
/// Create a code editor input mode with default settings.
pub(super) fn code_editor(language: impl Into<SharedString>) -> Self {
InputMode::CodeEditor {
rows: 2,
multi_line: true,
tab: TabSize::default(),
language: language.into(),
highlighter: Rc::new(RefCell::new(None)),
line_number: true,
indent_guides: true,
diagnostics: DiagnosticSet::new(&Rope::new()),
}
}
/// Create an auto grow input mode with given min and max rows.
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
InputMode::AutoGrow {
rows: min_rows,
min_rows,
max_rows,
}
}
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
match &mut self {
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line,
InputMode::AutoGrow { .. } => {}
}
self
}
#[inline] #[inline]
pub(super) fn is_single_line(&self) -> bool { pub(super) fn is_single_line(&self) -> bool {
matches!(self, InputMode::SingleLine) !self.is_multi_line()
} }
#[inline] #[inline]
@ -54,15 +104,16 @@ impl InputMode {
#[inline] #[inline]
pub(super) fn is_multi_line(&self) -> bool { pub(super) fn is_multi_line(&self) -> bool {
matches!( match self {
self, InputMode::PlainText { multi_line, .. } => *multi_line,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. } InputMode::CodeEditor { multi_line, .. } => *multi_line,
) InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
}
} }
pub(super) fn set_rows(&mut self, new_rows: usize) { pub(super) fn set_rows(&mut self, new_rows: usize) {
match self { match self {
InputMode::MultiLine { rows, .. } => { InputMode::PlainText { rows, .. } => {
*rows = new_rows; *rows = new_rows;
} }
InputMode::CodeEditor { rows, .. } => { InputMode::CodeEditor { rows, .. } => {
@ -75,7 +126,6 @@ impl InputMode {
} => { } => {
*rows = new_rows.clamp(*min_rows, *max_rows); *rows = new_rows.clamp(*min_rows, *max_rows);
} }
_ => {}
} }
} }
@ -90,11 +140,14 @@ impl InputMode {
/// At least 1 row be return. /// At least 1 row be return.
pub(super) fn rows(&self) -> usize { pub(super) fn rows(&self) -> usize {
if !self.is_multi_line() {
return 1;
}
match self { match self {
InputMode::MultiLine { rows, .. } => *rows, InputMode::PlainText { rows, .. } => *rows,
InputMode::CodeEditor { rows, .. } => *rows, InputMode::CodeEditor { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows, InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
} }
.max(1) .max(1)
} }
@ -103,7 +156,6 @@ impl InputMode {
#[allow(unused)] #[allow(unused)]
pub(super) fn min_rows(&self) -> usize { pub(super) fn min_rows(&self) -> usize {
match self { match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows, InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1, _ => 1,
} }
@ -112,10 +164,13 @@ impl InputMode {
#[allow(unused)] #[allow(unused)]
pub(super) fn max_rows(&self) -> usize { pub(super) fn max_rows(&self) -> usize {
if !self.is_multi_line() {
return 1;
}
match self { match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows, InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1, _ => usize::MAX,
} }
} }
@ -124,7 +179,11 @@ impl InputMode {
#[inline] #[inline]
pub(super) fn line_number(&self) -> bool { pub(super) fn line_number(&self) -> bool {
match self { match self {
InputMode::CodeEditor { line_number, .. } => *line_number, InputMode::CodeEditor {
line_number,
multi_line,
..
} => *line_number && *multi_line,
_ => false, _ => false,
} }
} }
@ -202,3 +261,88 @@ impl InputMode {
} }
} }
} }
#[cfg(test)]
mod tests {
use ropey::Rope;
use crate::{
highlighter::DiagnosticSet,
input::{TabSize, mode::InputMode},
};
#[test]
fn test_code_editor() {
let mode = InputMode::code_editor("rust");
assert_eq!(mode.is_code_editor(), true);
assert_eq!(mode.is_multi_line(), true);
assert_eq!(mode.is_single_line(), false);
assert_eq!(mode.line_number(), true);
assert_eq!(mode.has_indent_guides(), true);
assert_eq!(mode.max_rows(), usize::MAX);
assert_eq!(mode.min_rows(), 1);
let mode = InputMode::CodeEditor {
multi_line: false,
line_number: true,
indent_guides: true,
rows: 0,
tab: Default::default(),
language: "rust".into(),
highlighter: Default::default(),
diagnostics: DiagnosticSet::new(&Rope::new()),
};
assert_eq!(mode.is_code_editor(), true);
assert_eq!(mode.is_multi_line(), false);
assert_eq!(mode.is_single_line(), true);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.has_indent_guides(), false);
assert_eq!(mode.max_rows(), 1);
assert_eq!(mode.min_rows(), 1);
}
#[test]
fn test_plain() {
let mode = InputMode::PlainText {
multi_line: true,
tab: TabSize::default(),
rows: 5,
};
assert_eq!(mode.is_code_editor(), false);
assert_eq!(mode.is_multi_line(), true);
assert_eq!(mode.is_single_line(), false);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.rows(), 5);
assert_eq!(mode.max_rows(), usize::MAX);
assert_eq!(mode.min_rows(), 1);
let mode = InputMode::plain_text();
assert_eq!(mode.is_code_editor(), false);
assert_eq!(mode.is_multi_line(), false);
assert_eq!(mode.is_single_line(), true);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.max_rows(), 1);
assert_eq!(mode.min_rows(), 1);
}
#[test]
fn test_auto_grow() {
let mut mode = InputMode::auto_grow(2, 5);
assert_eq!(mode.is_code_editor(), false);
assert_eq!(mode.is_multi_line(), true);
assert_eq!(mode.is_single_line(), false);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.rows(), 2);
assert_eq!(mode.max_rows(), 5);
assert_eq!(mode.min_rows(), 2);
mode.set_rows(4);
assert_eq!(mode.rows(), 4);
mode.set_rows(1);
assert_eq!(mode.rows(), 2);
mode.set_rows(10);
assert_eq!(mode.rows(), 5);
}
}

View file

@ -12,15 +12,14 @@ use gpui::{
}; };
use ropey::{Rope, RopeSlice}; use ropey::{Rope, RopeSlice};
use serde::Deserialize; use serde::Deserialize;
use std::cell::RefCell;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use sum_tree::Bias; use sum_tree::Bias;
use unicode_segmentation::*; use unicode_segmentation::*;
use super::{ use super::{
TabSize, blink_cursor::BlinkCursor, change::Change, element::TextElement, blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
mask_pattern::MaskPattern, mode::InputMode, number_input, text_wrapper::TextWrapper, mode::InputMode, number_input, text_wrapper::TextWrapper,
}; };
use crate::Size; use crate::Size;
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp}; use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
@ -391,7 +390,7 @@ impl InputState {
loading: false, loading: false,
pattern: None, pattern: None,
validate: None, validate: None,
mode: InputMode::SingleLine, mode: InputMode::default(),
last_layout: None, last_layout: None,
last_bounds: None, last_bounds: None,
last_selected_range: None, last_selected_range: None,
@ -417,24 +416,17 @@ impl InputState {
} }
} }
/// Set Input to use [`InputMode::MultiLine`] mode. /// Set Input to use multi line mode.
/// ///
/// Default rows is 2. /// Default rows is 2.
pub fn multi_line(mut self) -> Self { pub fn multi_line(mut self, multi_line: bool) -> Self {
self.mode = InputMode::MultiLine { self.mode = self.mode.multi_line(multi_line);
rows: 2,
tab: TabSize::default(),
};
self self
} }
/// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit. /// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit.
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self { pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
self.mode = InputMode::AutoGrow { self.mode = InputMode::auto_grow(min_rows, max_rows);
rows: min_rows,
min_rows: min_rows,
max_rows: max_rows,
};
self self
} }
@ -445,7 +437,9 @@ impl InputState {
/// - line_number: true /// - line_number: true
/// - tab_size: 2 /// - tab_size: 2
/// - hard_tabs: false /// - hard_tabs: false
/// - height: full /// - height: 100%
/// - multi_line: true
/// - indent_guides: true
/// ///
/// If `highlighter` is None, will use the default highlighter. /// If `highlighter` is None, will use the default highlighter.
/// ///
@ -459,15 +453,7 @@ impl InputState {
/// - Large Text support, up to 50K lines. /// - Large Text support, up to 50K lines.
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self { pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
let language: SharedString = language.into(); let language: SharedString = language.into();
self.mode = InputMode::CodeEditor { self.mode = InputMode::code_editor(language);
rows: 2,
tab: TabSize::default(),
language,
highlighter: Rc::new(RefCell::new(None)),
line_number: true,
indent_guides: true,
diagnostics: DiagnosticSet::new(&Rope::new()),
};
self.searchable = true; self.searchable = true;
self self
} }
@ -487,7 +473,7 @@ impl InputState {
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode. /// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
pub fn line_number(mut self, line_number: bool) -> Self { pub fn line_number(mut self, line_number: bool) -> Self {
debug_assert!(self.mode.is_code_editor()); debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number; *l = line_number;
} }
@ -496,7 +482,7 @@ impl InputState {
/// Set line number, only for [`InputMode::CodeEditor`] mode. /// Set line number, only for [`InputMode::CodeEditor`] mode.
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) { pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_code_editor()); debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number; *l = line_number;
} }
@ -510,7 +496,9 @@ impl InputState {
/// default: 2 /// default: 2
pub fn rows(mut self, rows: usize) -> Self { pub fn rows(mut self, rows: usize) -> Self {
match &mut self.mode { match &mut self.mode {
InputMode::MultiLine { rows: r, .. } => *r = rows, InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => {
*r = rows
}
InputMode::AutoGrow { InputMode::AutoGrow {
max_rows: max_r, max_rows: max_r,
rows: r, rows: r,
@ -519,7 +507,6 @@ impl InputState {
*r = rows; *r = rows;
*max_r = rows; *max_r = rows;
} }
_ => {}
} }
self self
} }

View file

@ -15,61 +15,53 @@ use gpui_component::input::{InputState, Input};
## Usage ## Usage
### Basic Textarea ### Textarea
```rust ```rust
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.placeholder("Enter your message...") .placeholder("Enter your message...")
); );
Input::new(&textarea) Input::new(&state)
``` ```
### Fixed Height Textarea With fixed height Textarea:
```rust ```rust
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.rows(10) // Set number of rows .rows(10) // Set number of rows
.placeholder("Enter text here...") .placeholder("Enter text here...")
); );
Input::new(&textarea) Input::new(&state)
.h(px(320.)) // Set explicit height .h(px(320.)) // Set explicit height
``` ```
### Auto-Resizing Textarea ### AutoGrow
```rust ```rust
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.auto_grow(1, 5) // min_rows: 1, max_rows: 5 .auto_grow(1, 5) // min_rows: 1, max_rows: 5
.placeholder("Type here and watch it grow...") .placeholder("Type here and watch it grow...")
); );
Input::new(&textarea) Input::new(&state)
``` ```
### With Default Content ### 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](https://tree-sitter.github.io/tree-sitter/) for syntax highlighting, and [ropey](https://github.com/cessen/ropey) for text storage and manipulation.
```rust ```rust
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.rows(6)
.default_value("Hello World!\n\nThis is a multi-line textarea with default content.")
);
Input::new(&textarea)
```
### Code Editor Mode
```rust
let code_editor = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.code_editor("rust") // Language for syntax highlighting .code_editor("rust") // Language for syntax highlighting
.line_number(true) // Show line numbers .line_number(true) // Show line numbers
@ -77,142 +69,129 @@ let code_editor = cx.new(|cx|
.default_value("fn main() {\n println!(\"Hello, world!\");\n}") .default_value("fn main() {\n println!(\"Hello, world!\");\n}")
); );
Input::new(&code_editor) Input::new(&state)
.h_full() // Full height .h_full() // Full height
``` ```
### Textarea with Custom Tab Size #### 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.
```rust
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
```rust ```rust
use gpui_component::input::TabSize; use gpui_component::input::TabSize;
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.tab_size(TabSize { .tab_size(TabSize {
tab_size: 4, tab_size: 4,
hard_tabs: false, // Use spaces instead of tabs hard_tabs: false, // Use spaces instead of tabs
}) })
); );
Input::new(&textarea) Input::new(&state)
``` ```
### Searchable Textarea ### 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:
```rust ```rust
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.searchable(true) // Enable Ctrl+F search .searchable(true) // Enable Ctrl+F search
.rows(15) .rows(15)
.default_value("Search through this content...") .default_value("Search through this content...")
); );
Input::new(&textarea) Input::new(&state)
``` ```
### Soft Wrap Control ### 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:
```rust ```rust
// With soft wrap (default) // With soft wrap (default)
let textarea_wrap = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.soft_wrap(true) .soft_wrap(true)
.rows(6) .rows(6)
); );
// Without soft wrap (horizontal scrolling) // Without soft wrap (horizontal scrolling)
let textarea_no_wrap = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.soft_wrap(false) .soft_wrap(false)
.rows(6) .rows(6)
.default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.") .default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.")
); );
v_flex()
.gap_4()
.child(Input::new(&textarea_wrap))
.child(Input::new(&textarea_no_wrap))
```
### Character Counting
```rust
struct TextareaView {
textarea: Entity<InputState>,
}
impl Render for TextareaView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let char_count = self.textarea.read(cx).value().len();
let max_chars = 500;
v_flex()
.gap_2()
.child(
Input::new(&self.textarea)
.h(px(120.))
)
.child(
div()
.text_right()
.text_sm()
.text_color(if char_count > max_chars {
cx.theme().destructive
} else {
cx.theme().muted_foreground
})
.child(format!("{}/{}", char_count, max_chars))
)
}
}
``` ```
### Text Manipulation ### Text Manipulation
```rust ```rust
// Insert text at cursor position // Insert text at cursor position
textarea.update(cx, |input, cx| { state.update(cx, |state, cx| {
input.insert("inserted text", window, cx); state.insert("inserted text", window, cx);
}); });
// Replace all content // Replace all content
textarea.update(cx, |input, cx| { state.update(cx, |state, cx| {
input.replace("new content", window, cx); state.replace("new content", window, cx);
}); });
// Set cursor position // Set cursor position
textarea.update(cx, |input, cx| { state.update(cx, |state, cx| {
input.set_cursor_position(Position { line: 2, character: 5 }, window, cx); state.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
}); });
// Get cursor position // Get cursor position
let position = textarea.read(cx).cursor_position(); let position = state.read(cx).cursor_position();
println!("Line: {}, Column: {}", position.line, position.character); println!("Line: {}, Column: {}", position.line, position.character);
``` ```
### Validation ### Validation
```rust ```rust
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line(true)
.validate(|text, _| { .validate(|text, _| {
// Validate that content is not empty and under 1000 chars // Validate that content is not empty and under 1000 chars
!text.trim().is_empty() && text.len() <= 1000 !text.trim().is_empty() && text.len() <= 1000
}) })
); );
Input::new(&textarea) Input::new(&state)
``` ```
### Handle Events ### Handle Events
```rust ```rust
cx.subscribe_in(&textarea, window, |view, state, event, window, cx| { cx.subscribe_in(&state, window, |view, state, event, window, cx| {
match event { match event {
InputEvent::Change => { InputEvent::Change => {
let content = state.read(cx).value(); let content = state.read(cx).value();
@ -234,7 +213,7 @@ cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
### Disabled State ### Disabled State
```rust ```rust
Input::new(&textarea) Input::new(&state)
.disabled(true) .disabled(true)
.h(px(200.)) .h(px(200.))
``` ```
@ -243,7 +222,7 @@ Input::new(&textarea)
```rust ```rust
// Without default appearance // Without default appearance
Input::new(&textarea) Input::new(&state)
.appearance(false) .appearance(false)
.h(px(200.)) .h(px(200.))
@ -255,80 +234,25 @@ div()
.rounded_lg() .rounded_lg()
.p_4() .p_4()
.child( .child(
Input::new(&textarea) Input::new(&state)
.appearance(false) .appearance(false)
.h(px(150.)) .h(px(150.))
) )
``` ```
## API Reference
### InputState (Multi-line Methods)
| Method | Description |
| -------------------------------------- | ------------------------------------------------ |
| `multi_line()` | Enable multi-line mode with 2 rows default |
| `auto_grow(min, max)` | Enable auto-resize between min and max rows |
| `code_editor(language)` | Enable code editor mode with syntax highlighting |
| `rows(count)` | Set number of visible rows |
| `tab_size(TabSize)` | Configure tab behavior |
| `searchable(bool)` | Enable/disable search (Ctrl+F) |
| `soft_wrap(bool)` | Enable/disable text wrapping |
| `line_number(bool)` | Show/hide line numbers (code editor only) |
| `cursor_position()` | Get current cursor position as `Position` |
| `set_cursor_position(pos, window, cx)` | Set cursor to specific line/column |
| `insert(text, window, cx)` | Insert text at cursor |
| `replace(text, window, cx)` | Replace all content |
### Input (Multi-line Methods)
| Method | Description |
| ----------- | -------------------------- |
| `h(height)` | Set explicit height |
| `h_full()` | Take full available height |
### Position
| Field | Description |
| ----------- | ---------------------------------- |
| `line` | 0-based line number |
| `character` | 0-based character position in line |
### TabSize
| Field | Description |
| ----------- | ------------------------------------------ |
| `tab_size` | Number of spaces per tab (default: 2) |
| `hard_tabs` | Use actual tab characters (default: false) |
### Keyboard Shortcuts
| Shortcut | Action |
| ------------- | --------------------------- |
| `Enter` | Insert new line |
| `Shift+Enter` | Insert new line (secondary) |
| `Tab` | Indent line/selection |
| `Shift+Tab` | Outdent line/selection |
| `Ctrl/Cmd+A` | Select all |
| `Ctrl/Cmd+Z` | Undo |
| `Ctrl/Cmd+Y` | Redo |
| `Ctrl/Cmd+F` | Open search (if enabled) |
| `Ctrl/Cmd+[` | Outdent block |
| `Ctrl/Cmd+]` | Indent block |
## Examples ## Examples
### Comment Box ### Comment Box
```rust ```rust
struct CommentBox { struct CommentBox {
textarea: Entity<InputState>, state: Entity<InputState>,
char_limit: usize, char_limit: usize,
} }
impl CommentBox { impl CommentBox {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let textarea = cx.new(|cx| let state = cx.new(|cx|
InputState::new(window, cx) InputState::new(window, cx)
.auto_grow(3, 8) .auto_grow(3, 8)
.placeholder("Write your comment...") .placeholder("Write your comment...")
@ -336,7 +260,7 @@ impl CommentBox {
); );
Self { Self {
textarea, state,
char_limit: 500, char_limit: 500,
} }
} }
@ -344,13 +268,13 @@ impl CommentBox {
impl Render for CommentBox { impl Render for CommentBox {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let content = self.textarea.read(cx).value(); let content = self.state.read(cx).value();
let char_count = content.len(); let char_count = content.len();
let remaining = self.char_limit.saturating_sub(char_count); let remaining = self.char_limit.saturating_sub(char_count);
v_flex() v_flex()
.gap_2() .gap_2()
.child(Input::new(&self.textarea)) .child(Input::new(&self.state))
.child( .child(
h_flex() h_flex()
.justify_between() .justify_between()
@ -457,22 +381,3 @@ impl Render for TextEditor {
} }
} }
``` ```
## Performance Notes
- Optimized for large text content (up to 200K lines in code editor mode)
- Efficient text wrapping and line measurement
- Virtual scrolling for long documents
- Minimal re-renders on text changes
- Efficient syntax highlighting with tree-sitter
- Smart auto-grow calculations
## Best Practices
1. **Auto-resize**: Use `auto_grow()` for dynamic content like comments or messages
2. **Fixed size**: Use `multi_line().rows(n)` for consistent layouts like forms
3. **Code editing**: Use `code_editor()` for syntax-aware editing
4. **Validation**: Always validate long-form content on the client side
5. **Character limits**: Show character counters for user guidance
6. **Search**: Enable search for long content areas
7. **Soft wrap**: Disable for code, enable for prose