diff --git a/Cargo.lock b/Cargo.lock
index e442c3a9..83562a52 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3206,6 +3206,7 @@ dependencies = [
name = "gpui-component"
version = "0.1.0"
dependencies = [
+ "aho-corasick",
"anyhow",
"chrono",
"enum-iterator",
diff --git a/assets/icons/case-sensitive.svg b/assets/icons/case-sensitive.svg
new file mode 100644
index 00000000..648134fa
--- /dev/null
+++ b/assets/icons/case-sensitive.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg
new file mode 100644
index 00000000..5d7b480c
--- /dev/null
+++ b/assets/icons/replace.svg
@@ -0,0 +1,16 @@
+
diff --git a/crates/story/examples/code-editor.rs b/crates/story/examples/code-editor.rs
index f3c43cab..259914b3 100644
--- a/crates/story/examples/code-editor.rs
+++ b/crates/story/examples/code-editor.rs
@@ -137,11 +137,7 @@ impl CompletionProvider for ExampleLspStore {
_: &mut Window,
cx: &mut Context,
) -> Task>> {
- let trigger_character = trigger
- .trigger_character
- .as_deref()
- .unwrap_or("")
- .to_string();
+ let trigger_character = trigger.trigger_character.unwrap_or_default();
if trigger_character.is_empty() {
return Task::ready(Ok(vec![]));
}
diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs
index d95608a9..b048fe3c 100644
--- a/crates/story/examples/markdown.rs
+++ b/crates/story/examples/markdown.rs
@@ -26,6 +26,7 @@ impl Example {
tab_size: 2,
..Default::default()
})
+ .searchable(true)
.placeholder("Enter your Markdown here...")
.default_value(EXAMPLE)
});
@@ -66,6 +67,7 @@ impl Render for Example {
.child(
TextInput::new(&self.input_state)
.h_full()
+ .p_0()
.appearance(false)
.focus_bordered(false),
),
diff --git a/crates/story/src/textarea_story.rs b/crates/story/src/textarea_story.rs
index ea2dcfe7..49ab20a4 100644
--- a/crates/story/src/textarea_story.rs
+++ b/crates/story/src/textarea_story.rs
@@ -54,28 +54,30 @@ impl TextareaStory {
InputState::new(window, cx)
.multi_line()
.rows(10)
- .placeholder("Enter text here...").default_value(
- unindent::unindent(
- r#"Hello 世界,this is GPUI component.
+ .placeholder("Enter text here...")
+ .searchable(true)
+ .default_value(
+ unindent::unindent(
+ r#"Hello 世界,this is GPUI component.
- The GPUI Component is a collection of UI components for GPUI framework, including.
+ The GPUI Component is a collection of UI components for GPUI framework, including.
- Button, Input, Checkbox, Radio, Dropdown, Tab, and more...
+ Button, Input, Checkbox, Radio, Dropdown, Tab, and more...
- Here is an application that is built by using GPUI Component.
+ Here is an application that is built by using GPUI Component.
- > This application is still under development, not published yet.
+ > This application is still under development, not published yet.
- 
+ 
- 
+ 
- ## Demo
+ ## Demo
- If you want to see the demo, here is a some demo applications.
- "#,
+ If you want to see the demo, here is a some demo applications.
+ "#,
+ )
)
- )
});
let textarea_auto_grow = cx.new(|cx| {
diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml
index 559dfff5..4cb2888d 100644
--- a/crates/ui/Cargo.toml
+++ b/crates/ui/Cargo.toml
@@ -91,6 +91,7 @@ chrono = "0.4.38"
# Code Editor
lsp-types.workspace = true
+aho-corasick = "1.1.3"
tree-sitter = "0.25.4"
tree-sitter-json = "0.24.8"
tree-sitter-bash = { version = "0.23.3", optional = true }
diff --git a/crates/ui/locales/ui.yml b/crates/ui/locales/ui.yml
index a802c835..4c03904c 100644
--- a/crates/ui/locales/ui.yml
+++ b/crates/ui/locales/ui.yml
@@ -155,3 +155,14 @@ List:
zh-CN: 搜索...
zh-HK: 搜索...
it: Ricerca...
+Input:
+ Replace:
+ en: Replace
+ zh-CN: 替换
+ zh-HK: 替換
+ it: Sostituisci
+ Replace All:
+ en: Replace All
+ zh-CN: 全部替换
+ zh-HK: 全部替換
+ it: Sostituisci tutto
diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs
index b465f4aa..0e6e4b70 100644
--- a/crates/ui/src/icon.rs
+++ b/crates/ui/src/icon.rs
@@ -18,13 +18,14 @@ pub enum IconName {
Bot,
Building2,
Calendar,
+ CaseSensitive,
ChartPie,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
- ChevronUp,
ChevronsUpDown,
+ ChevronUp,
CircleCheck,
CircleUser,
CircleX,
@@ -68,6 +69,7 @@ pub enum IconName {
PanelRightClose,
PanelRightOpen,
Plus,
+ Replace,
ResizeCorner,
Search,
Settings,
@@ -102,6 +104,7 @@ impl IconName {
Self::Bot => "icons/bot.svg",
Self::Building2 => "icons/building-2.svg",
Self::Calendar => "icons/calendar.svg",
+ Self::CaseSensitive => "icons/case-sensitive.svg",
Self::ChartPie => "icons/chart-pie.svg",
Self::Check => "icons/check.svg",
Self::ChevronDown => "icons/chevron-down.svg",
@@ -152,6 +155,7 @@ impl IconName {
Self::PanelRightClose => "icons/panel-right-close.svg",
Self::PanelRightOpen => "icons/panel-right-open.svg",
Self::Plus => "icons/plus.svg",
+ Self::Replace => "icons/replace.svg",
Self::ResizeCorner => "icons/resize-corner.svg",
Self::Search => "icons/search.svg",
Self::Settings => "icons/settings.svg",
diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs
index c525d27e..9a313d9f 100644
--- a/crates/ui/src/input/element.rs
+++ b/crates/ui/src/input/element.rs
@@ -11,7 +11,7 @@ use smallvec::SmallVec;
use crate::{
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
- ActiveTheme as _, Root,
+ ActiveTheme as _, Colorize, Root,
};
use super::{mode::InputMode, InputState, LastLayout};
@@ -76,8 +76,8 @@ impl TextElement {
let line_number_width = last_layout.line_number_width;
let mut selected_range = state.selected_range;
- if let Some(marked_range) = &state.marked_range {
- selected_range = (marked_range.end..marked_range.end).into();
+ if let Some(ime_marked_range) = &state.ime_marked_range {
+ selected_range = (ime_marked_range.end..ime_marked_range.end).into();
}
let cursor = state.cursor();
@@ -230,35 +230,29 @@ impl TextElement {
(cursor_bounds, scroll_offset, current_row)
}
- fn layout_selections(
- &self,
+ fn layout_match_range(
+ range: Range,
last_layout: &LastLayout,
bounds: &mut Bounds,
- _: &mut Window,
- cx: &mut App,
) -> Option> {
- let line_height = last_layout.line_height;
- let visible_top = last_layout.visible_top;
- let visible_start_offset = last_layout.visible_start_offset;
- let lines = &last_layout.lines;
- let line_number_width = last_layout.line_number_width;
-
- let state = self.state.read(cx);
- let mut selected_range = state.selected_range;
- if let Some(marked_range) = &state.marked_range {
- if !marked_range.is_empty() {
- selected_range = (marked_range.end..marked_range.end).into();
- }
- }
- if selected_range.is_empty() {
+ if range.is_empty() {
return None;
}
- let (start_ix, end_ix) = if selected_range.start < selected_range.end {
- (selected_range.start, selected_range.end)
- } else {
- (selected_range.end, selected_range.start)
- };
+ if range.start < last_layout.visible_range_offset.start
+ || range.end > last_layout.visible_range_offset.end
+ {
+ return None;
+ }
+
+ let line_height = last_layout.line_height;
+ let visible_top = last_layout.visible_top;
+ let visible_start_offset = last_layout.visible_range_offset.start;
+ let lines = &last_layout.lines;
+ let line_number_width = last_layout.line_number_width;
+
+ let start_ix = range.start;
+ let end_ix = range.end;
let mut prev_lines_offset = visible_start_offset;
let mut offset_y = visible_top;
@@ -369,6 +363,62 @@ impl TextElement {
builder.build().ok()
}
+ fn layout_search_matches(
+ &self,
+ last_layout: &LastLayout,
+ bounds: &mut Bounds,
+ cx: &mut App,
+ ) -> Vec<(Path, bool)> {
+ let search_panel = self.state.read(cx).search_panel.clone();
+ let Some((ranges, current_match_ix)) = search_panel.and_then(|panel| {
+ if let Some(matcher) = panel.read(cx).matcher() {
+ Some((matcher.matched_ranges.clone(), matcher.current_match_ix))
+ } else {
+ None
+ }
+ }) else {
+ return vec![];
+ };
+
+ let mut paths = Vec::new();
+ for (index, range) in ranges.as_ref().iter().enumerate() {
+ if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) {
+ paths.push((path, current_match_ix == index));
+ }
+ }
+
+ paths
+ }
+
+ fn layout_selections(
+ &self,
+ last_layout: &LastLayout,
+ bounds: &mut Bounds,
+ cx: &mut App,
+ ) -> Option> {
+ let state = self.state.read(cx);
+ let mut selected_range = state.selected_range;
+ if let Some(ime_marked_range) = &state.ime_marked_range {
+ if !ime_marked_range.is_empty() {
+ selected_range = (ime_marked_range.end..ime_marked_range.end).into();
+ }
+ }
+ if selected_range.is_empty() {
+ return None;
+ }
+
+ let (start_ix, end_ix) = if selected_range.start < selected_range.end {
+ (selected_range.start, selected_range.end)
+ } else {
+ (selected_range.end, selected_range.start)
+ };
+
+ let range = start_ix.max(last_layout.visible_range_offset.start)
+ ..end_ix.min(last_layout.visible_range_offset.end);
+
+ Self::layout_match_range(range, &last_layout, bounds)
+ }
+
/// Calculate the visible range of lines in the viewport.
///
/// Returns
@@ -470,6 +520,7 @@ pub(super) struct PrepaintState {
/// row index (zero based), no wrap, same line as the cursor.
current_row: Option,
selection_path: Option>,
+ search_match_paths: Vec<(Path, bool)>,
bounds: Bounds,
}
@@ -604,10 +655,10 @@ impl Element for TextElement {
// Calculate the width of the line numbers
let empty_line_number = window.text_system().shape_line(
- "+++++".into(),
+ "++++++".into(),
font_size,
&[TextRun {
- len: 5,
+ len: 6,
font: style.font(),
color: gpui::black(),
background_color: None,
@@ -649,8 +700,10 @@ impl Element for TextElement {
runs.extend(highlight_styles.iter().map(|(range, style)| {
let mut run = text_style.clone().highlight(*style).to_run(range.len());
- if let Some(marked_range) = &state.marked_range {
- if range.start >= marked_range.start && range.end <= marked_range.end {
+ if let Some(ime_marked_range) = &state.ime_marked_range {
+ if range.start >= ime_marked_range.start
+ && range.end <= ime_marked_range.end
+ {
run.color = marked_run.color;
run.strikethrough = marked_run.strikethrough;
run.underline = marked_run.underline;
@@ -664,20 +717,20 @@ impl Element for TextElement {
} else {
vec![run]
}
- } else if let Some(marked_range) = &state.marked_range {
+ } else if let Some(ime_marked_range) = &state.ime_marked_range {
// IME marked text
vec![
TextRun {
- len: marked_range.start,
+ len: ime_marked_range.start,
..run.clone()
},
TextRun {
- len: marked_range.end - marked_range.start,
+ len: ime_marked_range.end - ime_marked_range.start,
underline: marked_run.underline,
..run.clone()
},
TextRun {
- len: display_text.len() - marked_range.end,
+ len: display_text.len() - ime_marked_range.end,
..run.clone()
},
]
@@ -689,7 +742,7 @@ impl Element for TextElement {
};
let wrap_width = if multi_line && state.soft_wrap {
- Some(bounds.size.width - line_number_width)
+ Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
} else {
None
};
@@ -706,8 +759,8 @@ impl Element for TextElement {
.expect("failed to shape text");
// measure.end();
- let mut longest_line_width = px(0.);
- if state.mode.is_multi_line() && lines.len() > 1 {
+ let mut longest_line_width = wrap_width.unwrap_or(px(0.));
+ if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
let longtest_line: SharedString = state
.text
.line(state.text.summary().longest_row as usize)
@@ -750,7 +803,7 @@ impl Element for TextElement {
let mut last_layout = LastLayout {
visible_range,
visible_top,
- visible_start_offset,
+ visible_range_offset: visible_start_offset..visible_end_offset,
line_height,
wrap_width,
line_number_width,
@@ -793,7 +846,8 @@ impl Element for TextElement {
self.layout_cursor(&last_layout, &mut bounds, window, cx);
last_layout.cursor_bounds = cursor_bounds;
- let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx);
+ let search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx);
+ let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
let state = self.state.read(cx);
let line_numbers = if state.mode.line_number() {
@@ -821,7 +875,7 @@ impl Element for TextElement {
let ix = last_layout.visible_range.start + ix;
let line_no = ix + 1;
- let mut line_no_text = format!("{:>5}", line_no);
+ let mut line_no_text = format!("{:>6}", line_no);
if !line.wrap_boundaries.is_empty() {
line_no_text.push_str(&"\n ".repeat(line.wrap_boundaries.len()));
}
@@ -852,6 +906,7 @@ impl Element for TextElement {
cursor_scroll_offset,
current_row,
selection_path,
+ search_match_paths,
}
}
@@ -948,6 +1003,14 @@ impl Element for TextElement {
// Paint selections
if window.is_window_active() {
+ for (path, is_active) in prepaint.search_match_paths.iter() {
+ window.paint_path(path.clone(), cx.theme().selection.saturation(0.1));
+
+ if *is_active {
+ window.paint_path(path.clone(), cx.theme().selection);
+ }
+ }
+
if let Some(path) = prepaint.selection_path.take() {
window.paint_path(path, cx.theme().selection);
}
@@ -986,11 +1049,7 @@ impl Element for TextElement {
input_bounds.size.height,
),
},
- cx.theme()
- .highlight_theme
- .style
- .background
- .unwrap_or(cx.theme().input),
+ cx.theme().background,
));
// Each item is the normal lines.
diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs
index cf7699ae..f89abe95 100644
--- a/crates/ui/src/input/mod.rs
+++ b/crates/ui/src/input/mod.rs
@@ -10,6 +10,7 @@ mod number_input;
mod otp_input;
mod popovers;
mod rope_ext;
+mod search;
mod state;
mod text_input;
mod text_wrapper;
diff --git a/crates/ui/src/input/search.rs b/crates/ui/src/input/search.rs
new file mode 100644
index 00000000..8d4edafc
--- /dev/null
+++ b/crates/ui/src/input/search.rs
@@ -0,0 +1,547 @@
+use aho_corasick::AhoCorasick;
+use rust_i18n::t;
+use std::{ops::Range, rc::Rc};
+
+use gpui::{
+ actions, div, prelude::FluentBuilder as _, App, AppContext as _, Context, Empty, Entity,
+ EntityInputHandler, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement,
+ KeyBinding, ParentElement as _, Render, Styled, Subscription, Window,
+};
+use rope::Rope;
+
+use crate::{
+ actions::SelectPrev,
+ button::{Button, ButtonVariants},
+ h_flex,
+ input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt, Search, TextInput},
+ v_flex, ActiveTheme, IconName, Selectable, Sizable,
+};
+
+const KEY_CONTEXT: &'static str = "SearchPanel";
+
+actions!(input, [Tab]);
+
+pub(super) fn init(cx: &mut App) {
+ cx.bind_keys(vec![KeyBinding::new(
+ "shift-enter",
+ SelectPrev,
+ Some(KEY_CONTEXT),
+ )]);
+}
+
+#[derive(Debug, Clone)]
+pub struct SearchMatcher {
+ text: Rope,
+ pub query: Option,
+
+ pub(super) matched_ranges: Rc>>,
+ pub(super) current_match_ix: usize,
+ /// Is in replacing mode, if true, the next update will not reset the current match index.
+ replacing: bool,
+}
+
+impl SearchMatcher {
+ pub fn new() -> Self {
+ Self {
+ text: "".into(),
+ query: None,
+ matched_ranges: Rc::new(Vec::new()),
+ current_match_ix: 0,
+ replacing: false,
+ }
+ }
+
+ /// Update source text and re-match
+ pub(crate) fn update(&mut self, text: &Rope) {
+ if self.text.eq(text) {
+ return;
+ }
+
+ self.text = text.clone();
+ self.update_matches();
+ }
+
+ fn update_matches(&mut self) {
+ let mut new_ranges = Vec::new();
+ if let Some(query) = &self.query {
+ let matches = query.stream_find_iter(self.text.bytes_in_range(0..self.text.len()));
+
+ for query_match in matches.into_iter() {
+ let query_match = query_match.expect("query match for select all action");
+ new_ranges.push(query_match.range());
+ }
+ }
+ self.matched_ranges = Rc::new(new_ranges);
+ if !self.replacing {
+ self.current_match_ix = 0;
+ self.replacing = false;
+ }
+ }
+
+ /// Update the search query and reset the current match index.
+ pub fn update_query(&mut self, query: &str, case_insensitive: bool) {
+ if query.len() > 0 {
+ self.query = Some(
+ AhoCorasick::builder()
+ .ascii_case_insensitive(case_insensitive)
+ .build(&[query.to_string()])
+ .expect("failed to build AhoCorasick query in SearchMatcher"),
+ );
+ } else {
+ self.query = None;
+ }
+ self.update_matches();
+ }
+
+ /// Returns the number of matches found.
+ #[allow(unused)]
+ #[inline]
+ fn len(&self) -> usize {
+ self.matched_ranges.len()
+ }
+
+ fn peek(&self) -> Option> {
+ self.matched_ranges.get(self.current_match_ix + 1).cloned()
+ }
+}
+
+impl Iterator for SearchMatcher {
+ type Item = Range;
+
+ fn next(&mut self) -> Option {
+ if self.matched_ranges.is_empty() {
+ return None;
+ }
+
+ if self.current_match_ix < self.matched_ranges.len().saturating_sub(1) {
+ self.current_match_ix += 1;
+ } else {
+ self.current_match_ix = 0;
+ }
+
+ self.matched_ranges.get(self.current_match_ix).cloned()
+ }
+}
+
+impl DoubleEndedIterator for SearchMatcher {
+ fn next_back(&mut self) -> Option {
+ if self.matched_ranges.is_empty() {
+ return None;
+ }
+
+ if self.current_match_ix == 0 {
+ self.current_match_ix = self.matched_ranges.len();
+ }
+
+ self.current_match_ix -= 1;
+ let item = self.matched_ranges[self.current_match_ix].clone();
+
+ Some(item)
+ }
+}
+
+pub(super) struct SearchPanel {
+ text_state: Entity,
+ search_input: Entity,
+ replace_input: Entity,
+ case_insensitive: bool,
+ replace_mode: bool,
+ matcher: SearchMatcher,
+
+ open: bool,
+ _subscriptions: Vec,
+}
+
+impl InputState {
+ /// Update the search matcher when text changes.
+ pub(super) fn update_search(&mut self, cx: &mut App) {
+ let Some(search_panel) = self.search_panel.as_ref() else {
+ return;
+ };
+
+ let text = self.text.clone();
+ search_panel.update(cx, |this, _| {
+ this.matcher.update(&text);
+ });
+ }
+
+ pub(super) fn on_action_search(
+ &mut self,
+ _: &Search,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if !self.searchable {
+ return;
+ }
+
+ let search_panel = match self.search_panel.as_ref() {
+ Some(panel) => panel.clone(),
+ None => SearchPanel::new(cx.entity(), window, cx),
+ };
+
+ let text = self.text.clone();
+ let text_state = cx.entity();
+ let selected_text = self.selected_text();
+ search_panel.update(cx, |this, cx| {
+ this.text_state = text_state;
+ this.matcher.update(&text);
+ this.show(&selected_text, window, cx);
+ });
+ self.search_panel = Some(search_panel);
+ cx.notify();
+ }
+}
+
+impl SearchPanel {
+ pub fn new(text_state: Entity, window: &mut Window, cx: &mut App) -> Entity {
+ let search_input = cx.new(|cx| InputState::new(window, cx));
+ let replace_input = cx.new(|cx| InputState::new(window, cx));
+
+ cx.new(|cx| {
+ let _subscriptions = vec![cx.subscribe(
+ &search_input,
+ |this: &mut Self, search_input, ev: &InputEvent, cx| {
+ // Handle search input changes
+ match ev {
+ InputEvent::Change => {
+ let value = search_input.read(cx).value();
+ this.matcher
+ .update_query(value.as_str(), this.case_insensitive);
+ }
+ _ => {}
+ }
+ },
+ )];
+
+ Self {
+ text_state,
+ search_input,
+ replace_input,
+ case_insensitive: true,
+ replace_mode: false,
+ matcher: SearchMatcher::new(),
+ open: true,
+ _subscriptions,
+ }
+ })
+ }
+
+ pub(super) fn show(
+ &mut self,
+ selected_text: &Rope,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.open = true;
+ self.search_input.read(cx).focus_handle.focus(window);
+
+ self.search_input.update(cx, |this, cx| {
+ if selected_text.len() > 0 {
+ this.set_value(selected_text.to_string(), window, cx);
+ }
+ this.select_all(&super::SelectAll, window, cx);
+ });
+ self.update_search(cx);
+ cx.notify();
+ }
+
+ fn update_search(&mut self, cx: &mut Context) {
+ let query = self.search_input.read(cx).value();
+ self.matcher
+ .update_query(query.as_str(), self.case_insensitive);
+ self.update_text_selection(cx);
+ }
+
+ pub(super) fn hide(&mut self, window: &mut Window, cx: &mut Context) {
+ self.open = false;
+ self.text_state.read(cx).focus_handle.focus(window);
+ cx.notify();
+ }
+
+ fn on_action_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context) {
+ self.prev(window, cx);
+ }
+
+ fn on_action_next(&mut self, _: &Enter, window: &mut Window, cx: &mut Context) {
+ self.next(window, cx);
+ }
+
+ fn on_action_escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context) {
+ self.hide(window, cx);
+ }
+
+ fn on_action_tab(&mut self, _: &IndentInline, window: &mut Window, cx: &mut Context) {
+ self.text_state.focus_handle(cx).focus(window);
+ }
+
+ fn update_text_selection(&mut self, cx: &mut Context) {
+ if let Some(range) = self
+ .matcher
+ .matched_ranges
+ .get(self.matcher.current_match_ix)
+ .cloned()
+ {
+ let state = self.text_state.clone();
+ cx.spawn(async move |_, cx| {
+ _ = cx.update(|cx| {
+ state.update(cx, |state, cx| {
+ state.selected_range = range.into();
+ cx.notify();
+ });
+ });
+ })
+ .detach();
+ }
+ }
+
+ fn prev(&mut self, _: &mut Window, cx: &mut Context) {
+ if let Some(range) = self.matcher.next_back() {
+ self.text_state.update(cx, |state, cx| {
+ state.scroll_to(range.start, cx);
+ });
+ }
+ }
+
+ fn next(&mut self, _: &mut Window, cx: &mut Context) {
+ if let Some(range) = self.matcher.next() {
+ self.text_state.update(cx, |state, cx| {
+ state.scroll_to(range.end, cx);
+ });
+ }
+ }
+
+ pub(super) fn matcher(&self) -> Option<&SearchMatcher> {
+ if !self.open {
+ return None;
+ }
+
+ Some(&self.matcher)
+ }
+
+ fn replace_next(&mut self, window: &mut Window, cx: &mut Context) {
+ let new_text = self.replace_input.read(cx).value();
+ self.matcher.replacing = true;
+ if let Some(range) = self
+ .matcher
+ .matched_ranges
+ .get(self.matcher.current_match_ix)
+ .cloned()
+ {
+ let text_state = self.text_state.clone();
+
+ let next_range = self.matcher.peek().unwrap_or(range.clone());
+ cx.spawn_in(window, async move |_, cx| {
+ cx.update(|window, cx| {
+ text_state.update(cx, |state, cx| {
+ let range_utf16 = state.range_to_utf16(&range);
+ state.scroll_to(next_range.end, cx);
+ state.replace_text_in_range(
+ Some(range_utf16),
+ new_text.as_str(),
+ window,
+ cx,
+ );
+ });
+ })
+ })
+ .detach();
+ }
+ }
+
+ fn replace_all(&mut self, window: &mut Window, cx: &mut Context) {
+ let new_text = self.replace_input.read(cx).value();
+ self.matcher.replacing = true;
+ let ranges = self.matcher.matched_ranges.clone();
+ if ranges.is_empty() {
+ return;
+ }
+
+ let text_state = self.text_state.clone();
+ cx.spawn_in(window, async move |_, cx| {
+ cx.update(|window, cx| {
+ text_state.update(cx, |state, cx| {
+ // Replace from the end to avoid messing up the ranges.
+ let mut rope = state.text.clone();
+ for range in ranges.iter().rev() {
+ rope.replace(range.clone(), new_text.as_str());
+ }
+ state.replace_text_in_range(
+ Some(0..state.text.len()),
+ &rope.to_string(),
+ window,
+ cx,
+ );
+ state.scroll_to(0, cx);
+ });
+ })
+ })
+ .detach();
+ }
+}
+
+impl Focusable for SearchPanel {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.search_input.read(cx).focus_handle.clone()
+ }
+}
+
+impl Render for SearchPanel {
+ fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
+ if !self.open {
+ return Empty.into_any_element();
+ }
+
+ v_flex()
+ .id("search-panel")
+ .occlude()
+ .track_focus(&self.focus_handle(cx))
+ .key_context(KEY_CONTEXT)
+ .on_action(cx.listener(Self::on_action_prev))
+ .on_action(cx.listener(Self::on_action_next))
+ .on_action(cx.listener(Self::on_action_escape))
+ .on_action(cx.listener(Self::on_action_tab))
+ .font_family(".SystemUIFont")
+ .items_center()
+ .py_2()
+ .px_3()
+ .w_full()
+ .gap_1()
+ .bg(cx.theme().popover)
+ .border_b_1()
+ .rounded(cx.theme().radius.half())
+ .border_color(cx.theme().border)
+ .child(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .child(
+ div().flex_1().gap_1().child(
+ TextInput::new(&self.search_input)
+ .focus_bordered(false)
+ .suffix(
+ Button::new("case-insensitive")
+ .selected(!self.case_insensitive)
+ .xsmall()
+ .compact()
+ .ghost()
+ .icon(IconName::CaseSensitive)
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.case_insensitive = !this.case_insensitive;
+ this.update_search(cx);
+ cx.notify();
+ })),
+ )
+ .small()
+ .w_full()
+ .cleanable()
+ .shadow_none(),
+ ),
+ )
+ .child(
+ Button::new("replace-mode")
+ .xsmall()
+ .ghost()
+ .icon(IconName::Replace)
+ .selected(self.replace_mode)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.replace_mode = !this.replace_mode;
+ this.replace_input.read(cx).focus_handle.focus(window);
+ cx.notify();
+ })),
+ )
+ .child(
+ Button::new("prev")
+ .xsmall()
+ .ghost()
+ .icon(IconName::ChevronLeft)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.prev(window, cx);
+ })),
+ )
+ .child(
+ Button::new("next")
+ .xsmall()
+ .ghost()
+ .icon(IconName::ChevronRight)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.next(window, cx);
+ })),
+ )
+ .child(div().w_5())
+ .child(
+ Button::new("close")
+ .xsmall()
+ .ghost()
+ .icon(IconName::Close)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.on_action_escape(&Escape, window, cx);
+ })),
+ ),
+ )
+ .when(self.replace_mode, |this| {
+ this.child(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .child(
+ TextInput::new(&self.replace_input)
+ .focus_bordered(false)
+ .small()
+ .w_full()
+ .shadow_none(),
+ )
+ .child(
+ Button::new("replace-one")
+ .small()
+ .label(t!("Input.Replace"))
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.replace_next(window, cx);
+ })),
+ )
+ .child(
+ Button::new("replace-all")
+ .small()
+ .label(t!("Input.Replace All"))
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.replace_all(window, cx);
+ })),
+ ),
+ )
+ })
+ .into_any_element()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_search() {
+ let mut search = SearchMatcher::new();
+ search.update(&Rope::from("Hello 世界 this is a Is test string."));
+ search.update_query("Is", true);
+
+ assert_eq!(search.len(), 3);
+ let mut matches = search.clone().into_iter();
+ assert_eq!(matches.current_match_ix, 0);
+ assert_eq!(matches.next(), Some(18..20));
+ assert_eq!(matches.next(), Some(23..25));
+ assert_eq!(matches.current_match_ix, 2);
+ assert_eq!(matches.next(), Some(15..17));
+ assert_eq!(matches.current_match_ix, 0);
+ assert_eq!(matches.next_back(), Some(23..25));
+ assert_eq!(matches.current_match_ix, 2);
+ assert_eq!(matches.next_back(), Some(18..20));
+ assert_eq!(matches.current_match_ix, 1);
+ assert_eq!(matches.next_back(), Some(15..17));
+ assert_eq!(matches.current_match_ix, 0);
+ assert_eq!(matches.next_back(), Some(23..25));
+
+ search.update_query("IS", false);
+ assert_eq!(search.len(), 0);
+ assert_eq!(search.next(), None);
+ assert_eq!(search.next_back(), None);
+ }
+}
diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs
index dadedab6..3f908421 100644
--- a/crates/ui/src/input/state.rs
+++ b/crates/ui/src/input/state.rs
@@ -5,7 +5,7 @@
use anyhow::Result;
use gpui::{
actions, div, point, prelude::FluentBuilder as _, px, Action, App, AppContext, Bounds,
- ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
+ ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable, Half,
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
@@ -31,6 +31,7 @@ use super::{
};
use crate::input::{
popovers::{ContextMenu, DiagnosticPopover},
+ search::{self, SearchPanel},
Position,
};
use crate::input::{RopeExt as _, Selection};
@@ -90,6 +91,7 @@ actions!(
MoveToNextWord,
Escape,
ToggleCodeActions,
+ Search,
]
);
@@ -216,8 +218,13 @@ pub fn init(cx: &mut App) {
KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
+ #[cfg(target_os = "macos")]
+ KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
+ #[cfg(not(target_os = "macos"))]
+ KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
]);
+ search::init(cx);
number_input::init(cx);
}
@@ -227,8 +234,8 @@ pub(super) struct LastLayout {
pub(super) visible_range: Range,
/// The first visible line top position in scroll viewport.
pub(super) visible_top: Pixels,
- /// The start byte offset of the first visible line.
- pub(super) visible_start_offset: usize,
+ /// The range of byte offset of the visible lines.
+ pub(super) visible_range_offset: Range,
/// The last layout lines (Only have visible lines).
pub(super) lines: Rc>,
/// The line_height of text layout, this will change will InputElement painted.
@@ -255,11 +262,13 @@ pub struct InputState {
/// - "Hello 世界💝" = 16
/// - "💝" = 4
pub(super) selected_range: Selection,
+ pub(super) search_panel: Option>,
+ pub(super) searchable: bool,
/// Range for save the selected word, use to keep word range when drag move.
pub(super) selected_word_range: Option,
pub(super) selection_reversed: bool,
/// The marked range is the temporary insert text on IME typing.
- pub(super) marked_range: Option,
+ pub(super) ime_marked_range: Option,
pub(super) last_layout: Option,
pub(super) last_cursor: Option,
/// The input container bounds
@@ -342,9 +351,11 @@ impl InputState {
blink_cursor,
history,
selected_range: Selection::default(),
+ search_panel: None,
+ searchable: false,
selected_word_range: None,
selection_reversed: false,
- marked_range: None,
+ ime_marked_range: None,
input_bounds: Bounds::default(),
selecting: false,
disabled: false,
@@ -425,6 +436,13 @@ impl InputState {
code_action_providers: vec![],
completion_provider: None,
};
+ self.searchable = true;
+ self
+ }
+
+ /// Set this input is searchable, default is false (Default true for Code Editor).
+ pub fn searchable(mut self, searchable: bool) -> Self {
+ self.searchable = searchable;
self
}
@@ -634,7 +652,7 @@ impl InputState {
};
let line_height = last_layout.line_height;
- let mut prev_lines_offset = last_layout.visible_start_offset;
+ let mut prev_lines_offset = last_layout.visible_range_offset.start;
let mut y_offset = last_layout.visible_top;
for (line_index, line) in last_layout.lines.iter().enumerate() {
let local_offset = offset.saturating_sub(prev_lines_offset);
@@ -1049,7 +1067,7 @@ impl InputState {
cx: &mut Context,
) {
self.move_to(0, window, cx);
- self.select_to(self.text.len(), window, cx)
+ self.select_to(self.text.len(), window, cx);
}
pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context) {
@@ -1549,7 +1567,7 @@ impl InputState {
return;
}
- if self.marked_range.is_some() {
+ if self.ime_marked_range.is_some() {
self.unmark_text(window, cx);
}
@@ -1577,9 +1595,9 @@ impl InputState {
) {
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
// Clear the marked range.
- if let Some(marked_range) = &self.marked_range {
- if marked_range.len() == 0 {
- self.marked_range = None;
+ if let Some(ime_marked_range) = &self.ime_marked_range {
+ if ime_marked_range.len() == 0 {
+ self.ime_marked_range = None;
}
}
@@ -1674,6 +1692,41 @@ impl InputState {
cx.notify();
}
+ pub(crate) fn scroll_to(&mut self, offset: usize, cx: &mut Context) {
+ let Some(last_layout) = self.last_layout.as_ref() else {
+ return;
+ };
+ let Some(bounds) = self.last_bounds.as_ref() else {
+ return;
+ };
+
+ let mut scroll_offset = self.scroll_handle.offset();
+ let line_height = last_layout.line_height;
+
+ let point = self.text.offset_to_point(offset);
+ let row = point.row as usize;
+
+ let mut row_offset_y = px(0.);
+ for (ix, wrap_line) in self.text_wrapper.lines.iter().enumerate() {
+ if ix == row {
+ break;
+ }
+
+ row_offset_y += wrap_line.height(line_height);
+ }
+
+ // Check if row_offset_y is out of the viewport
+ // If row offset is not in the viewport, scroll to make it visible
+ if row_offset_y < -scroll_offset.y {
+ // Scroll up
+ scroll_offset.y = -row_offset_y - line_height + bounds.size.height.half();
+ } else if row_offset_y + line_height > -scroll_offset.y + bounds.size.height {
+ // Scroll down
+ scroll_offset.y = -(row_offset_y - bounds.size.height.half());
+ }
+ self.update_scroll_offset(Some(scroll_offset), cx);
+ }
+
pub(super) fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
@@ -1756,6 +1809,7 @@ impl InputState {
fn move_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context) {
let offset = offset.clamp(0, self.text.len());
self.selected_range = (offset..offset).into();
+ self.scroll_to(offset, cx);
self.pause_blink_cursor(cx);
self.update_preferred_column();
self.hide_context_menu(cx);
@@ -1766,8 +1820,8 @@ impl InputState {
///
/// The offset is the UTF-8 offset.
pub fn cursor(&self) -> usize {
- if let Some(marked_range) = &self.marked_range {
- return marked_range.end;
+ if let Some(ime_marked_range) = &self.ime_marked_range {
+ return ime_marked_range.end;
}
if self.selection_reversed {
@@ -1809,7 +1863,7 @@ impl InputState {
// - included the scroll offset.
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
- let mut index = last_layout.visible_start_offset;
+ let mut index = last_layout.visible_range_offset.start;
let mut y_offset = last_layout.visible_top;
for (ix, line) in self
.text_wrapper
@@ -2183,6 +2237,10 @@ impl InputState {
}
}
}
+
+ pub(super) fn selected_text(&self) -> Rope {
+ self.text.slice(self.selected_range.into())
+ }
}
impl EntityInputHandler for InputState {
@@ -2215,12 +2273,12 @@ impl EntityInputHandler for InputState {
_window: &mut Window,
_cx: &mut Context,
) -> Option> {
- self.marked_range
+ self.ime_marked_range
.map(|range| self.range_to_utf16(&range.into()))
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {
- self.marked_range = None;
+ self.ime_marked_range = None;
}
/// Replace text in range.
@@ -2243,7 +2301,7 @@ impl EntityInputHandler for InputState {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
- .or(self.marked_range.map(|range| range.into()))
+ .or(self.ime_marked_range.map(|range| range.into()))
.unwrap_or(self.selected_range.into());
let old_text = self.text.clone();
@@ -2273,9 +2331,10 @@ impl EntityInputHandler for InputState {
self.mode
.update_highlighter(&range, &self.text, &new_text, true, cx);
self.selected_range = (new_offset..new_offset).into();
- self.marked_range.take();
+ self.ime_marked_range.take();
self.update_preferred_column();
self.update_scroll_offset(None, cx);
+ self.update_search(cx);
self.mode.update_auto_grow(&self.text_wrapper);
self.handle_completion_trigger(&range, &new_text, window, cx);
cx.emit(InputEvent::Change);
@@ -2298,7 +2357,7 @@ impl EntityInputHandler for InputState {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
- .or(self.marked_range.map(|range| range.into()))
+ .or(self.ime_marked_range.map(|range| range.into()))
.unwrap_or(self.selected_range.into());
let old_text = self.text.clone();
@@ -2320,9 +2379,9 @@ impl EntityInputHandler for InputState {
if new_text.is_empty() {
// Cancel selection, when cancel IME input.
self.selected_range = (range.start..range.start).into();
- self.marked_range = None;
+ self.ime_marked_range = None;
} else {
- self.marked_range = Some((range.start..range.start + new_text.len()).into());
+ self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
@@ -2352,7 +2411,7 @@ impl EntityInputHandler for InputState {
let mut end_origin = None;
let line_number_origin = point(line_number_width, px(0.));
let mut y_offset = last_layout.visible_top;
- let mut index_offset = last_layout.visible_start_offset;
+ let mut index_offset = last_layout.visible_range_offset.start;
for line in last_layout.lines.iter() {
if start_origin.is_some() && end_origin.is_some() {
@@ -2400,7 +2459,7 @@ impl EntityInputHandler for InputState {
let last_layout = self.last_layout.as_ref()?;
let line_height = last_layout.line_height;
let line_point = self.last_bounds?.localize(&point)?;
- let offset = last_layout.visible_start_offset;
+ let offset = last_layout.visible_range_offset.start;
for line in last_layout.lines.iter() {
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs
index e5b42f87..d9720635 100644
--- a/crates/ui/src/input/text_input.rs
+++ b/crates/ui/src/input/text_input.rs
@@ -1,8 +1,8 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
- div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
- IntoElement, IsZero, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement,
- Styled, Window,
+ div, px, relative, AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity,
+ InteractiveElement as _, IntoElement, IsZero, MouseButton, ParentElement as _, Pixels, Rems,
+ RenderOnce, StyleRefinement, Styled, Window,
};
use crate::button::{Button, ButtonVariants as _};
@@ -10,8 +10,8 @@ use crate::indicator::Indicator;
use crate::input::clear_button;
use crate::input::element::{LINE_NUMBER_RIGHT_MARGIN, RIGHT_MARGIN};
use crate::scroll::Scrollbar;
-use crate::ActiveTheme;
use crate::{h_flex, StyledExt};
+use crate::{v_flex, ActiveTheme};
use crate::{IconName, Size};
use crate::{Sizable, StyleSized};
@@ -139,6 +139,76 @@ impl TextInput {
}
})
}
+
+ /// This method must after the refine_style.
+ fn render_editor(
+ paddings: EdgesRefinement,
+ input_state: &Entity,
+ state: &InputState,
+ window: &Window,
+ _cx: &App,
+ ) -> impl IntoElement {
+ let base_size = window.text_style().font_size;
+ let rem_size = window.rem_size();
+
+ let paddings = Edges {
+ left: paddings
+ .left
+ .map(|v| v.to_pixels(base_size, rem_size))
+ .unwrap_or(px(0.)),
+ right: paddings
+ .right
+ .map(|v| v.to_pixels(base_size, rem_size))
+ .unwrap_or(px(0.)),
+ top: paddings
+ .top
+ .map(|v| v.to_pixels(base_size, rem_size))
+ .unwrap_or(px(0.)),
+ bottom: paddings
+ .bottom
+ .map(|v| v.to_pixels(base_size, rem_size))
+ .unwrap_or(px(0.)),
+ };
+
+ const MIN_SCROLL_PADDING: Pixels = px(2.0);
+
+ v_flex()
+ .size_full()
+ .children(state.search_panel.clone())
+ .child(div().flex_1().child(input_state.clone()).map(|this| {
+ if let Some(last_layout) = state.last_layout.as_ref() {
+ let left = if last_layout.line_number_width.is_zero() {
+ px(0.)
+ } else {
+ // Align left edge to the Line number.
+ paddings.left + last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN
+ };
+
+ let scroll_size = gpui::Size {
+ width: state.scroll_size.width - left + paddings.right + RIGHT_MARGIN,
+ height: state.scroll_size.height,
+ };
+
+ let scrollbar = if !state.soft_wrap {
+ Scrollbar::both(&state.scroll_state, &state.scroll_handle)
+ } else {
+ Scrollbar::vertical(&state.scroll_state, &state.scroll_handle)
+ };
+
+ this.relative().child(
+ div()
+ .absolute()
+ .top(-paddings.top + MIN_SCROLL_PADDING)
+ .left(left)
+ .right(-paddings.right + MIN_SCROLL_PADDING)
+ .bottom(-paddings.bottom + MIN_SCROLL_PADDING)
+ .child(scrollbar.scroll_size(scroll_size)),
+ )
+ } else {
+ this
+ }
+ }))
+ }
}
impl Styled for TextInput {
@@ -233,6 +303,7 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::select_to_end))
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
.on_action(window.listener_for(&self.state, InputState::copy))
+ .on_action(window.listener_for(&self.state, InputState::on_action_search))
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
.on_mouse_down(
MouseButton::Left,
@@ -246,10 +317,12 @@ impl RenderOnce for TextInput {
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
.size_full()
.line_height(LINE_HEIGHT)
+ .input_px(self.size)
.input_py(self.size)
.input_h(self.size)
.cursor_text()
.text_size(font_size)
+ .items_center()
.when(state.mode.is_multi_line(), |this| {
this.h_auto()
.when_some(self.height, |this, height| this.h(height))
@@ -266,11 +339,23 @@ impl RenderOnce for TextInput {
})
})
})
- .input_px(self.size)
.items_center()
.gap(gap_x)
+ .refine_style(&self.style)
.children(prefix)
- .child(self.state.clone())
+ .when(state.mode.is_multi_line(), |mut this| {
+ let paddings = this.style().padding.clone();
+ this.child(Self::render_editor(
+ paddings,
+ &self.state,
+ &state,
+ window,
+ cx,
+ ))
+ })
+ .when(!state.mode.is_multi_line(), |this| {
+ this.child(self.state.clone())
+ })
.when(has_suffix, |this| {
this.pr(self.size.input_px() / 2.).child(
h_flex()
@@ -297,62 +382,5 @@ impl RenderOnce for TextInput {
.children(suffix),
)
})
- .refine_style(&self.style)
- .when(state.mode.is_multi_line(), |mut this| {
- let paddings = this.style().padding.clone();
- let base_size = window.text_style().font_size;
- let rem_size = window.rem_size();
-
- let paddings = gpui::Edges {
- left: paddings
- .left
- .map(|v| v.to_pixels(base_size, rem_size))
- .unwrap_or(px(0.)),
- right: paddings
- .right
- .map(|v| v.to_pixels(base_size, rem_size))
- .unwrap_or(px(0.)),
- top: paddings
- .top
- .map(|v| v.to_pixels(base_size, rem_size))
- .unwrap_or(px(0.)),
- bottom: paddings
- .bottom
- .map(|v| v.to_pixels(base_size, rem_size))
- .unwrap_or(px(0.)),
- };
-
- if let Some(last_layout) = state.last_layout.as_ref() {
- let left = if last_layout.line_number_width.is_zero() {
- px(0.)
- } else {
- // Align left edge to the Line number.
- paddings.left + last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN
- };
-
- let scroll_size = gpui::Size {
- width: state.scroll_size.width - left + paddings.right + RIGHT_MARGIN,
- height: state.scroll_size.height,
- };
-
- let scrollbar = if !state.soft_wrap {
- Scrollbar::both(&state.scroll_state, &state.scroll_handle)
- } else {
- Scrollbar::vertical(&state.scroll_state, &state.scroll_handle)
- };
-
- this.relative().child(
- div()
- .absolute()
- .top_0()
- .left(left)
- .right_0()
- .bottom_0()
- .child(scrollbar.scroll_size(scroll_size)),
- )
- } else {
- this
- }
- })
}
}
diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs
index c364dc53..1fdd08a0 100644
--- a/crates/ui/src/styled.rs
+++ b/crates/ui/src/styled.rs
@@ -333,11 +333,11 @@ impl Size {
pub fn input_py(&self) -> Pixels {
match self {
- Size::Large => px(16.),
- Size::Medium => px(8.),
- Size::Small => px(4.),
+ Size::Large => px(10.),
+ Size::Medium => px(5.),
+ Size::Small => px(2.),
Size::XSmall => px(0.),
- _ => px(4.),
+ _ => px(2.),
}
}
}
diff --git a/themes/catppuccin.json b/themes/catppuccin.json
index ca026f49..b494484c 100644
--- a/themes/catppuccin.json
+++ b/themes/catppuccin.json
@@ -584,7 +584,6 @@
"editor.active_line.background": "#363a4f",
"editor.line_number": "#b8c0e0",
"editor.active_line_number": "#cad3f5",
-
"conflict": "#ed8796",
"created": "#a6da95",
"deleted": "#ed8796",
@@ -730,7 +729,7 @@
"highlight": {
"editor.foreground": "#cdd6f4",
"editor.background": "#181825",
- "editor.active_line.background": "#302d41",
+ "editor.active_line.background": "#222230AA",
"editor.line_number": "#6c7086",
"editor.active_line_number": "#cdd6f4",
"conflict": "#f38ba8",