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:
parent
1ce09cc1d1
commit
7e479aa7f2
8 changed files with 302 additions and 240 deletions
|
|
@ -22,7 +22,7 @@ impl Example {
|
|||
|
||||
let editor = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.tab_size(TabSize {
|
||||
tab_size: 4,
|
||||
hard_tabs: false,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub struct InputStory {
|
|||
mask_input2: Entity<InputState>,
|
||||
currency_input: Entity<InputState>,
|
||||
custom_input: Entity<InputState>,
|
||||
code_input: Entity<InputState>,
|
||||
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
|
@ -87,6 +88,13 @@ impl InputStory {
|
|||
let 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![
|
||||
cx.subscribe_in(&input1, window, Self::on_input_event),
|
||||
cx.subscribe_in(&input2, window, Self::on_input_event),
|
||||
|
|
@ -113,6 +121,7 @@ impl InputStory {
|
|||
mask_input2,
|
||||
currency_input,
|
||||
custom_input,
|
||||
code_input,
|
||||
_subscriptions,
|
||||
}
|
||||
}
|
||||
|
|
@ -250,5 +259,10 @@ impl Render for InputStory {
|
|||
.child(Input::new(&self.custom_input).appearance(false)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Single line code editor")
|
||||
.max_w_md()
|
||||
.child(Input::new(&self.code_input)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ impl TextareaStory {
|
|||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let textarea = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.rows(10)
|
||||
.placeholder("Enter text here...")
|
||||
.searchable(true)
|
||||
|
|
@ -77,7 +77,7 @@ impl TextareaStory {
|
|||
|
||||
let textarea_no_wrap = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.rows(6)
|
||||
.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")
|
||||
|
|
|
|||
|
|
@ -567,12 +567,12 @@ impl TextElement {
|
|||
bg_segments: &[(Range<usize>, Hsla)],
|
||||
window: &mut Window,
|
||||
) -> 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 visible_range = &last_layout.visible_range;
|
||||
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(
|
||||
display_text.to_string().into(),
|
||||
font_size,
|
||||
|
|
@ -657,6 +657,7 @@ impl TextElement {
|
|||
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
||||
let state = self.state.read(cx);
|
||||
let text = &state.text;
|
||||
let is_multi_line = state.mode.is_multi_line();
|
||||
|
||||
let (highlighter, diagnostics) = match &state.mode {
|
||||
InputMode::CodeEditor {
|
||||
|
|
@ -676,8 +677,13 @@ impl TextElement {
|
|||
.skip(visible_range.start)
|
||||
.take(visible_range.len())
|
||||
{
|
||||
// +1 for `\n`
|
||||
let line_len = line.len() + 1;
|
||||
let line_len = if is_multi_line {
|
||||
// +1 for `\n`
|
||||
line.len() + 1
|
||||
} else {
|
||||
line.len()
|
||||
};
|
||||
|
||||
let range = offset..offset + line_len;
|
||||
let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme);
|
||||
styles = gpui::combine_highlights(styles, line_styles).collect();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
use gpui::{
|
||||
point, px, Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels,
|
||||
SharedString, TextRun, TextStyle, Window,
|
||||
Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
|
||||
TextRun, TextStyle, Window, point, px,
|
||||
};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
use crate::{
|
||||
input::{
|
||||
element::TextElement, mode::InputMode, Indent, IndentInline, InputState, LastLayout,
|
||||
Outdent, OutdentInline,
|
||||
},
|
||||
RopeExt,
|
||||
input::{
|
||||
Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, element::TextElement,
|
||||
mode::InputMode,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
|
|
@ -56,16 +56,22 @@ impl TabSize {
|
|||
impl InputMode {
|
||||
#[inline]
|
||||
pub(super) fn is_indentable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. }
|
||||
)
|
||||
match self {
|
||||
InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
|
||||
*multi_line
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn has_indent_guides(&self) -> bool {
|
||||
match self {
|
||||
InputMode::CodeEditor { indent_guides, .. } => *indent_guides,
|
||||
InputMode::CodeEditor {
|
||||
indent_guides,
|
||||
multi_line,
|
||||
..
|
||||
} => *indent_guides && *multi_line,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +79,7 @@ impl InputMode {
|
|||
#[inline]
|
||||
pub(super) fn tab_size(&self) -> TabSize {
|
||||
match self {
|
||||
InputMode::MultiLine { tab, .. } => *tab,
|
||||
InputMode::PlainText { tab, .. } => *tab,
|
||||
InputMode::CodeEditor { tab, .. } => *tab,
|
||||
_ => TabSize::default(),
|
||||
}
|
||||
|
|
@ -168,7 +174,7 @@ impl InputState {
|
|||
///
|
||||
/// Only for [`InputMode::CodeEditor`] mode.
|
||||
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 {
|
||||
indent_guides: l, ..
|
||||
} = &mut self.mode
|
||||
|
|
@ -199,11 +205,11 @@ impl InputState {
|
|||
|
||||
/// 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 {
|
||||
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
||||
match &mut self.mode {
|
||||
InputMode::MultiLine { tab: t, .. } => *t = tab,
|
||||
InputMode::PlainText { tab: t, .. } => *t = tab,
|
||||
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,20 +10,23 @@ use crate::highlighter::DiagnosticSet;
|
|||
use crate::highlighter::SyntaxHighlighter;
|
||||
use crate::input::{RopeExt as _, TabSize};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub enum InputMode {
|
||||
#[default]
|
||||
SingleLine,
|
||||
MultiLine {
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum InputMode {
|
||||
/// A plain text input mode.
|
||||
PlainText {
|
||||
multi_line: bool,
|
||||
tab: TabSize,
|
||||
rows: usize,
|
||||
},
|
||||
/// An auto grow input mode.
|
||||
AutoGrow {
|
||||
rows: usize,
|
||||
min_rows: usize,
|
||||
max_rows: usize,
|
||||
},
|
||||
/// A code editor input mode.
|
||||
CodeEditor {
|
||||
multi_line: bool,
|
||||
tab: TabSize,
|
||||
rows: usize,
|
||||
/// Show line number
|
||||
|
|
@ -35,11 +38,58 @@ pub enum InputMode {
|
|||
},
|
||||
}
|
||||
|
||||
impl Default for InputMode {
|
||||
fn default() -> Self {
|
||||
InputMode::plain_text()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
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]
|
||||
pub(super) fn is_single_line(&self) -> bool {
|
||||
matches!(self, InputMode::SingleLine)
|
||||
!self.is_multi_line()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -54,15 +104,16 @@ impl InputMode {
|
|||
|
||||
#[inline]
|
||||
pub(super) fn is_multi_line(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
|
||||
)
|
||||
match self {
|
||||
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||
InputMode::CodeEditor { multi_line, .. } => *multi_line,
|
||||
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
||||
match self {
|
||||
InputMode::MultiLine { rows, .. } => {
|
||||
InputMode::PlainText { rows, .. } => {
|
||||
*rows = new_rows;
|
||||
}
|
||||
InputMode::CodeEditor { rows, .. } => {
|
||||
|
|
@ -75,7 +126,6 @@ impl InputMode {
|
|||
} => {
|
||||
*rows = new_rows.clamp(*min_rows, *max_rows);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,11 +140,14 @@ impl InputMode {
|
|||
|
||||
/// At least 1 row be return.
|
||||
pub(super) fn rows(&self) -> usize {
|
||||
if !self.is_multi_line() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
match self {
|
||||
InputMode::MultiLine { rows, .. } => *rows,
|
||||
InputMode::PlainText { rows, .. } => *rows,
|
||||
InputMode::CodeEditor { rows, .. } => *rows,
|
||||
InputMode::AutoGrow { rows, .. } => *rows,
|
||||
_ => 1,
|
||||
}
|
||||
.max(1)
|
||||
}
|
||||
|
|
@ -103,7 +156,6 @@ impl InputMode {
|
|||
#[allow(unused)]
|
||||
pub(super) fn min_rows(&self) -> usize {
|
||||
match self {
|
||||
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
|
||||
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
||||
_ => 1,
|
||||
}
|
||||
|
|
@ -112,10 +164,13 @@ impl InputMode {
|
|||
|
||||
#[allow(unused)]
|
||||
pub(super) fn max_rows(&self) -> usize {
|
||||
if !self.is_multi_line() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
match self {
|
||||
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
|
||||
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
||||
_ => 1,
|
||||
_ => usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +179,11 @@ impl InputMode {
|
|||
#[inline]
|
||||
pub(super) fn line_number(&self) -> bool {
|
||||
match self {
|
||||
InputMode::CodeEditor { line_number, .. } => *line_number,
|
||||
InputMode::CodeEditor {
|
||||
line_number,
|
||||
multi_line,
|
||||
..
|
||||
} => *line_number && *multi_line,
|
||||
_ => 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,14 @@ use gpui::{
|
|||
};
|
||||
use ropey::{Rope, RopeSlice};
|
||||
use serde::Deserialize;
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use sum_tree::Bias;
|
||||
use unicode_segmentation::*;
|
||||
|
||||
use super::{
|
||||
TabSize, blink_cursor::BlinkCursor, change::Change, element::TextElement,
|
||||
mask_pattern::MaskPattern, mode::InputMode, number_input, text_wrapper::TextWrapper,
|
||||
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||
mode::InputMode, number_input, text_wrapper::TextWrapper,
|
||||
};
|
||||
use crate::Size;
|
||||
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||
|
|
@ -391,7 +390,7 @@ impl InputState {
|
|||
loading: false,
|
||||
pattern: None,
|
||||
validate: None,
|
||||
mode: InputMode::SingleLine,
|
||||
mode: InputMode::default(),
|
||||
last_layout: None,
|
||||
last_bounds: 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.
|
||||
pub fn multi_line(mut self) -> Self {
|
||||
self.mode = InputMode::MultiLine {
|
||||
rows: 2,
|
||||
tab: TabSize::default(),
|
||||
};
|
||||
pub fn multi_line(mut self, multi_line: bool) -> Self {
|
||||
self.mode = self.mode.multi_line(multi_line);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.mode = InputMode::AutoGrow {
|
||||
rows: min_rows,
|
||||
min_rows: min_rows,
|
||||
max_rows: max_rows,
|
||||
};
|
||||
self.mode = InputMode::auto_grow(min_rows, max_rows);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -445,7 +437,9 @@ impl InputState {
|
|||
/// - line_number: true
|
||||
/// - tab_size: 2
|
||||
/// - hard_tabs: false
|
||||
/// - height: full
|
||||
/// - height: 100%
|
||||
/// - multi_line: true
|
||||
/// - indent_guides: true
|
||||
///
|
||||
/// If `highlighter` is None, will use the default highlighter.
|
||||
///
|
||||
|
|
@ -459,15 +453,7 @@ impl InputState {
|
|||
/// - Large Text support, up to 50K lines.
|
||||
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
|
||||
let language: SharedString = language.into();
|
||||
self.mode = InputMode::CodeEditor {
|
||||
rows: 2,
|
||||
tab: TabSize::default(),
|
||||
language,
|
||||
highlighter: Rc::new(RefCell::new(None)),
|
||||
line_number: true,
|
||||
indent_guides: true,
|
||||
diagnostics: DiagnosticSet::new(&Rope::new()),
|
||||
};
|
||||
self.mode = InputMode::code_editor(language);
|
||||
self.searchable = true;
|
||||
self
|
||||
}
|
||||
|
|
@ -487,7 +473,7 @@ impl InputState {
|
|||
|
||||
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
|
||||
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 {
|
||||
*l = line_number;
|
||||
}
|
||||
|
|
@ -496,7 +482,7 @@ impl InputState {
|
|||
|
||||
/// Set line number, only for [`InputMode::CodeEditor`] mode.
|
||||
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 {
|
||||
*l = line_number;
|
||||
}
|
||||
|
|
@ -510,7 +496,9 @@ impl InputState {
|
|||
/// default: 2
|
||||
pub fn rows(mut self, rows: usize) -> Self {
|
||||
match &mut self.mode {
|
||||
InputMode::MultiLine { rows: r, .. } => *r = rows,
|
||||
InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => {
|
||||
*r = rows
|
||||
}
|
||||
InputMode::AutoGrow {
|
||||
max_rows: max_r,
|
||||
rows: r,
|
||||
|
|
@ -519,7 +507,6 @@ impl InputState {
|
|||
*r = rows;
|
||||
*max_r = rows;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,61 +15,53 @@ use gpui_component::input::{InputState, Input};
|
|||
|
||||
## Usage
|
||||
|
||||
### Basic Textarea
|
||||
### Textarea
|
||||
|
||||
```rust
|
||||
let textarea = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.placeholder("Enter your message...")
|
||||
);
|
||||
|
||||
Input::new(&textarea)
|
||||
Input::new(&state)
|
||||
```
|
||||
|
||||
### Fixed Height Textarea
|
||||
With fixed height Textarea:
|
||||
|
||||
```rust
|
||||
let textarea = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.rows(10) // Set number of rows
|
||||
.placeholder("Enter text here...")
|
||||
);
|
||||
|
||||
Input::new(&textarea)
|
||||
Input::new(&state)
|
||||
.h(px(320.)) // Set explicit height
|
||||
```
|
||||
|
||||
### Auto-Resizing Textarea
|
||||
### AutoGrow
|
||||
|
||||
```rust
|
||||
let textarea = cx.new(|cx|
|
||||
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(&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
|
||||
let textarea = 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|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.code_editor("rust") // Language for syntax highlighting
|
||||
.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}")
|
||||
);
|
||||
|
||||
Input::new(&code_editor)
|
||||
Input::new(&state)
|
||||
.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
|
||||
use gpui_component::input::TabSize;
|
||||
|
||||
let textarea = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.tab_size(TabSize {
|
||||
tab_size: 4,
|
||||
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
|
||||
let textarea = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.searchable(true) // Enable Ctrl+F search
|
||||
.rows(15)
|
||||
.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
|
||||
// With soft wrap (default)
|
||||
let textarea_wrap = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.soft_wrap(true)
|
||||
.rows(6)
|
||||
);
|
||||
|
||||
// Without soft wrap (horizontal scrolling)
|
||||
let textarea_no_wrap = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.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.")
|
||||
);
|
||||
|
||||
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
|
||||
|
||||
```rust
|
||||
// Insert text at cursor position
|
||||
textarea.update(cx, |input, cx| {
|
||||
input.insert("inserted text", window, cx);
|
||||
state.update(cx, |state, cx| {
|
||||
state.insert("inserted text", window, cx);
|
||||
});
|
||||
|
||||
// Replace all content
|
||||
textarea.update(cx, |input, cx| {
|
||||
input.replace("new content", window, cx);
|
||||
state.update(cx, |state, cx| {
|
||||
state.replace("new content", window, cx);
|
||||
});
|
||||
|
||||
// Set cursor position
|
||||
textarea.update(cx, |input, cx| {
|
||||
input.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
|
||||
state.update(cx, |state, cx| {
|
||||
state.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
|
||||
});
|
||||
|
||||
// Get cursor position
|
||||
let position = textarea.read(cx).cursor_position();
|
||||
let position = state.read(cx).cursor_position();
|
||||
println!("Line: {}, Column: {}", position.line, position.character);
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
```rust
|
||||
let textarea = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.multi_line(true)
|
||||
.validate(|text, _| {
|
||||
// Validate that content is not empty and under 1000 chars
|
||||
!text.trim().is_empty() && text.len() <= 1000
|
||||
})
|
||||
);
|
||||
|
||||
Input::new(&textarea)
|
||||
Input::new(&state)
|
||||
```
|
||||
|
||||
### Handle Events
|
||||
|
||||
```rust
|
||||
cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
|
||||
cx.subscribe_in(&state, window, |view, state, event, window, cx| {
|
||||
match event {
|
||||
InputEvent::Change => {
|
||||
let content = state.read(cx).value();
|
||||
|
|
@ -234,7 +213,7 @@ cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
|
|||
### Disabled State
|
||||
|
||||
```rust
|
||||
Input::new(&textarea)
|
||||
Input::new(&state)
|
||||
.disabled(true)
|
||||
.h(px(200.))
|
||||
```
|
||||
|
|
@ -243,7 +222,7 @@ Input::new(&textarea)
|
|||
|
||||
```rust
|
||||
// Without default appearance
|
||||
Input::new(&textarea)
|
||||
Input::new(&state)
|
||||
.appearance(false)
|
||||
.h(px(200.))
|
||||
|
||||
|
|
@ -255,80 +234,25 @@ div()
|
|||
.rounded_lg()
|
||||
.p_4()
|
||||
.child(
|
||||
Input::new(&textarea)
|
||||
Input::new(&state)
|
||||
.appearance(false)
|
||||
.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
|
||||
|
||||
### Comment Box
|
||||
|
||||
```rust
|
||||
struct CommentBox {
|
||||
textarea: Entity<InputState>,
|
||||
state: Entity<InputState>,
|
||||
char_limit: usize,
|
||||
}
|
||||
|
||||
impl CommentBox {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let textarea = cx.new(|cx|
|
||||
let state = cx.new(|cx|
|
||||
InputState::new(window, cx)
|
||||
.auto_grow(3, 8)
|
||||
.placeholder("Write your comment...")
|
||||
|
|
@ -336,7 +260,7 @@ impl CommentBox {
|
|||
);
|
||||
|
||||
Self {
|
||||
textarea,
|
||||
state,
|
||||
char_limit: 500,
|
||||
}
|
||||
}
|
||||
|
|
@ -344,13 +268,13 @@ impl CommentBox {
|
|||
|
||||
impl Render for CommentBox {
|
||||
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 remaining = self.char_limit.saturating_sub(char_count);
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Input::new(&self.textarea))
|
||||
.child(Input::new(&self.state))
|
||||
.child(
|
||||
h_flex()
|
||||
.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
|
||||
|
|
|
|||
Loading…
Reference in a new issue