input: Add CodeEditor mode. (#863)

## TODO

- [x] Cache highlight result.
- [x] Add a `code-editor` example with more languages test.
- [x] Support setup font to Input (Base on window scope text style).
- [x] Line number
- [x] Change font size and font family.
- [x] Enter newline to keep indent.
- [x] Height with flex_1
- [x] Add to support press `up`, `down` to move cursor to start or end
of the selection, if there is selected.
- [x] Add `Tab`, `Shift-Tab` to indent or outdent for cursor and
selection.
- [x] Double click to select at least 1 char.


https://github.com/user-attachments/assets/af74862d-15a9-4802-b4b4-2aeb606227ec
This commit is contained in:
Jason Lee 2025-05-20 15:51:00 +08:00 committed by GitHub
parent 67dd658635
commit 5658db42c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 952 additions and 171 deletions

View file

@ -0,0 +1,151 @@
use std::sync::LazyLock;
use gpui::*;
use gpui_component::{
checkbox::Checkbox,
dropdown::{Dropdown, DropdownEvent, DropdownState},
h_flex,
highlighter::{HighlightTheme, Highlighter},
input::{InputEvent, InputState, TabSize, TextInput},
v_flex, ActiveTheme as _,
};
use story::Assets;
static LIGHT_THEME: LazyLock<HighlightTheme> = LazyLock::new(|| HighlightTheme::default_light());
static DARK_THEME: LazyLock<HighlightTheme> = LazyLock::new(|| HighlightTheme::default_dark());
pub struct Example {
input_state: Entity<InputState>,
language_state: Entity<DropdownState<Vec<SharedString>>>,
language: SharedString,
is_dark: bool,
line_number: bool,
_subscribes: Vec<Subscription>,
}
const EXAMPLE: &str = include_str!("./code-editor.rs");
const LANGUAGES: [&str; 7] = ["rust", "javascript", "html", "css", "go", "python", "ruby"];
impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let default_language: SharedString = LANGUAGES[0].into();
let input_state = cx.new(|cx| {
InputState::new(window, cx)
.code_editor(Some(&default_language), &LIGHT_THEME)
.line_number(true)
.tab_size(TabSize {
tab_size: 4,
hard_tabs: false,
})
.default_value(EXAMPLE)
.placeholder("Enter your code here...")
});
let language_state = cx.new(|cx| {
DropdownState::new(
LANGUAGES.iter().map(|s| s.to_string().into()).collect(),
Some(0),
window,
cx,
)
});
let _subscribes = vec![
cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| {
cx.notify();
}),
cx.subscribe(
&language_state,
|this, state, _: &DropdownEvent<Vec<SharedString>>, cx| {
if let Some(val) = state.read(cx).selected_value() {
this.update_highlighter(Some(val.clone()), cx);
cx.notify();
}
},
),
];
Self {
input_state,
language_state,
language: default_language,
is_dark: false,
line_number: true,
_subscribes,
}
}
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn update_highlighter(&mut self, new_language: Option<SharedString>, cx: &mut Context<Self>) {
let is_dark = cx.theme().mode.is_dark();
let is_language_changed = new_language.is_some();
if new_language.is_some() {
self.language = new_language.unwrap();
}
let language = self.language.as_ref();
if self.is_dark != is_dark || is_language_changed {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
state.set_highlighter(Highlighter::new(Some(language), &DARK_THEME), cx);
} else {
state.set_highlighter(Highlighter::new(Some(language), &LIGHT_THEME), cx);
}
});
}
}
}
impl Render for Example {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.update_highlighter(None, cx);
v_flex()
.size_full()
.child(
h_flex()
.p_4()
.pb_0()
.gap_4()
.flex_shrink_0()
.items_center()
.justify_between()
.child(Dropdown::new(&self.language_state).title_prefix("Language: "))
.child(
Checkbox::new("line-numbger")
.checked(self.line_number)
.on_click(cx.listener(|this, checked: &bool, window, cx| {
this.line_number = *checked;
this.input_state.update(cx, |state, cx| {
state.set_line_number(this.line_number, window, cx);
});
cx.notify();
}))
.label("Line Number"),
),
)
.child(
div()
.id("source")
.w_full()
.flex_1()
.font_family("Menlo")
.p_4()
.text_size(px(13.))
.child(TextInput::new(&self.input_state).h_full()),
)
}
}
fn main() {
let app = Application::new().with_assets(Assets);
app.run(move |cx| {
story::init(cx);
cx.activate(true);
story::create_new_window("Code Editor", Example::view, cx);
});
}

View file

@ -1,24 +1,32 @@
use std::sync::LazyLock;
use gpui::*; use gpui::*;
use gpui_component::{ use gpui_component::{
h_flex, h_flex,
highlighter::{HighlightTheme, Highlighter},
input::{InputState, TextInput}, input::{InputState, TextInput},
text::TextView, text::TextView,
ActiveTheme as _, ActiveTheme as _,
}; };
use story::Assets; use story::Assets;
static LIGHT_THEME: LazyLock<HighlightTheme> = LazyLock::new(|| HighlightTheme::default_light());
static DARK_THEME: LazyLock<HighlightTheme> = LazyLock::new(|| HighlightTheme::default_dark());
pub struct Example { pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
is_dark: bool,
_subscribe: Subscription, _subscribe: Subscription,
} }
const EXAMPLE: &str = include_str!("./html.html"); const EXAMPLE: &str = include_str!("./html.html");
const LANG: &str = "html";
impl Example { impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let input_state = cx.new(|cx| { let input_state = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .code_editor(Some(LANG), &LIGHT_THEME)
.default_value(EXAMPLE) .default_value(EXAMPLE)
.placeholder("Enter your HTML here...") .placeholder("Enter your HTML here...")
}); });
@ -32,6 +40,7 @@ impl Example {
Self { Self {
input_state, input_state,
is_dark: false,
_subscribe, _subscribe,
} }
} }
@ -42,7 +51,19 @@ impl Example {
} }
impl Render for Example { impl Render for Example {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_dark = cx.theme().mode.is_dark();
if self.is_dark != is_dark {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
state.set_highlighter(Highlighter::new(Some(LANG), &DARK_THEME), cx);
} else {
state.set_highlighter(Highlighter::new(Some(LANG), &LIGHT_THEME), cx);
}
});
}
h_flex() h_flex()
.h_full() .h_full()
.child( .child(
@ -52,6 +73,8 @@ impl Render for Example {
.w_1_2() .w_1_2()
.border_r_1() .border_r_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.font_family("Menlo")
.text_size(px(13.))
.child(TextInput::new(&self.input_state).h_full().appearance(false)), .child(TextInput::new(&self.input_state).h_full().appearance(false)),
) )
.child( .child(

View file

@ -1,16 +1,21 @@
use std::rc::Rc; use std::{rc::Rc, sync::LazyLock};
use gpui::*; use gpui::*;
use gpui_component::{ use gpui_component::{
highlighter::HighlightTheme, highlighter::{HighlightTheme, Highlighter},
input::{InputState, TextInput}, input::{InputEvent, InputState, TextInput},
text::{TextView, TextViewStyle}, text::{TextView, TextViewStyle},
ActiveTheme as _, ActiveTheme as _,
}; };
use story::Assets; use story::Assets;
static LIGHT_THEME: LazyLock<HighlightTheme> = LazyLock::new(|| HighlightTheme::default_light());
static DARK_THEME: LazyLock<HighlightTheme> = LazyLock::new(|| HighlightTheme::default_dark());
const LANG: &str = "markdown";
pub struct Example { pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
is_dark: bool,
} }
const EXAMPLE: &str = include_str!("./markdown.md"); const EXAMPLE: &str = include_str!("./markdown.md");
@ -19,19 +24,19 @@ impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let input_state = cx.new(|cx| { let input_state = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .code_editor(Some(LANG), &LIGHT_THEME)
.placeholder("Enter your Markdown here...") .placeholder("Enter your Markdown here...")
.default_value(EXAMPLE) .default_value(EXAMPLE)
}); });
let _subscribe = cx.subscribe( let _subscribe = cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| {
&input_state, cx.notify();
|_, _, _: &gpui_component::input::InputEvent, cx| { });
cx.notify();
},
);
Self { input_state } Self {
input_state,
is_dark: false,
}
} }
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> { fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
@ -40,13 +45,25 @@ impl Example {
} }
impl Render for Example { impl Render for Example {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = if cx.theme().mode.is_dark() { let theme = if cx.theme().mode.is_dark() {
HighlightTheme::default_dark() HighlightTheme::default_dark()
} else { } else {
HighlightTheme::default_light() HighlightTheme::default_light()
}; };
let is_dark = cx.theme().mode.is_dark();
if self.is_dark != is_dark {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
state.set_highlighter(Highlighter::new(Some(LANG), &DARK_THEME), cx);
} else {
state.set_highlighter(Highlighter::new(Some(LANG), &LIGHT_THEME), cx);
}
});
}
div() div()
.flex() .flex()
.flex_row() .flex_row()

View file

@ -6,7 +6,7 @@ use std::{
use syntect::{highlighting, parsing}; use syntect::{highlighting, parsing};
static SYNTAXES: LazyLock<parsing::SyntaxSet> = static SYNTAXES: LazyLock<parsing::SyntaxSet> =
LazyLock::new(parsing::SyntaxSet::load_defaults_nonewlines); LazyLock::new(parsing::SyntaxSet::load_defaults_newlines);
static DEFAULT_LIGHT: LazyLock<Arc<highlighting::Theme>> = LazyLock::new(|| { static DEFAULT_LIGHT: LazyLock<Arc<highlighting::Theme>> = LazyLock::new(|| {
let mut cursor = std::io::Cursor::new(include_bytes!("./themes/light.tmTheme")); let mut cursor = std::io::Cursor::new(include_bytes!("./themes/light.tmTheme"));

View file

@ -1,16 +1,20 @@
use std::ops::Range;
use gpui::{ use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, fill, hash, point, px, relative, size, App, Bounds, Corners, Element, ElementId,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, ElementInputHandler, Entity, GlobalElementId, HighlightStyle, IntoElement, LayoutId,
Pixels, Point, SharedString, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine, MouseButton, MouseMoveEvent, PaintQuad, Path, Pixels, Point, SharedString, Style, TextAlign,
TextRun, UnderlineStyle, Window, WrappedLine,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{ActiveTheme as _, Root}; use crate::{ActiveTheme as _, Root};
use super::InputState; use super::{mode::InputMode, InputState};
const RIGHT_MARGIN: Pixels = px(5.); const RIGHT_MARGIN: Pixels = px(5.);
const BOTTOM_MARGIN_ROWS: usize = 1; const BOTTOM_MARGIN_ROWS: usize = 1;
const LINE_NUMBER_MARGIN_RIGHT: Pixels = px(10.);
pub(super) struct TextElement { pub(super) struct TextElement {
input: Entity<InputState>, input: Entity<InputState>,
@ -50,6 +54,7 @@ impl TextElement {
lines: &[WrappedLine], lines: &[WrappedLine],
line_height: Pixels, line_height: Pixels,
bounds: &mut Bounds<Pixels>, bounds: &mut Bounds<Pixels>,
line_number_width: Pixels,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (Option<PaintQuad>, Point<Pixels>) { ) -> (Option<PaintQuad>, Point<Pixels>) {
@ -162,7 +167,7 @@ impl TextElement {
cursor = Some(fill( cursor = Some(fill(
Bounds::new( Bounds::new(
point( point(
bounds.left() + cursor_pos.x, bounds.left() + cursor_pos.x + line_number_width,
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.), bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
), ),
size(px(1.), cursor_height), size(px(1.), cursor_height),
@ -180,6 +185,7 @@ impl TextElement {
lines: &[WrappedLine], lines: &[WrappedLine],
line_height: Pixels, line_height: Pixels,
bounds: &mut Bounds<Pixels>, bounds: &mut Bounds<Pixels>,
line_number_width: Pixels,
_: &mut Window, _: &mut Window,
cx: &mut App, cx: &mut App,
) -> Option<Path<Pixels>> { ) -> Option<Path<Pixels>> {
@ -293,19 +299,53 @@ impl TextElement {
// print_points_as_svg_path(&line_corners, &points); // print_points_as_svg_path(&line_corners, &points);
let path_origin = bounds.origin + point(line_number_width, px(0.));
let first_p = *points.get(0).unwrap(); let first_p = *points.get(0).unwrap();
let mut builder = gpui::PathBuilder::fill(); let mut builder = gpui::PathBuilder::fill();
builder.move_to(bounds.origin + first_p); builder.move_to(path_origin + first_p);
for p in points.iter().skip(1) { for p in points.iter().skip(1) {
builder.line_to(bounds.origin + *p); builder.line_to(path_origin + *p);
} }
builder.build().ok() builder.build().ok()
} }
fn highlight_text(&self, cx: &mut App) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
let input = self.input.read(cx);
let text = input.text.as_ref();
let cache_key = hash(&text);
match &input.mode {
InputMode::CodeEditor {
highlighter, cache, ..
} => {
if cache.0 == cache_key {
return Some(cache.1.clone());
}
if let Some(highlighter) = highlighter {
let styles = highlighter.highlight(&text);
self.input.update(cx, |input, _cx| {
input
.mode
.set_code_editor_cache((cache_key, styles.clone()));
});
Some(styles)
} else {
None
}
}
_ => None,
}
}
} }
pub(super) struct PrepaintState { pub(super) struct PrepaintState {
lines: SmallVec<[WrappedLine; 1]>, lines: SmallVec<[WrappedLine; 1]>,
line_numbers: Option<SmallVec<[WrappedLine; 1]>>,
line_number_width: Pixels,
cursor: Option<PaintQuad>, cursor: Option<PaintQuad>,
cursor_scroll_offset: Point<Pixels>, cursor_scroll_offset: Point<Pixels>,
selection_path: Option<Path<Pixels>>, selection_path: Option<Path<Pixels>>,
@ -392,12 +432,14 @@ impl Element for TextElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Self::PrepaintState { ) -> Self::PrepaintState {
let highlights = self.highlight_text(cx);
let multi_line = self.input.read(cx).is_multi_line(); let multi_line = self.input.read(cx).is_multi_line();
let line_height = window.line_height(); let line_height = window.line_height();
let input = self.input.read(cx); let input = self.input.read(cx);
let text = input.text.clone(); let text = input.text.clone();
let placeholder = self.placeholder.clone(); let placeholder = self.placeholder.clone();
let style = window.text_style(); let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size());
let mut bounds = bounds; let mut bounds = bounds;
let (display_text, text_color) = if text.is_empty() { let (display_text, text_color) = if text.is_empty() {
@ -411,6 +453,52 @@ impl Element for TextElement {
(text, cx.theme().foreground) (text, cx.theme().foreground)
}; };
let text_style = window.text_style();
// Calculate the width of the line numbers
let mut line_number_width = px(0.);
let line_numbers = if input.mode.line_number() {
let mut line_numbers = SmallVec::new();
let total_lines = input.text_wrapper.lines.len();
let run_len = if total_lines > 999 { 4 } else { 3 };
let runs = vec![TextRun {
len: run_len,
font: style.font(),
color: cx.theme().muted_foreground,
background_color: None,
underline: None,
strikethrough: None,
}];
for (i, line_wrap) in input.text_wrapper.lines.iter().enumerate() {
let line_no = if run_len == 4 {
format!("{:>4}", i + 1).into()
} else {
format!("{:>3}", i + 1).into()
};
let line = window
.text_system()
.shape_text(line_no, font_size, &runs, None, None)
.unwrap();
line_number_width = (line.last().unwrap().width() + LINE_NUMBER_MARGIN_RIGHT)
.max(line_number_width);
line_numbers.extend(line);
for _ in 0..line_wrap.wrap_lines {
// Empty line no for wrapped lines
let line = window
.text_system()
.shape_text(" ".into(), font_size, &runs, None, None)
.unwrap();
line_numbers.extend(line);
}
}
Some(line_numbers)
} else {
None
};
let run = TextRun { let run = TextRun {
len: display_text.len(), len: display_text.len(),
font: style.font(), font: style.font(),
@ -444,12 +532,25 @@ impl Element for TextElement {
.filter(|run| run.len > 0) .filter(|run| run.len > 0)
.collect() .collect()
} else { } else {
vec![run] if let Some(highlights) = highlights {
let mut runs = vec![];
for (range, style) in highlights {
let run = text_style
.clone()
.highlight(style)
.to_run(range.end - range.start);
if run.len > 0 {
runs.push(run);
}
}
runs
} else {
vec![run]
}
}; };
let font_size = style.font_size.to_pixels(window.rem_size());
let wrap_width = if multi_line { let wrap_width = if multi_line {
Some(bounds.size.width - RIGHT_MARGIN) Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
} else { } else {
None None
}; };
@ -490,14 +591,29 @@ impl Element for TextElement {
// Calculate the scroll offset to keep the cursor in view // Calculate the scroll offset to keep the cursor in view
let (cursor, cursor_scroll_offset) = let (cursor, cursor_scroll_offset) = self.layout_cursor(
self.layout_cursor(&lines, line_height, &mut bounds, window, cx); &lines,
line_height,
&mut bounds,
line_number_width,
window,
cx,
);
let selection_path = self.layout_selections(&lines, line_height, &mut bounds, window, cx); let selection_path = self.layout_selections(
&lines,
line_height,
&mut bounds,
line_number_width,
window,
cx,
);
PrepaintState { PrepaintState {
bounds, bounds,
lines, lines,
line_numbers,
line_number_width,
cursor, cursor,
cursor_scroll_offset, cursor_scroll_offset,
selection_path, selection_path,
@ -566,8 +682,19 @@ impl Element for TextElement {
offset_y = px(2.5); offset_y = px(2.5);
} }
} }
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
for line in line_numbers.iter() {
let p = point(origin.x, origin.y + offset_y);
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
let line_size = line.size(line_height);
offset_y += line_size.height;
}
}
let mut offset_y = px(0.);
for line in prepaint.lines.iter() { for line in prepaint.lines.iter() {
let p = point(origin.x, origin.y + offset_y); let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y);
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx); _ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
offset_y += line.size(line_height).height; offset_y += line.size(line_height).height;
} }
@ -595,6 +722,7 @@ impl Element for TextElement {
input.set_input_bounds(input_bounds, cx); input.set_input_bounds(input_bounds, cx);
input.last_selected_range = Some(selected_range); input.last_selected_range = Some(selected_range);
input.scroll_size = scroll_size; input.scroll_size = scroll_size;
input.line_number_width = prepaint.line_number_width;
input input
.scroll_handle .scroll_handle
.set_offset(prepaint.cursor_scroll_offset); .set_offset(prepaint.cursor_scroll_offset);

View file

@ -3,6 +3,7 @@ mod change;
mod clear_button; mod clear_button;
mod element; mod element;
mod mask_pattern; mod mask_pattern;
mod mode;
mod number_input; mod number_input;
mod otp_input; mod otp_input;
mod state; mod state;
@ -11,6 +12,7 @@ mod text_wrapper;
pub(crate) use clear_button::*; pub(crate) use clear_button::*;
pub use mask_pattern::MaskPattern; pub use mask_pattern::MaskPattern;
pub use mode::TabSize;
pub use number_input::{NumberInput, NumberInputEvent, StepAction}; pub use number_input::{NumberInput, NumberInputEvent, StepAction};
pub use otp_input::*; pub use otp_input::*;
pub use state::*; pub use state::*;

194
crates/ui/src/input/mode.rs Normal file
View file

@ -0,0 +1,194 @@
use gpui::{DefiniteLength, HighlightStyle, SharedString};
use std::ops::Range;
use std::rc::Rc;
use super::text_wrapper::TextWrapper;
use crate::highlighter::Highlighter;
#[derive(Debug, Copy, Clone)]
pub struct TabSize {
/// Default is 2
pub tab_size: usize,
/// Set true to use `\t` as tab indent, default is false
pub hard_tabs: bool,
}
impl Default for TabSize {
fn default() -> Self {
Self {
tab_size: 2,
hard_tabs: false,
}
}
}
impl TabSize {
pub(super) fn to_string(&self) -> SharedString {
if self.hard_tabs {
"\t".into()
} else {
" ".repeat(self.tab_size).into()
}
}
}
#[derive(Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
tab: TabSize,
rows: usize,
height: Option<DefiniteLength>,
},
CodeEditor {
tab: TabSize,
rows: usize,
height: Option<DefiniteLength>,
/// Show line number
line_number: bool,
highlighter: Option<Rc<Highlighter<'static>>>,
cache: (u64, Vec<(Range<usize>, HighlightStyle)>),
},
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
}
impl InputMode {
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
*rows = new_rows;
}
InputMode::CodeEditor { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow {
rows,
min_rows,
max_rows,
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
pub(super) fn set_height(&mut self, new_height: Option<DefiniteLength>) {
match self {
InputMode::MultiLine { height, .. } => {
*height = new_height;
}
InputMode::CodeEditor { height, .. } => {
*height = new_height;
}
_ => {}
}
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
let wrapped_lines = text_wrapper.wrapped_lines.len();
self.set_rows(wrapped_lines);
}
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::CodeEditor { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
/// At least 1 row be return.
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
.max(1)
}
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
}
}
pub(super) fn height(&self) -> Option<DefiniteLength> {
match self {
InputMode::MultiLine { height, .. } => *height,
InputMode::CodeEditor { height, .. } => *height,
_ => None,
}
}
pub(super) fn set_code_editor_cache(
&mut self,
cache: (u64, Vec<(Range<usize>, HighlightStyle)>),
) {
if let InputMode::CodeEditor { cache: c, .. } = self {
*c = cache;
}
}
/// Return false if the mode is not [`InputMode::CodeEditor`].
#[allow(unused)]
#[inline]
pub(super) fn line_number(&self) -> bool {
match self {
InputMode::CodeEditor { line_number, .. } => *line_number,
_ => false,
}
}
#[inline]
pub(super) fn tab_size(&self) -> Option<&TabSize> {
match self {
InputMode::MultiLine { tab, .. } => Some(tab),
InputMode::CodeEditor { tab, .. } => Some(tab),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::TabSize;
#[test]
fn test_tab_size() {
let tab = TabSize {
tab_size: 2,
hard_tabs: false,
};
assert_eq!(tab.to_string(), " ");
let tab = TabSize {
tab_size: 4,
hard_tabs: false,
};
assert_eq!(tab.to_string(), " ");
let tab = TabSize {
tab_size: 2,
hard_tabs: true,
};
assert_eq!(tab.to_string(), "\t");
let tab = TabSize {
tab_size: 4,
hard_tabs: true,
};
assert_eq!(tab.to_string(), "\t");
}
}

View file

@ -11,8 +11,8 @@ use std::rc::Rc;
use unicode_segmentation::*; use unicode_segmentation::*;
use gpui::{ use gpui::{
actions, div, impl_internal_actions, point, prelude::FluentBuilder as _, px, App, AppContext, actions, div, impl_internal_actions, point, prelude::FluentBuilder as _, px, relative, App,
Bounds, ClipboardItem, Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter, AppContext, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter,
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription, Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
@ -23,10 +23,20 @@ use gpui::{
// - Move cursor to skip line eof empty chars. // - Move cursor to skip line eof empty chars.
use super::{ use super::{
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern, blink_cursor::BlinkCursor,
number_input, text_wrapper::TextWrapper, change::Change,
element::TextElement,
mask_pattern::MaskPattern,
mode::{InputMode, TabSize},
number_input,
text_wrapper::TextWrapper,
};
use crate::{
highlighter::{HighlightTheme, Highlighter},
history::History,
scroll::ScrollbarState,
Root,
}; };
use crate::{history::History, scroll::ScrollbarState, Root};
#[derive(Clone, PartialEq, Eq, Deserialize)] #[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct Enter { pub struct Enter {
@ -45,6 +55,8 @@ actions!(
DeleteToEndOfLine, DeleteToEndOfLine,
DeleteToPreviousWordStart, DeleteToPreviousWordStart,
DeleteToNextWordEnd, DeleteToNextWordEnd,
Indent,
Outdent,
Up, Up,
Down, Down,
Left, Left,
@ -112,6 +124,8 @@ pub fn init(cx: &mut App) {
KeyBinding::new("down", Down, Some(CONTEXT)), KeyBinding::new("down", Down, Some(CONTEXT)),
KeyBinding::new("left", Left, Some(CONTEXT)), KeyBinding::new("left", Left, Some(CONTEXT)),
KeyBinding::new("right", Right, Some(CONTEXT)), KeyBinding::new("right", Right, Some(CONTEXT)),
KeyBinding::new("tab", Indent, Some(CONTEXT)),
KeyBinding::new("shift-tab", Outdent, Some(CONTEXT)),
KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)), KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)), KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)), KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
@ -191,95 +205,6 @@ pub fn init(cx: &mut App) {
number_input::init(cx); number_input::init(cx);
} }
#[derive(Debug, Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
rows: usize,
height: Option<DefiniteLength>,
},
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
}
impl InputMode {
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow {
rows,
min_rows,
max_rows,
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
match self {
Self::AutoGrow { .. } => {
let wrapped_lines = text_wrapper.wrapped_lines.len();
self.set_rows(wrapped_lines);
}
_ => {}
}
}
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
/// At least 1 row be return.
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
.max(1)
}
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
}
}
pub(super) fn set_height(&mut self, new_height: Option<DefiniteLength>) {
match self {
InputMode::MultiLine { height, .. } => {
*height = new_height;
}
_ => {}
}
}
pub(super) fn height(&self) -> Option<DefiniteLength> {
match self {
InputMode::MultiLine { height, .. } => *height,
_ => None,
}
}
}
/// InputState to keep editing state of the [`super::TextInput`]. /// InputState to keep editing state of the [`super::TextInput`].
pub struct InputState { pub struct InputState {
pub(super) focus_handle: FocusHandle, pub(super) focus_handle: FocusHandle,
@ -317,6 +242,7 @@ pub struct InputState {
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>, pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
/// The size of the scrollable content. /// The size of the scrollable content.
pub(crate) scroll_size: gpui::Size<Pixels>, pub(crate) scroll_size: gpui::Size<Pixels>,
pub(crate) line_number_width: Pixels,
/// The mask pattern for formatting the input text /// The mask pattern for formatting the input text
pub(crate) mask_pattern: MaskPattern, pub(crate) mask_pattern: MaskPattern,
@ -390,6 +316,7 @@ impl InputState {
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())), scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_size: gpui::size(px(0.), px(0.)), scroll_size: gpui::size(px(0.), px(0.)),
preferred_x_offset: None, preferred_x_offset: None,
line_number_width: px(0.),
placeholder: SharedString::default(), placeholder: SharedString::default(),
mask_pattern: MaskPattern::default(), mask_pattern: MaskPattern::default(),
_subscriptions, _subscriptions,
@ -403,6 +330,7 @@ impl InputState {
self.mode = InputMode::MultiLine { self.mode = InputMode::MultiLine {
rows: 2, rows: 2,
height: None, height: None,
tab: TabSize::default(),
}; };
self self
} }
@ -417,12 +345,105 @@ impl InputState {
self self
} }
/// Set Input to use [`InputMode::CodeEditor`] mode.
///
/// Default options:
///
/// - line_number: true
/// - tab_size: 2
/// - hard_tabs: false
/// - height: full
///
/// Code Editor aim for help used to simple code editing or display, not a full-featured code editor.
///
/// ## Features
///
/// - Syntax Highlighting
/// - Auto Indent
/// - Line Number
pub fn code_editor(mut self, language: Option<&str>, theme: &'static HighlightTheme) -> Self {
let highlighter = Highlighter::new(language, theme);
self.mode = InputMode::CodeEditor {
rows: 2,
tab: TabSize::default(),
highlighter: Some(Rc::new(highlighter)),
cache: (0, vec![]),
line_number: true,
height: Some(relative(1.)),
};
self
}
/// Set placeholder /// Set placeholder
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self { pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into(); self.placeholder = placeholder.into();
self self
} }
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
pub fn line_number(mut self, line_number: bool) -> Self {
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
self
}
/// Set line number, only for [`InputMode::CodeEditor`] mode.
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
cx.notify();
}
/// Set the tab size for the input.
///
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
pub fn tab_size(mut self, tab: TabSize) -> Self {
match &mut self.mode {
InputMode::MultiLine { tab: t, .. } => *t = tab,
InputMode::CodeEditor { tab: t, .. } => *t = tab,
_ => {}
}
self
}
/// Set the number of rows for the multi-line Textarea.
///
/// This is only used when `multi_line` is set to true.
///
/// default: 2
pub fn rows(mut self, rows: usize) -> Self {
match &mut self.mode {
InputMode::MultiLine { rows: r, .. } => *r = rows,
InputMode::AutoGrow {
max_rows: max_r,
rows: r,
..
} => {
*r = rows;
*max_r = rows;
}
_ => {}
}
self
}
/// Set highlighter, only for [`InputMode::CodeEditor`] mode.
pub fn set_highlighter(&mut self, highlighter: Highlighter<'static>, cx: &mut Context<Self>) {
let new_highlighter = Rc::new(highlighter);
match &mut self.mode {
InputMode::CodeEditor {
highlighter, cache, ..
} => {
*highlighter = Some(new_highlighter);
*cache = (0, vec![]);
}
_ => {}
}
cx.notify();
}
/// Set placeholder /// Set placeholder
pub fn set_placeholder( pub fn set_placeholder(
&mut self, &mut self,
@ -565,7 +586,7 @@ impl InputState {
pub(super) fn is_multi_line(&self) -> bool { pub(super) fn is_multi_line(&self) -> bool {
matches!( matches!(
self.mode, self.mode,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
) )
} }
@ -579,28 +600,6 @@ impl InputState {
matches!(self.mode, InputMode::AutoGrow { .. }) matches!(self.mode, InputMode::AutoGrow { .. })
} }
/// Set the number of rows for the multi-line Textarea.
///
/// This is only used when `multi_line` is set to true.
///
/// default: 2
pub fn rows(mut self, rows: usize) -> Self {
match self.mode {
InputMode::MultiLine { height, .. } => {
self.mode = InputMode::MultiLine { rows, height };
}
InputMode::AutoGrow { max_rows, .. } => {
self.mode = InputMode::AutoGrow {
rows,
min_rows: rows,
max_rows,
};
}
_ => {}
}
self
}
/// Set the text of the input field. /// Set the text of the input field.
/// ///
/// And the selection_range will be reset to 0..0. /// And the selection_range will be reset to 0..0.
@ -714,6 +713,7 @@ impl InputState {
/// Set the default value of the input field. /// Set the default value of the input field.
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self { pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
self.text = value.into(); self.text = value.into();
self.text_wrapper.text = self.text.clone();
self self
} }
@ -758,6 +758,10 @@ impl InputState {
if self.is_single_line() { if self.is_single_line() {
return; return;
} }
if !self.selected_range.is_empty() {
self.move_to(self.selected_range.start.saturating_sub(1), window, cx);
}
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
self.move_vertical(-1, window, cx); self.move_vertical(-1, window, cx);
} }
@ -766,6 +770,11 @@ impl InputState {
if self.is_single_line() { if self.is_single_line() {
return; return;
} }
if !self.selected_range.is_empty() {
self.move_to(self.selected_range.end.saturating_sub(1), window, cx);
}
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
self.move_vertical(1, window, cx); self.move_vertical(1, window, cx);
} }
@ -966,6 +975,24 @@ impl InputState {
line line
} }
/// Get start line of selection start or end (The min value).
///
/// This is means is always get the first line of selection.
fn start_of_line_of_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
if self.is_single_line() {
return 0;
}
let offset = self.previous_boundary(self.selected_range.start.min(self.selected_range.end));
let line = self
.text_for_range(self.range_to_utf16(&(0..offset + 1)), &mut None, window, cx)
.unwrap_or_default()
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
line
}
/// Get end of line /// Get end of line
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize { fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
if self.is_single_line() { if self.is_single_line() {
@ -1001,6 +1028,49 @@ impl InputState {
line line
} }
/// Get indent string of next line.
///
/// To get current and next line indent, to return more depth one.
pub(super) fn indent_of_next_line(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> String {
if self.is_single_line() {
return "".into();
}
let mut current_indent = String::new();
let mut next_indent = String::new();
let current_line_start_pos = self.start_of_line(window, cx);
let next_line_start_pos = self.end_of_line(window, cx);
for c in self.text.chars().skip(current_line_start_pos) {
if !c.is_whitespace() {
break;
}
if c == '\n' || c == '\r' {
break;
}
current_indent.push(c);
}
for c in self.text.chars().skip(next_line_start_pos) {
if !c.is_whitespace() {
break;
}
if c == '\n' || c == '\r' {
break;
}
next_indent.push(c);
}
if next_indent.len() > current_indent.len() {
return next_indent;
} else {
return current_indent;
}
}
pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() { if self.selected_range.is_empty() {
self.select_to(self.previous_boundary(self.cursor_offset()), window, cx) self.select_to(self.previous_boundary(self.cursor_offset()), window, cx)
@ -1091,6 +1161,9 @@ impl InputState {
pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
if self.is_multi_line() { if self.is_multi_line() {
let is_eof = self.selected_range.end == self.text.len(); let is_eof = self.selected_range.end == self.text.len();
// Get current line indent
let indent = self.indent_of_next_line(window, cx);
self.replace_text_in_range(None, "\n", window, cx); self.replace_text_in_range(None, "\n", window, cx);
// Move cursor to the start of the next line // Move cursor to the start of the next line
@ -1099,6 +1172,14 @@ impl InputState {
new_offset += 1; new_offset += 1;
} }
self.move_to(new_offset, window, cx); self.move_to(new_offset, window, cx);
// Add indent
self.replace_text_in_range(
Some(self.range_to_utf16(&(self.cursor_offset()..self.cursor_offset()))),
&indent,
window,
cx,
);
} }
cx.emit(InputEvent::PressEnter { cx.emit(InputEvent::PressEnter {
@ -1106,6 +1187,125 @@ impl InputState {
}); });
} }
pub(super) fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
let Some(tab_size) = self.mode.tab_size() else {
return;
};
let tab_indent = tab_size.to_string();
let selected_range = self.selected_range.clone();
let mut added_len = 0;
if !self.selected_range.is_empty() {
let mut offset = self.start_of_line_of_selection(window, cx);
let selected_text = self
.text_for_range(
self.range_to_utf16(&(offset..selected_range.end)),
&mut None,
window,
cx,
)
.unwrap_or("".into());
let mut lines_count = 0;
for line in selected_text.lines() {
lines_count += 1;
self.replace_text_in_range(
Some(self.range_to_utf16(&(offset..offset))),
&tab_indent,
window,
cx,
);
added_len += tab_indent.len();
// +1 for "\n"
offset += line.len() + tab_indent.len() + 1;
}
if lines_count > 1 {
self.selected_range =
selected_range.start + tab_indent.len()..selected_range.end + added_len;
} else {
self.selected_range =
selected_range.start + added_len..selected_range.end + added_len;
}
} else {
// Selected none
let offset = self.selected_range.start;
self.replace_text_in_range(
Some(self.range_to_utf16(&(offset..offset))),
&tab_indent,
window,
cx,
);
added_len = tab_indent.len();
self.selected_range = selected_range.start + added_len..selected_range.end + added_len;
}
}
pub(super) fn outdent(&mut self, _: &Outdent, window: &mut Window, cx: &mut Context<Self>) {
let Some(tab_size) = self.mode.tab_size() else {
return;
};
let tab_indent = tab_size.to_string();
let selected_range = self.selected_range.clone();
let mut removed_len = 0;
if !self.selected_range.is_empty() {
let mut offset = self.start_of_line_of_selection(window, cx);
let selected_text = self
.text_for_range(
self.range_to_utf16(&(offset..selected_range.end)),
&mut None,
window,
cx,
)
.unwrap_or("".into());
let mut lines_count = 0;
for line in selected_text.lines() {
lines_count += 1;
if line.starts_with(tab_indent.as_ref()) {
self.replace_text_in_range(
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
"",
window,
cx,
);
removed_len += tab_indent.len();
}
// +1 for "\n"
offset += line.len().saturating_sub(tab_indent.len()) + 1;
}
if lines_count > 1 {
self.selected_range = selected_range.start.saturating_sub(tab_indent.len())
..selected_range.end.saturating_sub(removed_len);
} else {
self.selected_range = selected_range.start.saturating_sub(tab_indent.len())
..selected_range.end.saturating_sub(tab_indent.len());
}
} else {
// Selected none
let offset = self.start_of_line_of_selection(window, cx);
if self.text[offset..].starts_with(tab_indent.as_ref()) {
self.replace_text_in_range(
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
"",
window,
cx,
);
removed_len = tab_indent.len();
self.selected_range = selected_range.start.saturating_sub(removed_len)
..selected_range.end.saturating_sub(removed_len);
}
}
}
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.replace_text("", window, cx); self.replace_text("", window, cx);
} }
@ -1312,7 +1512,7 @@ impl InputState {
// //
// - included the input padding. // - included the input padding.
// - included the scroll offset. // - included the scroll offset.
let inner_position = position - bounds.origin; let inner_position = position - bounds.origin - point(self.line_number_width, px(0.));
let mut index = 0; let mut index = 0;
let mut y_offset = px(0.); let mut y_offset = px(0.);
@ -1418,40 +1618,66 @@ impl InputState {
/// Select the word at the given offset. /// Select the word at the given offset.
/// ///
/// The offset is the UTF-8 offset. /// The offset is the UTF-8 offset.
///
/// FIXME: When click on a non-word character, the word is not selected.
fn select_word(&mut self, offset: usize, window: &mut Window, cx: &mut Context<Self>) { fn select_word(&mut self, offset: usize, window: &mut Window, cx: &mut Context<Self>) {
#[inline(always)]
fn is_word(c: char) -> bool { fn is_word(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '_') c.is_alphanumeric() || matches!(c, '_')
} }
let mut start = self.offset_to_utf16(offset); let mut start = offset;
let mut end = start; let mut end = start;
let prev_text = self let prev_text = self
.text_for_range(0..start, &mut None, window, cx) .text_for_range(self.range_to_utf16(&(0..start + 1)), &mut None, window, cx)
.unwrap_or_default(); .unwrap_or_default();
let next_text = self let next_text = self
.text_for_range(end..self.text.len(), &mut None, window, cx) .text_for_range(
self.range_to_utf16(&(end..self.text.len())),
&mut None,
window,
cx,
)
.unwrap_or_default(); .unwrap_or_default();
let prev_chars = prev_text.chars().rev().peekable(); let prev_chars = prev_text.chars().rev();
let next_chars = next_text.chars().peekable(); let next_chars = next_text.chars();
let mut last_char_len = 0;
for (_, c) in prev_chars.enumerate() { for (_, c) in prev_chars.enumerate() {
if !is_word(c) { if !is_word(c) {
break; break;
} }
start -= c.len_utf16(); last_char_len = c.len_utf8();
start = start.saturating_sub(last_char_len);
} }
start += last_char_len;
for (_, c) in next_chars.enumerate() { for (_, c) in next_chars.enumerate() {
if !is_word(c) { if !is_word(c) {
break; break;
} }
end += c.len_utf16(); end += c.len_utf8();
} }
self.selected_range = self.range_from_utf16(&(start..end)); // Ensure at least one character is selected
if start == end {
end = end + 1;
// Avoid select empty range
match self.text.get(start..end) {
None => return,
Some(part) => {
if part.trim().len() == 0 {
return;
}
}
}
}
self.selected_range = start..end;
self.selected_word_range = Some(self.selected_range.clone()); self.selected_word_range = Some(self.selected_range.clone());
cx.notify() cx.notify()
} }
@ -1711,7 +1937,7 @@ impl EntityInputHandler for InputState {
self.push_history(&range, &new_text, window, cx); self.push_history(&range, &new_text, window, cx);
self.text = mask_text; self.text = mask_text;
self.text_wrapper.update(self.text.clone(), cx); self.text_wrapper.update(self.text.clone(), false, cx);
self.selected_range = new_pos..new_pos; self.selected_range = new_pos..new_pos;
self.marked_range.take(); self.marked_range.take();
self.update_preferred_x_offset(cx); self.update_preferred_x_offset(cx);
@ -1832,6 +2058,8 @@ impl Focusable for InputState {
impl Render for InputState { impl Render for InputState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.text_wrapper.update(self.text.clone(), false, cx);
div() div()
.id("text-element") .id("text-element")
.flex_1() .flex_1()

View file

@ -134,9 +134,12 @@ impl TextInput {
impl RenderOnce for TextInput { impl RenderOnce for TextInput {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
const LINE_HEIGHT: Rems = Rems(1.25); const LINE_HEIGHT: Rems = Rems(1.25);
let font = window.text_style().font();
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
self.state.update(cx, |state, _| { self.state.update(cx, |state, cx| {
state.mode.set_height(self.height); state.mode.set_height(self.height);
state.text_wrapper.set_font(font, font_size, cx);
state.disabled = self.disabled; state.disabled = self.disabled;
}); });
@ -187,6 +190,8 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::down)) .on_action(window.listener_for(&self.state, InputState::down))
.on_action(window.listener_for(&self.state, InputState::select_up)) .on_action(window.listener_for(&self.state, InputState::select_up))
.on_action(window.listener_for(&self.state, InputState::select_down)) .on_action(window.listener_for(&self.state, InputState::select_down))
.on_action(window.listener_for(&self.state, InputState::indent))
.on_action(window.listener_for(&self.state, InputState::outdent))
}) })
.on_action(window.listener_for(&self.state, InputState::select_all)) .on_action(window.listener_for(&self.state, InputState::select_all))
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line)) .on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
@ -222,6 +227,7 @@ impl RenderOnce for TextInput {
.input_py(self.size) .input_py(self.size)
.input_h(self.size) .input_h(self.size)
.cursor_text() .cursor_text()
.text_size(font_size)
.when(state.is_multi_line(), |this| { .when(state.is_multi_line(), |this| {
this.h_auto() this.h_auto()
.when_some(self.height, |this, height| this.h(height)) .when_some(self.height, |this, height| this.h(height))
@ -239,7 +245,6 @@ impl RenderOnce for TextInput {
.items_center() .items_center()
.gap(gap_x) .gap(gap_x)
.children(prefix) .children(prefix)
// TODO: Define height here, and use it in the input element
.child(self.state.clone()) .child(self.state.clone())
.child( .child(
h_flex() h_flex()

View file

@ -2,6 +2,14 @@ use std::ops::Range;
use gpui::{App, Font, LineFragment, Pixels, SharedString}; use gpui::{App, Font, LineFragment, Pixels, SharedString};
#[allow(unused)]
pub(super) struct LineWrap {
/// The number of soft wrapped lines of this line (Not include first line.)
pub(super) wrap_lines: usize,
/// The range of the line text in the entire text.
pub(super) range: Range<usize>,
}
/// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea /// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea
/// ///
/// After use lines to calculate the scroll size of the TextArea /// After use lines to calculate the scroll size of the TextArea
@ -9,6 +17,8 @@ pub(super) struct TextWrapper {
pub(super) text: SharedString, pub(super) text: SharedString,
/// The wrapped lines, value is start and end index of the line (by split \n). /// The wrapped lines, value is start and end index of the line (by split \n).
pub(super) wrapped_lines: Vec<Range<usize>>, pub(super) wrapped_lines: Vec<Range<usize>>,
/// The lines by split \n
pub(super) lines: Vec<LineWrap>,
pub(super) font: Font, pub(super) font: Font,
pub(super) font_size: Pixels, pub(super) font_size: Pixels,
/// If is none, it means the text is not wrapped /// If is none, it means the text is not wrapped
@ -24,49 +34,72 @@ impl TextWrapper {
font_size, font_size,
wrap_width, wrap_width,
wrapped_lines: Vec::new(), wrapped_lines: Vec::new(),
lines: Vec::new(),
} }
} }
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) { pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
if self.wrap_width == wrap_width { self.wrap_width = wrap_width;
self.update(self.text.clone(), true, cx);
}
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
self.font = font;
self.font_size = font_size;
self.update(self.text.clone(), true, cx);
}
/// Update the text wrapper and recalculate the wrapped lines.
///
/// If the `text` is the same as the current text, do nothing.
pub(super) fn update(&mut self, text: SharedString, force: bool, cx: &mut App) {
if self.text == text && !force {
return; return;
} }
self.wrap_width = wrap_width;
self.update(self.text.clone(), cx);
}
pub(super) fn set_font(&mut self, font: Font, cx: &mut App) {
self.font = font;
self.update(self.text.clone(), cx);
}
pub(super) fn update(&mut self, text: SharedString, cx: &mut App) {
let mut wrapped_lines = vec![]; let mut wrapped_lines = vec![];
let mut lines = vec![];
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX); let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
let mut line_wrapper = cx let mut line_wrapper = cx
.text_system() .text_system()
.line_wrapper(self.font.clone(), self.font_size); .line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0;
for line in text.lines() { for line in text.lines() {
let mut line_wraps = vec![];
let mut prev_boundary_ix = 0; let mut prev_boundary_ix = 0;
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
wrapped_lines.push(prev_boundary_ix..boundary.ix); line_wraps.push(prev_boundary_ix..boundary.ix);
prev_boundary_ix = boundary.ix; prev_boundary_ix = boundary.ix;
} }
lines.push(LineWrap {
wrap_lines: line_wraps.len(),
range: prev_line_ix..line.len(),
});
wrapped_lines.extend(line_wraps);
// Reset of the line // Reset of the line
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 { if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
wrapped_lines.push(prev_boundary_ix..line.len()); wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
} }
prev_line_ix += line.len();
} }
// Add last empty line. // Add last empty line.
if text.chars().last().unwrap_or('\n') == '\n' { if text.chars().last().unwrap_or('\n') == '\n' {
wrapped_lines.push(text.len()..text.len()); wrapped_lines.push(text.len()..text.len());
lines.push(LineWrap {
wrap_lines: 0,
range: text.len()..text.len(),
});
} }
self.text = text; self.text = text;
self.wrapped_lines = wrapped_lines; self.wrapped_lines = wrapped_lines;
self.lines = lines;
} }
} }