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:
parent
67dd658635
commit
5658db42c7
10 changed files with 952 additions and 171 deletions
151
crates/story/examples/code-editor.rs
Normal file
151
crates/story/examples/code-editor.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,24 +1,32 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use gpui::*;
|
||||
use gpui_component::{
|
||||
h_flex,
|
||||
highlighter::{HighlightTheme, Highlighter},
|
||||
input::{InputState, TextInput},
|
||||
text::TextView,
|
||||
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>,
|
||||
is_dark: bool,
|
||||
_subscribe: Subscription,
|
||||
}
|
||||
|
||||
const EXAMPLE: &str = include_str!("./html.html");
|
||||
const LANG: &str = "html";
|
||||
|
||||
impl Example {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input_state = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.code_editor(Some(LANG), &LIGHT_THEME)
|
||||
.default_value(EXAMPLE)
|
||||
.placeholder("Enter your HTML here...")
|
||||
});
|
||||
|
|
@ -32,6 +40,7 @@ impl Example {
|
|||
|
||||
Self {
|
||||
input_state,
|
||||
is_dark: false,
|
||||
_subscribe,
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +51,19 @@ impl 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_full()
|
||||
.child(
|
||||
|
|
@ -52,6 +73,8 @@ impl Render for Example {
|
|||
.w_1_2()
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().border)
|
||||
.font_family("Menlo")
|
||||
.text_size(px(13.))
|
||||
.child(TextInput::new(&self.input_state).h_full().appearance(false)),
|
||||
)
|
||||
.child(
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
use std::rc::Rc;
|
||||
use std::{rc::Rc, sync::LazyLock};
|
||||
|
||||
use gpui::*;
|
||||
use gpui_component::{
|
||||
highlighter::HighlightTheme,
|
||||
input::{InputState, TextInput},
|
||||
highlighter::{HighlightTheme, Highlighter},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
text::{TextView, TextViewStyle},
|
||||
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());
|
||||
const LANG: &str = "markdown";
|
||||
|
||||
pub struct Example {
|
||||
input_state: Entity<InputState>,
|
||||
is_dark: bool,
|
||||
}
|
||||
|
||||
const EXAMPLE: &str = include_str!("./markdown.md");
|
||||
|
|
@ -19,19 +24,19 @@ impl Example {
|
|||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input_state = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.code_editor(Some(LANG), &LIGHT_THEME)
|
||||
.placeholder("Enter your Markdown here...")
|
||||
.default_value(EXAMPLE)
|
||||
});
|
||||
|
||||
let _subscribe = cx.subscribe(
|
||||
&input_state,
|
||||
|_, _, _: &gpui_component::input::InputEvent, cx| {
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
let _subscribe = cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self { input_state }
|
||||
Self {
|
||||
input_state,
|
||||
is_dark: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
|
|
@ -40,13 +45,25 @@ impl 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() {
|
||||
HighlightTheme::default_dark()
|
||||
} else {
|
||||
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()
|
||||
.flex()
|
||||
.flex_row()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
use syntect::{highlighting, parsing};
|
||||
|
||||
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(|| {
|
||||
let mut cursor = std::io::Cursor::new(include_bytes!("./themes/light.tmTheme"));
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
||||
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path,
|
||||
Pixels, Point, SharedString, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine,
|
||||
fill, hash, point, px, relative, size, App, Bounds, Corners, Element, ElementId,
|
||||
ElementInputHandler, Entity, GlobalElementId, HighlightStyle, IntoElement, LayoutId,
|
||||
MouseButton, MouseMoveEvent, PaintQuad, Path, Pixels, Point, SharedString, Style, TextAlign,
|
||||
TextRun, UnderlineStyle, Window, WrappedLine,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{ActiveTheme as _, Root};
|
||||
|
||||
use super::InputState;
|
||||
use super::{mode::InputMode, InputState};
|
||||
|
||||
const RIGHT_MARGIN: Pixels = px(5.);
|
||||
const BOTTOM_MARGIN_ROWS: usize = 1;
|
||||
const LINE_NUMBER_MARGIN_RIGHT: Pixels = px(10.);
|
||||
|
||||
pub(super) struct TextElement {
|
||||
input: Entity<InputState>,
|
||||
|
|
@ -50,6 +54,7 @@ impl TextElement {
|
|||
lines: &[WrappedLine],
|
||||
line_height: Pixels,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
line_number_width: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (Option<PaintQuad>, Point<Pixels>) {
|
||||
|
|
@ -162,7 +167,7 @@ impl TextElement {
|
|||
cursor = Some(fill(
|
||||
Bounds::new(
|
||||
point(
|
||||
bounds.left() + cursor_pos.x,
|
||||
bounds.left() + cursor_pos.x + line_number_width,
|
||||
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
|
||||
),
|
||||
size(px(1.), cursor_height),
|
||||
|
|
@ -180,6 +185,7 @@ impl TextElement {
|
|||
lines: &[WrappedLine],
|
||||
line_height: Pixels,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
line_number_width: Pixels,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Path<Pixels>> {
|
||||
|
|
@ -293,19 +299,53 @@ impl TextElement {
|
|||
|
||||
// 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 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) {
|
||||
builder.line_to(bounds.origin + *p);
|
||||
builder.line_to(path_origin + *p);
|
||||
}
|
||||
|
||||
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 {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_numbers: Option<SmallVec<[WrappedLine; 1]>>,
|
||||
line_number_width: Pixels,
|
||||
cursor: Option<PaintQuad>,
|
||||
cursor_scroll_offset: Point<Pixels>,
|
||||
selection_path: Option<Path<Pixels>>,
|
||||
|
|
@ -392,12 +432,14 @@ impl Element for TextElement {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
let highlights = self.highlight_text(cx);
|
||||
let multi_line = self.input.read(cx).is_multi_line();
|
||||
let line_height = window.line_height();
|
||||
let input = self.input.read(cx);
|
||||
let text = input.text.clone();
|
||||
let placeholder = self.placeholder.clone();
|
||||
let style = window.text_style();
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let mut bounds = bounds;
|
||||
|
||||
let (display_text, text_color) = if text.is_empty() {
|
||||
|
|
@ -411,6 +453,52 @@ impl Element for TextElement {
|
|||
(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 {
|
||||
len: display_text.len(),
|
||||
font: style.font(),
|
||||
|
|
@ -444,12 +532,25 @@ impl Element for TextElement {
|
|||
.filter(|run| run.len > 0)
|
||||
.collect()
|
||||
} 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 {
|
||||
Some(bounds.size.width - RIGHT_MARGIN)
|
||||
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -490,14 +591,29 @@ impl Element for TextElement {
|
|||
|
||||
// Calculate the scroll offset to keep the cursor in view
|
||||
|
||||
let (cursor, cursor_scroll_offset) =
|
||||
self.layout_cursor(&lines, line_height, &mut bounds, window, cx);
|
||||
let (cursor, cursor_scroll_offset) = self.layout_cursor(
|
||||
&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 {
|
||||
bounds,
|
||||
lines,
|
||||
line_numbers,
|
||||
line_number_width,
|
||||
cursor,
|
||||
cursor_scroll_offset,
|
||||
selection_path,
|
||||
|
|
@ -566,8 +682,19 @@ impl Element for TextElement {
|
|||
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() {
|
||||
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);
|
||||
offset_y += line.size(line_height).height;
|
||||
}
|
||||
|
|
@ -595,6 +722,7 @@ impl Element for TextElement {
|
|||
input.set_input_bounds(input_bounds, cx);
|
||||
input.last_selected_range = Some(selected_range);
|
||||
input.scroll_size = scroll_size;
|
||||
input.line_number_width = prepaint.line_number_width;
|
||||
input
|
||||
.scroll_handle
|
||||
.set_offset(prepaint.cursor_scroll_offset);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod change;
|
|||
mod clear_button;
|
||||
mod element;
|
||||
mod mask_pattern;
|
||||
mod mode;
|
||||
mod number_input;
|
||||
mod otp_input;
|
||||
mod state;
|
||||
|
|
@ -11,6 +12,7 @@ mod text_wrapper;
|
|||
|
||||
pub(crate) use clear_button::*;
|
||||
pub use mask_pattern::MaskPattern;
|
||||
pub use mode::TabSize;
|
||||
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
||||
pub use otp_input::*;
|
||||
pub use state::*;
|
||||
|
|
|
|||
194
crates/ui/src/input/mode.rs
Normal file
194
crates/ui/src/input/mode.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,8 @@ use std::rc::Rc;
|
|||
use unicode_segmentation::*;
|
||||
|
||||
use gpui::{
|
||||
actions, div, impl_internal_actions, point, prelude::FluentBuilder as _, px, App, AppContext,
|
||||
Bounds, ClipboardItem, Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter,
|
||||
actions, div, impl_internal_actions, point, prelude::FluentBuilder as _, px, relative, App,
|
||||
AppContext, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
|
||||
Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
|
||||
|
|
@ -23,10 +23,20 @@ use gpui::{
|
|||
// - Move cursor to skip line eof empty chars.
|
||||
|
||||
use super::{
|
||||
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||
number_input, text_wrapper::TextWrapper,
|
||||
blink_cursor::BlinkCursor,
|
||||
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)]
|
||||
pub struct Enter {
|
||||
|
|
@ -45,6 +55,8 @@ actions!(
|
|||
DeleteToEndOfLine,
|
||||
DeleteToPreviousWordStart,
|
||||
DeleteToNextWordEnd,
|
||||
Indent,
|
||||
Outdent,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
|
|
@ -112,6 +124,8 @@ pub fn init(cx: &mut App) {
|
|||
KeyBinding::new("down", Down, Some(CONTEXT)),
|
||||
KeyBinding::new("left", Left, 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-right", SelectRight, Some(CONTEXT)),
|
||||
KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
|
||||
|
|
@ -191,95 +205,6 @@ pub fn init(cx: &mut App) {
|
|||
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`].
|
||||
pub struct InputState {
|
||||
pub(super) focus_handle: FocusHandle,
|
||||
|
|
@ -317,6 +242,7 @@ pub struct InputState {
|
|||
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
|
||||
/// The size of the scrollable content.
|
||||
pub(crate) scroll_size: gpui::Size<Pixels>,
|
||||
pub(crate) line_number_width: Pixels,
|
||||
|
||||
/// The mask pattern for formatting the input text
|
||||
pub(crate) mask_pattern: MaskPattern,
|
||||
|
|
@ -390,6 +316,7 @@ impl InputState {
|
|||
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
|
||||
scroll_size: gpui::size(px(0.), px(0.)),
|
||||
preferred_x_offset: None,
|
||||
line_number_width: px(0.),
|
||||
placeholder: SharedString::default(),
|
||||
mask_pattern: MaskPattern::default(),
|
||||
_subscriptions,
|
||||
|
|
@ -403,6 +330,7 @@ impl InputState {
|
|||
self.mode = InputMode::MultiLine {
|
||||
rows: 2,
|
||||
height: None,
|
||||
tab: TabSize::default(),
|
||||
};
|
||||
self
|
||||
}
|
||||
|
|
@ -417,12 +345,105 @@ impl InputState {
|
|||
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
|
||||
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = placeholder.into();
|
||||
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
|
||||
pub fn set_placeholder(
|
||||
&mut self,
|
||||
|
|
@ -565,7 +586,7 @@ impl InputState {
|
|||
pub(super) fn is_multi_line(&self) -> bool {
|
||||
matches!(
|
||||
self.mode,
|
||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
|
||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -579,28 +600,6 @@ impl InputState {
|
|||
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.
|
||||
///
|
||||
/// And the selection_range will be reset to 0..0.
|
||||
|
|
@ -714,6 +713,7 @@ impl InputState {
|
|||
/// Set the default value of the input field.
|
||||
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
||||
self.text = value.into();
|
||||
self.text_wrapper.text = self.text.clone();
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -758,6 +758,10 @@ impl InputState {
|
|||
if self.is_single_line() {
|
||||
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.move_vertical(-1, window, cx);
|
||||
}
|
||||
|
|
@ -766,6 +770,11 @@ impl InputState {
|
|||
if self.is_single_line() {
|
||||
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.move_vertical(1, window, cx);
|
||||
}
|
||||
|
|
@ -966,6 +975,24 @@ impl InputState {
|
|||
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
|
||||
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
|
||||
if self.is_single_line() {
|
||||
|
|
@ -1001,6 +1028,49 @@ impl InputState {
|
|||
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>) {
|
||||
if self.selected_range.is_empty() {
|
||||
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>) {
|
||||
if self.is_multi_line() {
|
||||
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);
|
||||
|
||||
// Move cursor to the start of the next line
|
||||
|
|
@ -1099,6 +1172,14 @@ impl InputState {
|
|||
new_offset += 1;
|
||||
}
|
||||
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 {
|
||||
|
|
@ -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>) {
|
||||
self.replace_text("", window, cx);
|
||||
}
|
||||
|
|
@ -1312,7 +1512,7 @@ impl InputState {
|
|||
//
|
||||
// - included the input padding.
|
||||
// - 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 y_offset = px(0.);
|
||||
|
|
@ -1418,40 +1618,66 @@ impl InputState {
|
|||
/// Select the word at the given 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>) {
|
||||
#[inline(always)]
|
||||
fn is_word(c: char) -> bool {
|
||||
c.is_alphanumeric() || matches!(c, '_')
|
||||
}
|
||||
|
||||
let mut start = self.offset_to_utf16(offset);
|
||||
let mut start = offset;
|
||||
let mut end = start;
|
||||
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();
|
||||
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();
|
||||
|
||||
let prev_chars = prev_text.chars().rev().peekable();
|
||||
let next_chars = next_text.chars().peekable();
|
||||
let prev_chars = prev_text.chars().rev();
|
||||
let next_chars = next_text.chars();
|
||||
|
||||
let mut last_char_len = 0;
|
||||
for (_, c) in prev_chars.enumerate() {
|
||||
if !is_word(c) {
|
||||
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() {
|
||||
if !is_word(c) {
|
||||
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());
|
||||
cx.notify()
|
||||
}
|
||||
|
|
@ -1711,7 +1937,7 @@ impl EntityInputHandler for InputState {
|
|||
|
||||
self.push_history(&range, &new_text, window, cx);
|
||||
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.marked_range.take();
|
||||
self.update_preferred_x_offset(cx);
|
||||
|
|
@ -1832,6 +2058,8 @@ impl Focusable for InputState {
|
|||
|
||||
impl Render for InputState {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.text_wrapper.update(self.text.clone(), false, cx);
|
||||
|
||||
div()
|
||||
.id("text-element")
|
||||
.flex_1()
|
||||
|
|
|
|||
|
|
@ -134,9 +134,12 @@ impl TextInput {
|
|||
impl RenderOnce for TextInput {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
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.text_wrapper.set_font(font, font_size, cx);
|
||||
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::select_up))
|
||||
.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_to_start_of_line))
|
||||
|
|
@ -222,6 +227,7 @@ impl RenderOnce for TextInput {
|
|||
.input_py(self.size)
|
||||
.input_h(self.size)
|
||||
.cursor_text()
|
||||
.text_size(font_size)
|
||||
.when(state.is_multi_line(), |this| {
|
||||
this.h_auto()
|
||||
.when_some(self.height, |this, height| this.h(height))
|
||||
|
|
@ -239,7 +245,6 @@ impl RenderOnce for TextInput {
|
|||
.items_center()
|
||||
.gap(gap_x)
|
||||
.children(prefix)
|
||||
// TODO: Define height here, and use it in the input element
|
||||
.child(self.state.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ use std::ops::Range;
|
|||
|
||||
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
|
||||
///
|
||||
/// After use lines to calculate the scroll size of the TextArea
|
||||
|
|
@ -9,6 +17,8 @@ pub(super) struct TextWrapper {
|
|||
pub(super) text: SharedString,
|
||||
/// The wrapped lines, value is start and end index of the line (by split \n).
|
||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||
/// The lines by split \n
|
||||
pub(super) lines: Vec<LineWrap>,
|
||||
pub(super) font: Font,
|
||||
pub(super) font_size: Pixels,
|
||||
/// If is none, it means the text is not wrapped
|
||||
|
|
@ -24,49 +34,72 @@ impl TextWrapper {
|
|||
font_size,
|
||||
wrap_width,
|
||||
wrapped_lines: Vec::new(),
|
||||
lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 lines = vec![];
|
||||
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
|
||||
let mut line_wrapper = cx
|
||||
.text_system()
|
||||
.line_wrapper(self.font.clone(), self.font_size);
|
||||
|
||||
let mut prev_line_ix = 0;
|
||||
for line in text.lines() {
|
||||
let mut line_wraps = vec![];
|
||||
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) {
|
||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||
line_wraps.push(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
|
||||
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.
|
||||
if text.chars().last().unwrap_or('\n') == '\n' {
|
||||
wrapped_lines.push(text.len()..text.len());
|
||||
lines.push(LineWrap {
|
||||
wrap_lines: 0,
|
||||
range: text.len()..text.len(),
|
||||
});
|
||||
}
|
||||
|
||||
self.text = text;
|
||||
self.wrapped_lines = wrapped_lines;
|
||||
self.lines = lines;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue