editor: Improve search to select first match in visible range. (#1258)
This commit is contained in:
parent
a942c57627
commit
51efa766ea
3 changed files with 221 additions and 86 deletions
51
Cargo.lock
generated
51
Cargo.lock
generated
|
|
@ -2979,6 +2979,19 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"libc",
|
||||||
|
"libgit2-sys",
|
||||||
|
"log",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glib"
|
name = "glib"
|
||||||
version = "0.18.5"
|
version = "0.18.5"
|
||||||
|
|
@ -3118,6 +3131,7 @@ dependencies = [
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
"ashpd 0.11.0",
|
"ashpd 0.11.0",
|
||||||
"async-task",
|
"async-task",
|
||||||
|
"backtrace",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
"blade-graphics",
|
"blade-graphics",
|
||||||
"blade-macros",
|
"blade-macros",
|
||||||
|
|
@ -4284,6 +4298,18 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libgit2-sys"
|
||||||
|
version = "0.18.2+1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
|
|
@ -4311,6 +4337,18 @@ dependencies = [
|
||||||
"redox_syscall 0.5.17",
|
"redox_syscall 0.5.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-sys"
|
||||||
|
version = "1.1.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
|
|
@ -9215,11 +9253,13 @@ dependencies = [
|
||||||
"dunce",
|
"dunce",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
|
"git2",
|
||||||
"globset",
|
"globset",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
|
"rand 0.9.2",
|
||||||
"regex",
|
"regex",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"schemars",
|
"schemars",
|
||||||
|
|
@ -9232,10 +9272,21 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tendril",
|
"tendril",
|
||||||
"unicase",
|
"unicase",
|
||||||
|
"util_macros",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "util_macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/zed-industries/zed.git#53b2f37452189870c93d4514604f903d5ed885d9"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.105",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.18.0"
|
version = "1.18.0"
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ tree-sitter-languages = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui = { workspace = true, "features" = ["test-support"] }
|
||||||
sum_tree.workspace = true
|
sum_tree.workspace = true
|
||||||
gpui_macros.workspace = true
|
gpui_macros.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ use rust_i18n::t;
|
||||||
use std::{ops::Range, rc::Rc};
|
use std::{ops::Range, rc::Rc};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::FluentBuilder as _, App, AppContext as _, Context, Empty, Entity,
|
actions, canvas, div, prelude::FluentBuilder as _, App, AppContext as _, Context, Empty,
|
||||||
EntityInputHandler, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement,
|
Entity, EntityInputHandler, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement,
|
||||||
KeyBinding, ParentElement as _, Render, Styled, Subscription, Window,
|
KeyBinding, ParentElement as _, Pixels, Render, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
|
|
||||||
|
|
@ -14,7 +14,8 @@ use crate::{
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
h_flex,
|
h_flex,
|
||||||
input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt, Search, TextInput},
|
input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt, Search, TextInput},
|
||||||
v_flex, ActiveTheme, IconName, Selectable, Sizable,
|
label::Label,
|
||||||
|
v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable,
|
||||||
};
|
};
|
||||||
|
|
||||||
const KEY_CONTEXT: &'static str = "SearchPanel";
|
const KEY_CONTEXT: &'static str = "SearchPanel";
|
||||||
|
|
@ -103,6 +104,23 @@ impl SearchMatcher {
|
||||||
fn peek(&self) -> Option<Range<usize>> {
|
fn peek(&self) -> Option<Range<usize>> {
|
||||||
self.matched_ranges.get(self.current_match_ix + 1).cloned()
|
self.matched_ranges.get(self.current_match_ix + 1).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
if self.len() == 0 {
|
||||||
|
return "0/0".to_string();
|
||||||
|
}
|
||||||
|
format!("{}/{}", self.current_match_ix + 1, self.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the current match index based on the given offset.
|
||||||
|
fn update_cursor_by_offset(&mut self, offset: usize) {
|
||||||
|
for (ix, range) in self.matched_ranges.iter().enumerate() {
|
||||||
|
self.current_match_ix = ix;
|
||||||
|
if range.contains(&offset) || range.end >= offset {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for SearchMatcher {
|
impl Iterator for SearchMatcher {
|
||||||
|
|
@ -141,12 +159,13 @@ impl DoubleEndedIterator for SearchMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct SearchPanel {
|
pub(super) struct SearchPanel {
|
||||||
text_state: Entity<InputState>,
|
editor: Entity<InputState>,
|
||||||
search_input: Entity<InputState>,
|
search_input: Entity<InputState>,
|
||||||
replace_input: Entity<InputState>,
|
replace_input: Entity<InputState>,
|
||||||
case_insensitive: bool,
|
case_insensitive: bool,
|
||||||
replace_mode: bool,
|
replace_mode: bool,
|
||||||
matcher: SearchMatcher,
|
matcher: SearchMatcher,
|
||||||
|
input_width: Pixels,
|
||||||
|
|
||||||
open: bool,
|
open: bool,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
|
@ -181,10 +200,10 @@ impl InputState {
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = self.text.clone();
|
let text = self.text.clone();
|
||||||
let text_state = cx.entity();
|
let editor = cx.entity();
|
||||||
let selected_text = self.selected_text();
|
let selected_text = self.selected_text();
|
||||||
search_panel.update(cx, |this, cx| {
|
search_panel.update(cx, |this, cx| {
|
||||||
this.text_state = text_state;
|
this.editor = editor;
|
||||||
this.matcher.update(&text);
|
this.matcher.update(&text);
|
||||||
this.show(&selected_text, window, cx);
|
this.show(&selected_text, window, cx);
|
||||||
});
|
});
|
||||||
|
|
@ -194,34 +213,33 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchPanel {
|
impl SearchPanel {
|
||||||
pub fn new(text_state: Entity<InputState>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(editor: Entity<InputState>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
let search_input = cx.new(|cx| InputState::new(window, cx));
|
let search_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
let replace_input = cx.new(|cx| InputState::new(window, cx));
|
let replace_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let _subscriptions = vec![cx.subscribe(
|
let _subscriptions =
|
||||||
&search_input,
|
vec![
|
||||||
|this: &mut Self, search_input, ev: &InputEvent, cx| {
|
cx.subscribe(&search_input, |this: &mut Self, _, ev: &InputEvent, cx| {
|
||||||
// Handle search input changes
|
// Handle search input changes
|
||||||
match ev {
|
match ev {
|
||||||
InputEvent::Change => {
|
InputEvent::Change => {
|
||||||
let value = search_input.read(cx).value();
|
this.update_search_query(cx);
|
||||||
this.matcher
|
}
|
||||||
.update_query(value.as_str(), this.case_insensitive);
|
_ => {}
|
||||||
}
|
}
|
||||||
_ => {}
|
}),
|
||||||
}
|
];
|
||||||
},
|
|
||||||
)];
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
text_state,
|
editor,
|
||||||
search_input,
|
search_input,
|
||||||
replace_input,
|
replace_input,
|
||||||
case_insensitive: true,
|
case_insensitive: true,
|
||||||
replace_mode: false,
|
replace_mode: false,
|
||||||
matcher: SearchMatcher::new(),
|
matcher: SearchMatcher::new(),
|
||||||
open: true,
|
open: true,
|
||||||
|
input_width: Pixels::ZERO,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -238,24 +256,35 @@ impl SearchPanel {
|
||||||
|
|
||||||
self.search_input.update(cx, |this, cx| {
|
self.search_input.update(cx, |this, cx| {
|
||||||
if selected_text.len() > 0 {
|
if selected_text.len() > 0 {
|
||||||
|
// Set value will emit to update_search_query
|
||||||
this.set_value(selected_text.to_string(), window, cx);
|
this.set_value(selected_text.to_string(), window, cx);
|
||||||
}
|
}
|
||||||
this.select_all(&super::SelectAll, window, cx);
|
this.select_all(&super::SelectAll, window, cx);
|
||||||
});
|
});
|
||||||
self.update_search(cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_search(&mut self, cx: &mut Context<Self>) {
|
fn update_search_query(&mut self, cx: &mut Context<Self>) {
|
||||||
let query = self.search_input.read(cx).value();
|
let query = self.search_input.read(cx).value();
|
||||||
|
let visible_range_offset = self
|
||||||
|
.editor
|
||||||
|
.read(cx)
|
||||||
|
.last_layout
|
||||||
|
.as_ref()
|
||||||
|
.map(|l| l.visible_range_offset.clone());
|
||||||
|
|
||||||
self.matcher
|
self.matcher
|
||||||
.update_query(query.as_str(), self.case_insensitive);
|
.update_query(query.as_str(), self.case_insensitive);
|
||||||
self.update_text_selection(cx);
|
|
||||||
|
if let Some(visible_range_offset) = visible_range_offset {
|
||||||
|
self.matcher
|
||||||
|
.update_cursor_by_offset(visible_range_offset.start);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.open = false;
|
self.open = false;
|
||||||
self.text_state.read(cx).focus_handle.focus(window);
|
self.editor.read(cx).focus_handle.focus(window);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,32 +301,12 @@ impl SearchPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_action_tab(&mut self, _: &IndentInline, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_action_tab(&mut self, _: &IndentInline, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.text_state.focus_handle(cx).focus(window);
|
self.editor.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>) {
|
fn prev(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(range) = self.matcher.next_back() {
|
if let Some(range) = self.matcher.next_back() {
|
||||||
self.text_state.update(cx, |state, cx| {
|
self.editor.update(cx, |state, cx| {
|
||||||
state.scroll_to(range.start, cx);
|
state.scroll_to(range.start, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +314,7 @@ impl SearchPanel {
|
||||||
|
|
||||||
fn next(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
fn next(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(range) = self.matcher.next() {
|
if let Some(range) = self.matcher.next() {
|
||||||
self.text_state.update(cx, |state, cx| {
|
self.editor.update(cx, |state, cx| {
|
||||||
state.scroll_to(range.end, cx);
|
state.scroll_to(range.end, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +337,7 @@ impl SearchPanel {
|
||||||
.get(self.matcher.current_match_ix)
|
.get(self.matcher.current_match_ix)
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
let text_state = self.text_state.clone();
|
let text_state = self.editor.clone();
|
||||||
|
|
||||||
let next_range = self.matcher.peek().unwrap_or(range.clone());
|
let next_range = self.matcher.peek().unwrap_or(range.clone());
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
|
|
@ -357,10 +366,10 @@ impl SearchPanel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_state = self.text_state.clone();
|
let editor = self.editor.clone();
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
text_state.update(cx, |state, cx| {
|
editor.update(cx, |state, cx| {
|
||||||
// Replace from the end to avoid messing up the ranges.
|
// Replace from the end to avoid messing up the ranges.
|
||||||
let mut rope = state.text.clone();
|
let mut rope = state.text.clone();
|
||||||
for range in ranges.iter().rev() {
|
for range in ranges.iter().rev() {
|
||||||
|
|
@ -392,6 +401,8 @@ impl Render for SearchPanel {
|
||||||
return Empty.into_any_element();
|
return Empty.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let has_matches = self.matcher.len() > 0;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("search-panel")
|
.id("search-panel")
|
||||||
.occlude()
|
.occlude()
|
||||||
|
|
@ -416,27 +427,44 @@ impl Render for SearchPanel {
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().gap_1().child(
|
div()
|
||||||
TextInput::new(&self.search_input)
|
.flex_1()
|
||||||
.focus_bordered(false)
|
.gap_1()
|
||||||
.suffix(
|
.child(
|
||||||
Button::new("case-insensitive")
|
TextInput::new(&self.search_input)
|
||||||
.selected(!self.case_insensitive)
|
.focus_bordered(false)
|
||||||
.xsmall()
|
.suffix(
|
||||||
.compact()
|
Button::new("case-insensitive")
|
||||||
.ghost()
|
.selected(!self.case_insensitive)
|
||||||
.icon(IconName::CaseSensitive)
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.compact()
|
||||||
this.case_insensitive = !this.case_insensitive;
|
.ghost()
|
||||||
this.update_search(cx);
|
.icon(IconName::CaseSensitive)
|
||||||
cx.notify();
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
})),
|
this.case_insensitive = !this.case_insensitive;
|
||||||
|
this.update_search_query(cx);
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.small()
|
||||||
|
.w_full()
|
||||||
|
.shadow_none(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
canvas(
|
||||||
|
{
|
||||||
|
let view = cx.entity();
|
||||||
|
move |bounds, _, cx| {
|
||||||
|
view.update(cx, |r, _| {
|
||||||
|
r.input_width = bounds.size.width
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_, _, _, _| {},
|
||||||
)
|
)
|
||||||
.small()
|
.absolute()
|
||||||
.w_full()
|
.size_full(),
|
||||||
.cleanable()
|
),
|
||||||
.shadow_none(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("replace-mode")
|
Button::new("replace-mode")
|
||||||
|
|
@ -446,7 +474,11 @@ impl Render for SearchPanel {
|
||||||
.selected(self.replace_mode)
|
.selected(self.replace_mode)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.replace_mode = !this.replace_mode;
|
this.replace_mode = !this.replace_mode;
|
||||||
this.replace_input.read(cx).focus_handle.focus(window);
|
if this.replace_mode {
|
||||||
|
this.replace_input.read(cx).focus_handle.focus(window);
|
||||||
|
} else {
|
||||||
|
this.search_input.read(cx).focus_handle.focus(window);
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
@ -455,6 +487,7 @@ impl Render for SearchPanel {
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon(IconName::ChevronLeft)
|
.icon(IconName::ChevronLeft)
|
||||||
|
.disabled(!has_matches)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.prev(window, cx);
|
this.prev(window, cx);
|
||||||
})),
|
})),
|
||||||
|
|
@ -464,11 +497,20 @@ impl Render for SearchPanel {
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon(IconName::ChevronRight)
|
.icon(IconName::ChevronRight)
|
||||||
|
.disabled(!has_matches)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.next(window, cx);
|
this.next(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(div().w_5())
|
.child(
|
||||||
|
Label::new(self.matcher.label())
|
||||||
|
.when(!has_matches, |this| {
|
||||||
|
this.text_color(cx.theme().muted_foreground)
|
||||||
|
})
|
||||||
|
.text_left()
|
||||||
|
.min_w_16(),
|
||||||
|
)
|
||||||
|
.child(div().w_7())
|
||||||
.child(
|
.child(
|
||||||
Button::new("close")
|
Button::new("close")
|
||||||
.xsmall()
|
.xsmall()
|
||||||
|
|
@ -488,13 +530,14 @@ impl Render for SearchPanel {
|
||||||
TextInput::new(&self.replace_input)
|
TextInput::new(&self.replace_input)
|
||||||
.focus_bordered(false)
|
.focus_bordered(false)
|
||||||
.small()
|
.small()
|
||||||
.w_full()
|
.w(self.input_width)
|
||||||
.shadow_none(),
|
.shadow_none(),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("replace-one")
|
Button::new("replace-one")
|
||||||
.small()
|
.small()
|
||||||
.label(t!("Input.Replace"))
|
.label(t!("Input.Replace"))
|
||||||
|
.disabled(!has_matches)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.replace_next(window, cx);
|
this.replace_next(window, cx);
|
||||||
})),
|
})),
|
||||||
|
|
@ -503,6 +546,7 @@ impl Render for SearchPanel {
|
||||||
Button::new("replace-all")
|
Button::new("replace-all")
|
||||||
.small()
|
.small()
|
||||||
.label(t!("Input.Replace All"))
|
.label(t!("Input.Replace All"))
|
||||||
|
.disabled(!has_matches)
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.replace_all(window, cx);
|
this.replace_all(window, cx);
|
||||||
})),
|
})),
|
||||||
|
|
@ -519,12 +563,12 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_search() {
|
fn test_search() {
|
||||||
let mut search = SearchMatcher::new();
|
let mut matcher = SearchMatcher::new();
|
||||||
search.update(&Rope::from("Hello 世界 this is a Is test string."));
|
matcher.update(&Rope::from("Hello 世界 this is a Is test string."));
|
||||||
search.update_query("Is", true);
|
matcher.update_query("Is", true);
|
||||||
|
|
||||||
assert_eq!(search.len(), 3);
|
assert_eq!(matcher.len(), 3);
|
||||||
let mut matches = search.clone().into_iter();
|
let mut matches = matcher.clone().into_iter();
|
||||||
assert_eq!(matches.current_match_ix, 0);
|
assert_eq!(matches.current_match_ix, 0);
|
||||||
assert_eq!(matches.next(), Some(18..20));
|
assert_eq!(matches.next(), Some(18..20));
|
||||||
assert_eq!(matches.next(), Some(23..25));
|
assert_eq!(matches.next(), Some(23..25));
|
||||||
|
|
@ -539,9 +583,49 @@ mod tests {
|
||||||
assert_eq!(matches.current_match_ix, 0);
|
assert_eq!(matches.current_match_ix, 0);
|
||||||
assert_eq!(matches.next_back(), Some(23..25));
|
assert_eq!(matches.next_back(), Some(23..25));
|
||||||
|
|
||||||
search.update_query("IS", false);
|
matcher.update_query("IS", false);
|
||||||
assert_eq!(search.len(), 0);
|
assert_eq!(matcher.len(), 0);
|
||||||
assert_eq!(search.next(), None);
|
assert_eq!(matcher.next(), None);
|
||||||
assert_eq!(search.next_back(), None);
|
assert_eq!(matcher.next_back(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_label() {
|
||||||
|
let mut matcher = SearchMatcher::new();
|
||||||
|
matcher.update(&Rope::from("Hello 世界 this is a Is test string."));
|
||||||
|
matcher.update_query("Is", true);
|
||||||
|
assert_eq!(matcher.label(), "1/3");
|
||||||
|
matcher.next();
|
||||||
|
assert_eq!(matcher.label(), "2/3");
|
||||||
|
matcher.next();
|
||||||
|
assert_eq!(matcher.label(), "3/3");
|
||||||
|
matcher.next();
|
||||||
|
assert_eq!(matcher.label(), "1/3");
|
||||||
|
|
||||||
|
matcher.update_query("IS", false);
|
||||||
|
assert_eq!(matcher.label(), "0/0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_range_start() {
|
||||||
|
let mut matcher = SearchMatcher::new();
|
||||||
|
matcher.matched_ranges = Rc::new(vec![5..10, 15..20, 25..30]);
|
||||||
|
matcher.update_cursor_by_offset(0);
|
||||||
|
assert_eq!(matcher.current_match_ix, 0);
|
||||||
|
|
||||||
|
matcher.update_cursor_by_offset(5);
|
||||||
|
assert_eq!(matcher.current_match_ix, 0);
|
||||||
|
|
||||||
|
matcher.update_cursor_by_offset(12);
|
||||||
|
assert_eq!(matcher.current_match_ix, 1);
|
||||||
|
|
||||||
|
matcher.update_cursor_by_offset(16);
|
||||||
|
assert_eq!(matcher.current_match_ix, 1);
|
||||||
|
|
||||||
|
matcher.update_cursor_by_offset(30);
|
||||||
|
assert_eq!(matcher.current_match_ix, 2);
|
||||||
|
|
||||||
|
matcher.update_cursor_by_offset(31);
|
||||||
|
assert_eq!(matcher.current_match_ix, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue