input: Add to support search and replace. (#1252)
<img width="806" height="765" alt="image" src="https://github.com/user-attachments/assets/5e2a7144-f814-4874-b9c1-89ebf5d942ec" /> - Close #1212 to support search for Input. - Fix #1235 to support scroll to cursor when go to line.
This commit is contained in:
parent
e01b139995
commit
8a3ef51ea1
16 changed files with 883 additions and 156 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3206,6 +3206,7 @@ dependencies = [
|
||||||
name = "gpui-component"
|
name = "gpui-component"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"enum-iterator",
|
"enum-iterator",
|
||||||
|
|
|
||||||
1
assets/icons/case-sensitive.svg
Normal file
1
assets/icons/case-sensitive.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-case-sensitive-icon lucide-case-sensitive"><path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"/><path d="M22 9v7"/><path d="M3.304 13h6.392"/><circle cx="18.5" cy="12.5" r="3.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 387 B |
16
assets/icons/replace.svg
Normal file
16
assets/icons/replace.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-replace-icon lucide-replace"
|
||||||
|
><path d="M14 4a2 2 0 0 1 2-2" /><path d="M16 10a2 2 0 0 1-2-2" /><path
|
||||||
|
d="M20 2a2 2 0 0 1 2 2"
|
||||||
|
/><path d="M22 8a2 2 0 0 1-2 2" /><path d="m3 7 3 3 3-3" /><path
|
||||||
|
d="M6 10V5a3 3 0 0 1 3-3h1"
|
||||||
|
/><rect x="2" y="14" width="8" height="8" rx="2" /></svg>
|
||||||
|
After Width: | Height: | Size: 542 B |
|
|
@ -137,11 +137,7 @@ impl CompletionProvider for ExampleLspStore {
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<InputState>,
|
cx: &mut Context<InputState>,
|
||||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||||
let trigger_character = trigger
|
let trigger_character = trigger.trigger_character.unwrap_or_default();
|
||||||
.trigger_character
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
if trigger_character.is_empty() {
|
if trigger_character.is_empty() {
|
||||||
return Task::ready(Ok(vec![]));
|
return Task::ready(Ok(vec![]));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ impl Example {
|
||||||
tab_size: 2,
|
tab_size: 2,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
.searchable(true)
|
||||||
.placeholder("Enter your Markdown here...")
|
.placeholder("Enter your Markdown here...")
|
||||||
.default_value(EXAMPLE)
|
.default_value(EXAMPLE)
|
||||||
});
|
});
|
||||||
|
|
@ -66,6 +67,7 @@ impl Render for Example {
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.input_state)
|
TextInput::new(&self.input_state)
|
||||||
.h_full()
|
.h_full()
|
||||||
|
.p_0()
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.focus_bordered(false),
|
.focus_bordered(false),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -54,28 +54,30 @@ impl TextareaStory {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line()
|
||||||
.rows(10)
|
.rows(10)
|
||||||
.placeholder("Enter text here...").default_value(
|
.placeholder("Enter text here...")
|
||||||
unindent::unindent(
|
.searchable(true)
|
||||||
r#"Hello 世界,this is GPUI component.
|
.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| {
|
let textarea_auto_grow = cx.new(|cx| {
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ chrono = "0.4.38"
|
||||||
|
|
||||||
# Code Editor
|
# Code Editor
|
||||||
lsp-types.workspace = true
|
lsp-types.workspace = true
|
||||||
|
aho-corasick = "1.1.3"
|
||||||
tree-sitter = "0.25.4"
|
tree-sitter = "0.25.4"
|
||||||
tree-sitter-json = "0.24.8"
|
tree-sitter-json = "0.24.8"
|
||||||
tree-sitter-bash = { version = "0.23.3", optional = true }
|
tree-sitter-bash = { version = "0.23.3", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -155,3 +155,14 @@ List:
|
||||||
zh-CN: 搜索...
|
zh-CN: 搜索...
|
||||||
zh-HK: 搜索...
|
zh-HK: 搜索...
|
||||||
it: Ricerca...
|
it: Ricerca...
|
||||||
|
Input:
|
||||||
|
Replace:
|
||||||
|
en: Replace
|
||||||
|
zh-CN: 替换
|
||||||
|
zh-HK: 替換
|
||||||
|
it: Sostituisci
|
||||||
|
Replace All:
|
||||||
|
en: Replace All
|
||||||
|
zh-CN: 全部替换
|
||||||
|
zh-HK: 全部替換
|
||||||
|
it: Sostituisci tutto
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,14 @@ pub enum IconName {
|
||||||
Bot,
|
Bot,
|
||||||
Building2,
|
Building2,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
CaseSensitive,
|
||||||
ChartPie,
|
ChartPie,
|
||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronUp,
|
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
|
ChevronUp,
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleUser,
|
CircleUser,
|
||||||
CircleX,
|
CircleX,
|
||||||
|
|
@ -68,6 +69,7 @@ pub enum IconName {
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
Plus,
|
Plus,
|
||||||
|
Replace,
|
||||||
ResizeCorner,
|
ResizeCorner,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -102,6 +104,7 @@ impl IconName {
|
||||||
Self::Bot => "icons/bot.svg",
|
Self::Bot => "icons/bot.svg",
|
||||||
Self::Building2 => "icons/building-2.svg",
|
Self::Building2 => "icons/building-2.svg",
|
||||||
Self::Calendar => "icons/calendar.svg",
|
Self::Calendar => "icons/calendar.svg",
|
||||||
|
Self::CaseSensitive => "icons/case-sensitive.svg",
|
||||||
Self::ChartPie => "icons/chart-pie.svg",
|
Self::ChartPie => "icons/chart-pie.svg",
|
||||||
Self::Check => "icons/check.svg",
|
Self::Check => "icons/check.svg",
|
||||||
Self::ChevronDown => "icons/chevron-down.svg",
|
Self::ChevronDown => "icons/chevron-down.svg",
|
||||||
|
|
@ -152,6 +155,7 @@ impl IconName {
|
||||||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||||
Self::Plus => "icons/plus.svg",
|
Self::Plus => "icons/plus.svg",
|
||||||
|
Self::Replace => "icons/replace.svg",
|
||||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
|
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
|
||||||
ActiveTheme as _, Root,
|
ActiveTheme as _, Colorize, Root,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{mode::InputMode, InputState, LastLayout};
|
use super::{mode::InputMode, InputState, LastLayout};
|
||||||
|
|
@ -76,8 +76,8 @@ impl TextElement {
|
||||||
let line_number_width = last_layout.line_number_width;
|
let line_number_width = last_layout.line_number_width;
|
||||||
|
|
||||||
let mut selected_range = state.selected_range;
|
let mut selected_range = state.selected_range;
|
||||||
if let Some(marked_range) = &state.marked_range {
|
if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||||
selected_range = (marked_range.end..marked_range.end).into();
|
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursor = state.cursor();
|
let cursor = state.cursor();
|
||||||
|
|
@ -230,35 +230,29 @@ impl TextElement {
|
||||||
(cursor_bounds, scroll_offset, current_row)
|
(cursor_bounds, scroll_offset, current_row)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_selections(
|
fn layout_match_range(
|
||||||
&self,
|
range: Range<usize>,
|
||||||
last_layout: &LastLayout,
|
last_layout: &LastLayout,
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Option<Path<Pixels>> {
|
) -> Option<Path<Pixels>> {
|
||||||
let line_height = last_layout.line_height;
|
if range.is_empty() {
|
||||||
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() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (start_ix, end_ix) = if selected_range.start < selected_range.end {
|
if range.start < last_layout.visible_range_offset.start
|
||||||
(selected_range.start, selected_range.end)
|
|| range.end > last_layout.visible_range_offset.end
|
||||||
} else {
|
{
|
||||||
(selected_range.end, selected_range.start)
|
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 prev_lines_offset = visible_start_offset;
|
||||||
let mut offset_y = visible_top;
|
let mut offset_y = visible_top;
|
||||||
|
|
@ -369,6 +363,62 @@ impl TextElement {
|
||||||
builder.build().ok()
|
builder.build().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_search_matches(
|
||||||
|
&self,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
bounds: &mut Bounds<Pixels>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<(Path<Pixels>, 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<Pixels>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Path<Pixels>> {
|
||||||
|
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.
|
/// Calculate the visible range of lines in the viewport.
|
||||||
///
|
///
|
||||||
/// Returns
|
/// Returns
|
||||||
|
|
@ -470,6 +520,7 @@ pub(super) struct PrepaintState {
|
||||||
/// row index (zero based), no wrap, same line as the cursor.
|
/// row index (zero based), no wrap, same line as the cursor.
|
||||||
current_row: Option<usize>,
|
current_row: Option<usize>,
|
||||||
selection_path: Option<Path<Pixels>>,
|
selection_path: Option<Path<Pixels>>,
|
||||||
|
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,10 +655,10 @@ impl Element for TextElement {
|
||||||
|
|
||||||
// Calculate the width of the line numbers
|
// Calculate the width of the line numbers
|
||||||
let empty_line_number = window.text_system().shape_line(
|
let empty_line_number = window.text_system().shape_line(
|
||||||
"+++++".into(),
|
"++++++".into(),
|
||||||
font_size,
|
font_size,
|
||||||
&[TextRun {
|
&[TextRun {
|
||||||
len: 5,
|
len: 6,
|
||||||
font: style.font(),
|
font: style.font(),
|
||||||
color: gpui::black(),
|
color: gpui::black(),
|
||||||
background_color: None,
|
background_color: None,
|
||||||
|
|
@ -649,8 +700,10 @@ impl Element for TextElement {
|
||||||
|
|
||||||
runs.extend(highlight_styles.iter().map(|(range, style)| {
|
runs.extend(highlight_styles.iter().map(|(range, style)| {
|
||||||
let mut run = text_style.clone().highlight(*style).to_run(range.len());
|
let mut run = text_style.clone().highlight(*style).to_run(range.len());
|
||||||
if let Some(marked_range) = &state.marked_range {
|
if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||||
if range.start >= marked_range.start && range.end <= marked_range.end {
|
if range.start >= ime_marked_range.start
|
||||||
|
&& range.end <= ime_marked_range.end
|
||||||
|
{
|
||||||
run.color = marked_run.color;
|
run.color = marked_run.color;
|
||||||
run.strikethrough = marked_run.strikethrough;
|
run.strikethrough = marked_run.strikethrough;
|
||||||
run.underline = marked_run.underline;
|
run.underline = marked_run.underline;
|
||||||
|
|
@ -664,20 +717,20 @@ impl Element for TextElement {
|
||||||
} else {
|
} else {
|
||||||
vec![run]
|
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
|
// IME marked text
|
||||||
vec![
|
vec![
|
||||||
TextRun {
|
TextRun {
|
||||||
len: marked_range.start,
|
len: ime_marked_range.start,
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
TextRun {
|
TextRun {
|
||||||
len: marked_range.end - marked_range.start,
|
len: ime_marked_range.end - ime_marked_range.start,
|
||||||
underline: marked_run.underline,
|
underline: marked_run.underline,
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
TextRun {
|
TextRun {
|
||||||
len: display_text.len() - marked_range.end,
|
len: display_text.len() - ime_marked_range.end,
|
||||||
..run.clone()
|
..run.clone()
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -689,7 +742,7 @@ impl Element for TextElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
let wrap_width = if multi_line && state.soft_wrap {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -706,8 +759,8 @@ impl Element for TextElement {
|
||||||
.expect("failed to shape text");
|
.expect("failed to shape text");
|
||||||
// measure.end();
|
// measure.end();
|
||||||
|
|
||||||
let mut longest_line_width = px(0.);
|
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
|
||||||
if state.mode.is_multi_line() && lines.len() > 1 {
|
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
|
||||||
let longtest_line: SharedString = state
|
let longtest_line: SharedString = state
|
||||||
.text
|
.text
|
||||||
.line(state.text.summary().longest_row as usize)
|
.line(state.text.summary().longest_row as usize)
|
||||||
|
|
@ -750,7 +803,7 @@ impl Element for TextElement {
|
||||||
let mut last_layout = LastLayout {
|
let mut last_layout = LastLayout {
|
||||||
visible_range,
|
visible_range,
|
||||||
visible_top,
|
visible_top,
|
||||||
visible_start_offset,
|
visible_range_offset: visible_start_offset..visible_end_offset,
|
||||||
line_height,
|
line_height,
|
||||||
wrap_width,
|
wrap_width,
|
||||||
line_number_width,
|
line_number_width,
|
||||||
|
|
@ -793,7 +846,8 @@ impl Element for TextElement {
|
||||||
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
||||||
last_layout.cursor_bounds = cursor_bounds;
|
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 state = self.state.read(cx);
|
||||||
let line_numbers = if state.mode.line_number() {
|
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 ix = last_layout.visible_range.start + ix;
|
||||||
let line_no = ix + 1;
|
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() {
|
if !line.wrap_boundaries.is_empty() {
|
||||||
line_no_text.push_str(&"\n ".repeat(line.wrap_boundaries.len()));
|
line_no_text.push_str(&"\n ".repeat(line.wrap_boundaries.len()));
|
||||||
}
|
}
|
||||||
|
|
@ -852,6 +906,7 @@ impl Element for TextElement {
|
||||||
cursor_scroll_offset,
|
cursor_scroll_offset,
|
||||||
current_row,
|
current_row,
|
||||||
selection_path,
|
selection_path,
|
||||||
|
search_match_paths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -948,6 +1003,14 @@ impl Element for TextElement {
|
||||||
|
|
||||||
// Paint selections
|
// Paint selections
|
||||||
if window.is_window_active() {
|
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() {
|
if let Some(path) = prepaint.selection_path.take() {
|
||||||
window.paint_path(path, cx.theme().selection);
|
window.paint_path(path, cx.theme().selection);
|
||||||
}
|
}
|
||||||
|
|
@ -986,11 +1049,7 @@ impl Element for TextElement {
|
||||||
input_bounds.size.height,
|
input_bounds.size.height,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
cx.theme()
|
cx.theme().background,
|
||||||
.highlight_theme
|
|
||||||
.style
|
|
||||||
.background
|
|
||||||
.unwrap_or(cx.theme().input),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
// Each item is the normal lines.
|
// Each item is the normal lines.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ mod number_input;
|
||||||
mod otp_input;
|
mod otp_input;
|
||||||
mod popovers;
|
mod popovers;
|
||||||
mod rope_ext;
|
mod rope_ext;
|
||||||
|
mod search;
|
||||||
mod state;
|
mod state;
|
||||||
mod text_input;
|
mod text_input;
|
||||||
mod text_wrapper;
|
mod text_wrapper;
|
||||||
|
|
|
||||||
547
crates/ui/src/input/search.rs
Normal file
547
crates/ui/src/input/search.rs
Normal file
|
|
@ -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<AhoCorasick>,
|
||||||
|
|
||||||
|
pub(super) matched_ranges: Rc<Vec<Range<usize>>>,
|
||||||
|
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<Range<usize>> {
|
||||||
|
self.matched_ranges.get(self.current_match_ix + 1).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for SearchMatcher {
|
||||||
|
type Item = Range<usize>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
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<Self::Item> {
|
||||||
|
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<InputState>,
|
||||||
|
search_input: Entity<InputState>,
|
||||||
|
replace_input: Entity<InputState>,
|
||||||
|
case_insensitive: bool,
|
||||||
|
replace_mode: bool,
|
||||||
|
matcher: SearchMatcher,
|
||||||
|
|
||||||
|
open: bool,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self>,
|
||||||
|
) {
|
||||||
|
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<InputState>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
|
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>,
|
||||||
|
) {
|
||||||
|
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<Self>) {
|
||||||
|
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>) {
|
||||||
|
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>) {
|
||||||
|
self.prev(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_next(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.next(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.hide(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_tab(&mut self, _: &IndentInline, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.text_state.focus_handle(cx).focus(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_text_selection(&mut self, cx: &mut Context<Self>) {
|
||||||
|
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<Self>) {
|
||||||
|
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<Self>) {
|
||||||
|
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<Self>) {
|
||||||
|
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<Self>) {
|
||||||
|
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<Self>) -> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, point, prelude::FluentBuilder as _, px, Action, App, AppContext, Bounds,
|
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,
|
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
|
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
|
||||||
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
|
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
|
||||||
|
|
@ -31,6 +31,7 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
popovers::{ContextMenu, DiagnosticPopover},
|
popovers::{ContextMenu, DiagnosticPopover},
|
||||||
|
search::{self, SearchPanel},
|
||||||
Position,
|
Position,
|
||||||
};
|
};
|
||||||
use crate::input::{RopeExt as _, Selection};
|
use crate::input::{RopeExt as _, Selection};
|
||||||
|
|
@ -90,6 +91,7 @@ actions!(
|
||||||
MoveToNextWord,
|
MoveToNextWord,
|
||||||
Escape,
|
Escape,
|
||||||
ToggleCodeActions,
|
ToggleCodeActions,
|
||||||
|
Search,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -216,8 +218,13 @@ pub fn init(cx: &mut App) {
|
||||||
KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
|
KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
|
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);
|
number_input::init(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,8 +234,8 @@ pub(super) struct LastLayout {
|
||||||
pub(super) visible_range: Range<usize>,
|
pub(super) visible_range: Range<usize>,
|
||||||
/// The first visible line top position in scroll viewport.
|
/// The first visible line top position in scroll viewport.
|
||||||
pub(super) visible_top: Pixels,
|
pub(super) visible_top: Pixels,
|
||||||
/// The start byte offset of the first visible line.
|
/// The range of byte offset of the visible lines.
|
||||||
pub(super) visible_start_offset: usize,
|
pub(super) visible_range_offset: Range<usize>,
|
||||||
/// The last layout lines (Only have visible lines).
|
/// The last layout lines (Only have visible lines).
|
||||||
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
||||||
/// The line_height of text layout, this will change will InputElement painted.
|
/// The line_height of text layout, this will change will InputElement painted.
|
||||||
|
|
@ -255,11 +262,13 @@ pub struct InputState {
|
||||||
/// - "Hello 世界💝" = 16
|
/// - "Hello 世界💝" = 16
|
||||||
/// - "💝" = 4
|
/// - "💝" = 4
|
||||||
pub(super) selected_range: Selection,
|
pub(super) selected_range: Selection,
|
||||||
|
pub(super) search_panel: Option<Entity<SearchPanel>>,
|
||||||
|
pub(super) searchable: bool,
|
||||||
/// Range for save the selected word, use to keep word range when drag move.
|
/// Range for save the selected word, use to keep word range when drag move.
|
||||||
pub(super) selected_word_range: Option<Selection>,
|
pub(super) selected_word_range: Option<Selection>,
|
||||||
pub(super) selection_reversed: bool,
|
pub(super) selection_reversed: bool,
|
||||||
/// The marked range is the temporary insert text on IME typing.
|
/// The marked range is the temporary insert text on IME typing.
|
||||||
pub(super) marked_range: Option<Selection>,
|
pub(super) ime_marked_range: Option<Selection>,
|
||||||
pub(super) last_layout: Option<LastLayout>,
|
pub(super) last_layout: Option<LastLayout>,
|
||||||
pub(super) last_cursor: Option<usize>,
|
pub(super) last_cursor: Option<usize>,
|
||||||
/// The input container bounds
|
/// The input container bounds
|
||||||
|
|
@ -342,9 +351,11 @@ impl InputState {
|
||||||
blink_cursor,
|
blink_cursor,
|
||||||
history,
|
history,
|
||||||
selected_range: Selection::default(),
|
selected_range: Selection::default(),
|
||||||
|
search_panel: None,
|
||||||
|
searchable: false,
|
||||||
selected_word_range: None,
|
selected_word_range: None,
|
||||||
selection_reversed: false,
|
selection_reversed: false,
|
||||||
marked_range: None,
|
ime_marked_range: None,
|
||||||
input_bounds: Bounds::default(),
|
input_bounds: Bounds::default(),
|
||||||
selecting: false,
|
selecting: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
@ -425,6 +436,13 @@ impl InputState {
|
||||||
code_action_providers: vec![],
|
code_action_providers: vec![],
|
||||||
completion_provider: None,
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -634,7 +652,7 @@ impl InputState {
|
||||||
};
|
};
|
||||||
let line_height = last_layout.line_height;
|
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;
|
let mut y_offset = last_layout.visible_top;
|
||||||
for (line_index, line) in last_layout.lines.iter().enumerate() {
|
for (line_index, line) in last_layout.lines.iter().enumerate() {
|
||||||
let local_offset = offset.saturating_sub(prev_lines_offset);
|
let local_offset = offset.saturating_sub(prev_lines_offset);
|
||||||
|
|
@ -1049,7 +1067,7 @@ impl InputState {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.move_to(0, window, cx);
|
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<Self>) {
|
pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
|
@ -1549,7 +1567,7 @@ impl InputState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.marked_range.is_some() {
|
if self.ime_marked_range.is_some() {
|
||||||
self.unmark_text(window, cx);
|
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)
|
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
|
||||||
// Clear the marked range.
|
// Clear the marked range.
|
||||||
if let Some(marked_range) = &self.marked_range {
|
if let Some(ime_marked_range) = &self.ime_marked_range {
|
||||||
if marked_range.len() == 0 {
|
if ime_marked_range.len() == 0 {
|
||||||
self.marked_range = None;
|
self.ime_marked_range = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1674,6 +1692,41 @@ impl InputState {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn scroll_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||||
|
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(
|
pub(super) fn show_character_palette(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ShowCharacterPalette,
|
_: &ShowCharacterPalette,
|
||||||
|
|
@ -1756,6 +1809,7 @@ impl InputState {
|
||||||
fn move_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
fn move_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
let offset = offset.clamp(0, self.text.len());
|
let offset = offset.clamp(0, self.text.len());
|
||||||
self.selected_range = (offset..offset).into();
|
self.selected_range = (offset..offset).into();
|
||||||
|
self.scroll_to(offset, cx);
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
self.update_preferred_column();
|
self.update_preferred_column();
|
||||||
self.hide_context_menu(cx);
|
self.hide_context_menu(cx);
|
||||||
|
|
@ -1766,8 +1820,8 @@ impl InputState {
|
||||||
///
|
///
|
||||||
/// The offset is the UTF-8 offset.
|
/// The offset is the UTF-8 offset.
|
||||||
pub fn cursor(&self) -> usize {
|
pub fn cursor(&self) -> usize {
|
||||||
if let Some(marked_range) = &self.marked_range {
|
if let Some(ime_marked_range) = &self.ime_marked_range {
|
||||||
return marked_range.end;
|
return ime_marked_range.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.selection_reversed {
|
if self.selection_reversed {
|
||||||
|
|
@ -1809,7 +1863,7 @@ impl InputState {
|
||||||
// - included the scroll offset.
|
// - included the scroll offset.
|
||||||
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
|
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;
|
let mut y_offset = last_layout.visible_top;
|
||||||
for (ix, line) in self
|
for (ix, line) in self
|
||||||
.text_wrapper
|
.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 {
|
impl EntityInputHandler for InputState {
|
||||||
|
|
@ -2215,12 +2273,12 @@ impl EntityInputHandler for InputState {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<Range<usize>> {
|
) -> Option<Range<usize>> {
|
||||||
self.marked_range
|
self.ime_marked_range
|
||||||
.map(|range| self.range_to_utf16(&range.into()))
|
.map(|range| self.range_to_utf16(&range.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||||
self.marked_range = None;
|
self.ime_marked_range = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace text in range.
|
/// Replace text in range.
|
||||||
|
|
@ -2243,7 +2301,7 @@ impl EntityInputHandler for InputState {
|
||||||
let range = range_utf16
|
let range = range_utf16
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.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());
|
.unwrap_or(self.selected_range.into());
|
||||||
|
|
||||||
let old_text = self.text.clone();
|
let old_text = self.text.clone();
|
||||||
|
|
@ -2273,9 +2331,10 @@ impl EntityInputHandler for InputState {
|
||||||
self.mode
|
self.mode
|
||||||
.update_highlighter(&range, &self.text, &new_text, true, cx);
|
.update_highlighter(&range, &self.text, &new_text, true, cx);
|
||||||
self.selected_range = (new_offset..new_offset).into();
|
self.selected_range = (new_offset..new_offset).into();
|
||||||
self.marked_range.take();
|
self.ime_marked_range.take();
|
||||||
self.update_preferred_column();
|
self.update_preferred_column();
|
||||||
self.update_scroll_offset(None, cx);
|
self.update_scroll_offset(None, cx);
|
||||||
|
self.update_search(cx);
|
||||||
self.mode.update_auto_grow(&self.text_wrapper);
|
self.mode.update_auto_grow(&self.text_wrapper);
|
||||||
self.handle_completion_trigger(&range, &new_text, window, cx);
|
self.handle_completion_trigger(&range, &new_text, window, cx);
|
||||||
cx.emit(InputEvent::Change);
|
cx.emit(InputEvent::Change);
|
||||||
|
|
@ -2298,7 +2357,7 @@ impl EntityInputHandler for InputState {
|
||||||
let range = range_utf16
|
let range = range_utf16
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.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());
|
.unwrap_or(self.selected_range.into());
|
||||||
|
|
||||||
let old_text = self.text.clone();
|
let old_text = self.text.clone();
|
||||||
|
|
@ -2320,9 +2379,9 @@ impl EntityInputHandler for InputState {
|
||||||
if new_text.is_empty() {
|
if new_text.is_empty() {
|
||||||
// Cancel selection, when cancel IME input.
|
// Cancel selection, when cancel IME input.
|
||||||
self.selected_range = (range.start..range.start).into();
|
self.selected_range = (range.start..range.start).into();
|
||||||
self.marked_range = None;
|
self.ime_marked_range = None;
|
||||||
} else {
|
} 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
|
self.selected_range = new_selected_range_utf16
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
|
|
@ -2352,7 +2411,7 @@ impl EntityInputHandler for InputState {
|
||||||
let mut end_origin = None;
|
let mut end_origin = None;
|
||||||
let line_number_origin = point(line_number_width, px(0.));
|
let line_number_origin = point(line_number_width, px(0.));
|
||||||
let mut y_offset = last_layout.visible_top;
|
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() {
|
for line in last_layout.lines.iter() {
|
||||||
if start_origin.is_some() && end_origin.is_some() {
|
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 last_layout = self.last_layout.as_ref()?;
|
||||||
let line_height = last_layout.line_height;
|
let line_height = last_layout.line_height;
|
||||||
let line_point = self.last_bounds?.localize(&point)?;
|
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() {
|
for line in last_layout.lines.iter() {
|
||||||
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
div, px, relative, AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity,
|
||||||
IntoElement, IsZero, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement,
|
InteractiveElement as _, IntoElement, IsZero, MouseButton, ParentElement as _, Pixels, Rems,
|
||||||
Styled, Window,
|
RenderOnce, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
|
|
@ -10,8 +10,8 @@ use crate::indicator::Indicator;
|
||||||
use crate::input::clear_button;
|
use crate::input::clear_button;
|
||||||
use crate::input::element::{LINE_NUMBER_RIGHT_MARGIN, RIGHT_MARGIN};
|
use crate::input::element::{LINE_NUMBER_RIGHT_MARGIN, RIGHT_MARGIN};
|
||||||
use crate::scroll::Scrollbar;
|
use crate::scroll::Scrollbar;
|
||||||
use crate::ActiveTheme;
|
|
||||||
use crate::{h_flex, StyledExt};
|
use crate::{h_flex, StyledExt};
|
||||||
|
use crate::{v_flex, ActiveTheme};
|
||||||
use crate::{IconName, Size};
|
use crate::{IconName, Size};
|
||||||
use crate::{Sizable, StyleSized};
|
use crate::{Sizable, StyleSized};
|
||||||
|
|
||||||
|
|
@ -139,6 +139,76 @@ impl TextInput {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This method must after the refine_style.
|
||||||
|
fn render_editor(
|
||||||
|
paddings: EdgesRefinement<DefiniteLength>,
|
||||||
|
input_state: &Entity<InputState>,
|
||||||
|
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 {
|
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::select_to_end))
|
||||||
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
|
.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::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_key_down(window.listener_for(&self.state, InputState::on_key_down))
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
|
|
@ -246,10 +317,12 @@ impl RenderOnce for TextInput {
|
||||||
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
||||||
.size_full()
|
.size_full()
|
||||||
.line_height(LINE_HEIGHT)
|
.line_height(LINE_HEIGHT)
|
||||||
|
.input_px(self.size)
|
||||||
.input_py(self.size)
|
.input_py(self.size)
|
||||||
.input_h(self.size)
|
.input_h(self.size)
|
||||||
.cursor_text()
|
.cursor_text()
|
||||||
.text_size(font_size)
|
.text_size(font_size)
|
||||||
|
.items_center()
|
||||||
.when(state.mode.is_multi_line(), |this| {
|
.when(state.mode.is_multi_line(), |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
.when_some(self.height, |this, height| this.h(height))
|
.when_some(self.height, |this, height| this.h(height))
|
||||||
|
|
@ -266,11 +339,23 @@ impl RenderOnce for TextInput {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.input_px(self.size)
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
|
.refine_style(&self.style)
|
||||||
.children(prefix)
|
.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| {
|
.when(has_suffix, |this| {
|
||||||
this.pr(self.size.input_px() / 2.).child(
|
this.pr(self.size.input_px() / 2.).child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -297,62 +382,5 @@ impl RenderOnce for TextInput {
|
||||||
.children(suffix),
|
.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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -333,11 +333,11 @@ impl Size {
|
||||||
|
|
||||||
pub fn input_py(&self) -> Pixels {
|
pub fn input_py(&self) -> Pixels {
|
||||||
match self {
|
match self {
|
||||||
Size::Large => px(16.),
|
Size::Large => px(10.),
|
||||||
Size::Medium => px(8.),
|
Size::Medium => px(5.),
|
||||||
Size::Small => px(4.),
|
Size::Small => px(2.),
|
||||||
Size::XSmall => px(0.),
|
Size::XSmall => px(0.),
|
||||||
_ => px(4.),
|
_ => px(2.),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -584,7 +584,6 @@
|
||||||
"editor.active_line.background": "#363a4f",
|
"editor.active_line.background": "#363a4f",
|
||||||
"editor.line_number": "#b8c0e0",
|
"editor.line_number": "#b8c0e0",
|
||||||
"editor.active_line_number": "#cad3f5",
|
"editor.active_line_number": "#cad3f5",
|
||||||
|
|
||||||
"conflict": "#ed8796",
|
"conflict": "#ed8796",
|
||||||
"created": "#a6da95",
|
"created": "#a6da95",
|
||||||
"deleted": "#ed8796",
|
"deleted": "#ed8796",
|
||||||
|
|
@ -730,7 +729,7 @@
|
||||||
"highlight": {
|
"highlight": {
|
||||||
"editor.foreground": "#cdd6f4",
|
"editor.foreground": "#cdd6f4",
|
||||||
"editor.background": "#181825",
|
"editor.background": "#181825",
|
||||||
"editor.active_line.background": "#302d41",
|
"editor.active_line.background": "#222230AA",
|
||||||
"editor.line_number": "#6c7086",
|
"editor.line_number": "#6c7086",
|
||||||
"editor.active_line_number": "#cdd6f4",
|
"editor.active_line_number": "#cdd6f4",
|
||||||
"conflict": "#f38ba8",
|
"conflict": "#f38ba8",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue