feat(search): add case sensitive search ui button (#1441)

This commit is contained in:
Paul Delafosse 2022-10-10 21:21:32 +02:00 committed by GitHub
parent c880b15993
commit ee83113246
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 424 additions and 56 deletions

1
icons/case-sensitive.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><path fill="currentColor" d="M8.854 11.702h-1l-.816-2.159H3.772l-.768 2.16H2L4.954 4h.935l2.965 7.702Zm-2.111-2.97L5.534 5.45a3.142 3.142 0 0 1-.118-.515h-.021c-.036.218-.077.39-.124.515L4.073 8.732h2.67Zm7.013 2.97h-.88v-.86h-.022c-.383.66-.947.99-1.692.99c-.548 0-.978-.146-1.29-.436c-.307-.29-.461-.675-.461-1.155c0-1.027.605-1.625 1.815-1.794l1.65-.23c0-.935-.379-1.403-1.134-1.403c-.663 0-1.26.226-1.794.677V6.59c.54-.344 1.164-.516 1.87-.516c1.292 0 1.938.684 1.938 2.052v3.577Zm-.88-2.782l-1.327.183c-.409.057-.717.159-.924.306c-.208.143-.312.399-.312.768c0 .268.095.489.285.66c.193.169.45.253.768.253a1.41 1.41 0 0 0 1.08-.457c.286-.308.43-.696.43-1.165V8.92Z"/></svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -213,6 +213,8 @@ pub enum FocusCommand {
SearchForward,
#[strum(serialize = "search_backward")]
SearchBackward,
#[strum(serialize = "toggle_case_sensitive_search")]
ToggleCaseSensitive,
#[strum(serialize = "global_search_refresh")]
GlobalSearchRefresh,
#[strum(serialize = "clear_search")]

View File

@ -514,6 +514,10 @@ pub enum LapceUICommand {
},
UpdateSearchInput(String),
UpdateSearch(String),
UpdateSearchWithCaseSensitivity {
pattern: String,
case_sensitive: bool,
},
GlobalSearchResult(String, Arc<HashMap<PathBuf, Vec<Match>>>),
CancelFilePicker,
SetWorkspace(LapceWorkspace),

View File

@ -1308,17 +1308,16 @@ pub fn do_multi_selection(
(first.min(), first.max())
};
let search_str = self.buffer.slice_to_cow(start..end);
let search_case_sensitive =
let case_sensitive = self.find.borrow().case_sensitive();
let multicursor_case_sensitive =
config.editor.multicursor_case_sensitive;
let case_sensitive =
multicursor_case_sensitive || case_sensitive;
let search_whole_word =
config.editor.multicursor_whole_words;
let mut find = Find::new(0);
find.set_find(
&search_str,
search_case_sensitive,
false,
search_whole_word,
);
find.set_case_sensitive(case_sensitive);
find.set_find(&search_str, false, search_whole_word);
let mut offset = 0;
while let Some((start, end)) =
find.next(self.buffer.text(), offset, false, false)
@ -1347,17 +1346,15 @@ pub fn do_multi_selection(
let r = selection.last_inserted().unwrap();
let search_str =
self.buffer.slice_to_cow(r.min()..r.max());
let search_case_sensitive =
config.editor.multicursor_case_sensitive;
let case_sensitive = self.find.borrow().case_sensitive();
let case_sensitive =
config.editor.multicursor_case_sensitive
|| case_sensitive;
let search_whole_word =
config.editor.multicursor_whole_words;
let mut find = Find::new(0);
find.set_find(
&search_str,
search_case_sensitive,
false,
search_whole_word,
);
find.set_case_sensitive(case_sensitive);
find.set_find(&search_str, false, search_whole_word);
let mut offset = r.max();
let mut seen = HashSet::new();
while let Some((start, end)) =
@ -1396,8 +1393,10 @@ pub fn do_multi_selection(
} else {
let search_str =
self.buffer.slice_to_cow(r.min()..r.max());
let case_sensitive = self.find.borrow().case_sensitive();
let mut find = Find::new(0);
find.set_find(&search_str, false, false, false);
find.set_case_sensitive(case_sensitive);
find.set_find(&search_str, false, false);
let mut offset = r.max();
let mut seen = HashSet::new();
while let Some((start, end)) =

View File

@ -1559,7 +1559,8 @@ fn run_focus_command(
LapceUICommand::UpdateSearchInput(word.clone()),
Target::Widget(*self.main_split.tab_id),
));
Arc::make_mut(&mut self.find).set_find(&word, false, false, true);
Arc::make_mut(&mut self.find).set_find(&word, false, true);
let next =
self.find
.next(self.doc.buffer().text(), offset, false, true);
@ -1634,6 +1635,22 @@ fn run_focus_command(
}
}
}
ToggleCaseSensitive => {
let tab_id = *self.main_split.tab_id;
let find = Arc::make_mut(&mut self.find);
if let Some(pattern) = find.search_string.clone() {
let case_sensitive = find.toggle_case_sensitive();
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::UpdateSearchWithCaseSensitivity {
pattern,
case_sensitive,
},
Target::Widget(tab_id),
));
}
return CommandExecuted::No;
}
GlobalSearchRefresh => {
let tab_id = *self.main_split.tab_id;
let pattern = self.doc.buffer().to_string();
@ -2136,8 +2153,7 @@ fn run_focus_command(
.to_string()
};
if !pattern.contains('\n') {
Arc::make_mut(&mut self.find)
.set_find(&pattern, false, false, false);
Arc::make_mut(&mut self.find).set_find(&pattern, false, false);
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::UpdateSearchInput(pattern),

View File

@ -107,6 +107,26 @@ pub fn set_hls_dirty(&mut self, is_dirty: bool) {
self.hls_dirty = is_dirty
}
/// Returns `true` if case sensitive, otherwise `false`
pub fn case_sensitive(&self) -> bool {
match self.case_matching {
CaseMatching::Exact => true,
CaseMatching::CaseInsensitive => false,
}
}
/// FLips the current case sensitivity and return the new sensitivity
/// `true` for case_sensitive, `false` for case insensitive.
pub fn toggle_case_sensitive(&mut self) -> bool {
let case_matching = match self.case_matching {
CaseMatching::Exact => CaseMatching::CaseInsensitive,
CaseMatching::CaseInsensitive => CaseMatching::Exact,
};
self.case_matching = case_matching;
self.case_sensitive()
}
/// Returns `true` if the search query is a multi-line regex.
pub(crate) fn is_multiline_regex(&self) -> bool {
self.regex.is_some()
@ -120,40 +140,42 @@ pub fn unset(&mut self) {
self.hls_dirty = true;
}
/// Sets find parameters and search query. Returns `true` if parameters have been updated.
/// Returns `false` to indicate that parameters haven't change.
pub fn set_find(
&mut self,
search_string: &str,
case_sensitive: bool,
is_regex: bool,
whole_words: bool,
) -> bool {
if search_string.is_empty() {
self.unset();
}
/// Sets find case sensitivity.
pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
let case_matching = if case_sensitive {
CaseMatching::Exact
} else {
CaseMatching::CaseInsensitive
};
if self.case_matching != case_matching {
self.case_matching = case_matching;
}
}
/// Sets find parameters and search query. Returns `true` if parameters have been updated.
/// Returns `false` to indicate that parameters haven't change.
pub fn set_find(
&mut self,
search_string: &str,
is_regex: bool,
whole_words: bool,
) {
if search_string.is_empty() {
self.unset();
}
if let Some(ref s) = self.search_string {
if s == search_string
&& case_matching == self.case_matching
&& self.regex.is_some() == is_regex
&& self.whole_words == whole_words
{
// search parameters did not change
return false;
}
}
self.unset();
self.search_string = Some(search_string.to_string());
self.case_matching = case_matching;
self.whole_words = whole_words;
// create regex from untrusted input
@ -161,12 +183,10 @@ pub fn set_find(
false => None,
true => RegexBuilder::new(search_string)
.size_limit(REGEX_SIZE_LIMIT)
.case_insensitive(case_matching == CaseMatching::CaseInsensitive)
.case_insensitive(!self.case_sensitive())
.build()
.ok(),
};
true
}
pub fn next(

View File

@ -697,7 +697,7 @@ pub fn select(&mut self, ctx: &mut EventCtx) {
let pattern = self.palette.get_input().to_string();
let find = Arc::make_mut(&mut self.find);
find.visual = true;
find.set_find(&pattern, false, false, false);
find.set_find(&pattern, false, false);
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::UpdateSearchInput(pattern),

View File

@ -292,7 +292,10 @@ fn handle_request(&mut self, id: RequestId, rpc: ProxyRequest) {
};
self.respond_rpc(id, result);
}
GlobalSearch { pattern } => {
GlobalSearch {
pattern,
case_sensitive,
} => {
let workspace = self.workspace.clone();
let proxy_rpc = self.proxy_rpc.clone();
thread::spawn(move || {
@ -300,7 +303,7 @@ fn handle_request(&mut self, id: RequestId, rpc: ProxyRequest) {
let mut matches = HashMap::new();
let pattern = regex::escape(&pattern);
if let Ok(matcher) = RegexMatcherBuilder::new()
.case_insensitive(true)
.case_insensitive(!case_sensitive)
.build_literals(&[&pattern])
{
let mut searcher = SearcherBuilder::new().build();

View File

@ -48,6 +48,7 @@ pub enum ProxyRequest {
},
GlobalSearch {
pattern: String,
case_sensitive: bool,
},
CompletionResolve {
plugin_id: PluginId,
@ -576,8 +577,19 @@ pub fn save_buffer_as(
);
}
pub fn global_search(&self, pattern: String, f: impl ProxyCallback + 'static) {
self.request_async(ProxyRequest::GlobalSearch { pattern }, f);
pub fn global_search(
&self,
pattern: String,
case_sensitive: bool,
f: impl ProxyCallback + 'static,
) {
self.request_async(
ProxyRequest::GlobalSearch {
pattern,
case_sensitive,
},
f,
);
}
pub fn save(&self, rev: u64, path: PathBuf, f: impl ProxyCallback + 'static) {

View File

@ -13,6 +13,8 @@
use crate::{editor::view::LapceEditorView, svg::get_svg, tab::LapceIcon};
const CASE_SENSITIVE_ICON: &str = "case-sensitive.svg";
pub struct FindBox {
parent_view_id: WidgetId,
input_width: f64,
@ -57,6 +59,18 @@ pub fn new(
Target::Widget(parent_view_id),
),
},
LapceIcon {
icon: CASE_SENSITIVE_ICON,
rect: Rect::ZERO,
command: Command::new(
LAPCE_COMMAND,
LapceCommand {
kind: CommandKind::Focus(FocusCommand::ToggleCaseSensitive),
data: None,
},
Target::Widget(parent_view_id),
),
},
LapceIcon {
icon: "close.svg",
rect: Rect::ZERO,
@ -136,16 +150,17 @@ fn layout(
BoxConstraints::tight(Size::new(self.input_width, bc.max().height));
let mut input_size = self.input.layout(ctx, &input_bc, data, env);
self.input.set_origin(ctx, data, env, Point::ZERO);
let icons_len = self.icons.len() as f64;
let height = input_size.height;
let mut width = input_size.width + self.result_width + height * 3.0;
let mut width = input_size.width + self.result_width + height * icons_len;
if width - 20.0 > bc.max().width {
let input_bc = BoxConstraints::tight(Size::new(
bc.max().width - height * 3.0 - 20.0 - self.result_width,
bc.max().width - height * icons_len - 20.0 - self.result_width,
bc.max().height,
));
input_size = self.input.layout(ctx, &input_bc, data, env);
width = input_size.width + self.result_width + height * 3.0;
width = input_size.width + self.result_width + height * icons_len;
}
for (i, icon) in self.icons.iter_mut().enumerate() {
@ -260,8 +275,25 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
Point::new(input_size.width, text_layout.y_offset(input_size.height)),
);
let case_sensitive = data
.main_split
.active_editor()
.map(|editor| {
let editor_data = data.editor_view_content(editor.view_id);
editor_data.find.case_sensitive()
})
.unwrap_or_default();
for icon in self.icons.iter() {
if icon.rect.contains(self.mouse_pos) {
if icon.icon == CASE_SENSITIVE_ICON && case_sensitive {
ctx.fill(
&icon.rect,
data.config
.get_color_unchecked(LapceTheme::LAPCE_ACTIVE_TAB),
);
} else if icon.rect.contains(self.mouse_pos)
&& icon.icon != CASE_SENSITIVE_ICON
{
ctx.fill(
&icon.rect,
data.config

View File

@ -3,9 +3,11 @@
use druid::{
piet::{Text, TextAttribute, TextLayout as PietTextLayout, TextLayoutBuilder},
BoxConstraints, Command, Cursor, Data, Env, Event, EventCtx, FontWeight,
LayoutCtx, LifeCycle, LifeCycleCtx, MouseEvent, PaintCtx, Point, RenderContext,
Size, Target, UpdateCtx, Widget, WidgetExt, WidgetId,
LayoutCtx, LifeCycle, LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect,
RenderContext, Size, Target, UpdateCtx, Widget, WidgetExt, WidgetId, WidgetPod,
};
use lapce_core::command::FocusCommand;
use lapce_data::command::{CommandKind, LapceCommand, LAPCE_COMMAND};
use lapce_data::{
command::{LapceUICommand, LAPCE_UI_COMMAND},
config::LapceTheme,
@ -14,6 +16,8 @@
panel::PanelKind,
};
use crate::svg::get_svg;
use crate::tab::LapceIcon;
use crate::{
editor::view::LapceEditorView,
panel::{LapcePanel, PanelHeaderKind, PanelSizing},
@ -22,19 +26,276 @@
svg::file_svg,
};
pub struct SearchInput {
input: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
icons: Vec<LapceIcon>,
parent_view_id: WidgetId,
result_width: f64,
search_input_padding: f64,
mouse_pos: Point,
}
impl SearchInput {
fn new(view_id: WidgetId) -> Self {
let id = WidgetId::next();
let search_input_padding = 15.0;
let input = LapceEditorView::new(view_id, id, None)
.hide_header()
.hide_gutter()
.padding((search_input_padding, search_input_padding));
let icons = vec![LapceIcon {
icon: "case-sensitive.svg",
rect: Rect::ZERO,
command: Command::new(
LAPCE_COMMAND,
LapceCommand {
kind: CommandKind::Focus(FocusCommand::ToggleCaseSensitive),
data: None,
},
Target::Widget(view_id),
),
}];
Self {
parent_view_id: view_id,
result_width: 75.0,
input: WidgetPod::new(input.boxed()),
icons,
mouse_pos: Point::ZERO,
search_input_padding,
}
}
fn mouse_down(&self, ctx: &mut EventCtx, mouse_event: &MouseEvent) {
for icon in self.icons.iter() {
if icon.rect.contains(mouse_event.pos) {
ctx.submit_command(icon.command.clone());
}
}
}
fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool {
for icon in self.icons.iter() {
if icon.rect.contains(mouse_event.pos) {
return true;
}
}
false
}
}
impl Widget<LapceTabData> for SearchInput {
fn event(
&mut self,
ctx: &mut EventCtx,
event: &Event,
data: &mut LapceTabData,
env: &Env,
) {
self.input.event(ctx, event, data, env);
match event {
Event::MouseMove(mouse_event) => {
ctx.set_handled();
self.mouse_pos = mouse_event.pos;
if self.icon_hit_test(mouse_event) {
ctx.set_cursor(&druid::Cursor::Pointer);
} else {
ctx.clear_cursor();
}
}
Event::MouseDown(mouse_event) => {
ctx.set_handled();
self.mouse_down(ctx, mouse_event);
}
_ => {}
}
}
fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &LapceTabData,
env: &Env,
) -> Size {
let input_bc = BoxConstraints::tight(bc.max());
let mut input_size = self.input.layout(ctx, &input_bc, data, env);
self.input.set_origin(ctx, data, env, Point::ZERO);
let icon_len = self.icons.len() as f64;
let height = input_size.height;
let icon_height = height - self.search_input_padding;
let mut width = input_size.width + self.result_width + height * icon_len;
if width - 20.0 > bc.max().width {
let input_bc = BoxConstraints::tight(Size::new(
bc.max().width - height * icon_len - 20.0 - self.result_width,
bc.max().height,
));
input_size = self.input.layout(ctx, &input_bc, data, env);
width = input_size.width + self.result_width + height * icon_len;
}
for (i, icon) in self.icons.iter_mut().enumerate() {
icon.rect = Size::new(icon_height, icon_height)
.to_rect()
.with_origin(Point::new(
input_size.width + self.result_width + i as f64 * icon_height,
self.search_input_padding / 2.0,
))
.inflate(-5.0, -5.0);
}
Size::new(width, height)
}
fn lifecycle(
&mut self,
ctx: &mut LifeCycleCtx,
event: &LifeCycle,
data: &LapceTabData,
env: &Env,
) {
self.input.lifecycle(ctx, event, data, env);
}
fn update(
&mut self,
ctx: &mut UpdateCtx,
_old_data: &LapceTabData,
data: &LapceTabData,
env: &Env,
) {
self.input.update(ctx, data, env);
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
let buffer = data.editor_view_content(self.parent_view_id);
let rect = ctx.size().to_rect();
ctx.with_save(|ctx| {
ctx.clip(rect.inset((100.0, 0.0, 100.0, 100.0)));
let shadow_width = data.config.ui.drop_shadow_width() as f64;
if shadow_width > 0.0 {
ctx.blurred_rect(
rect,
shadow_width,
data.config
.get_color_unchecked(LapceTheme::LAPCE_DROPDOWN_SHADOW),
);
} else {
ctx.stroke(
rect.inflate(0.5, 0.5),
data.config.get_color_unchecked(LapceTheme::LAPCE_BORDER),
1.0,
);
}
});
ctx.fill(
rect,
data.config
.get_color_unchecked(LapceTheme::EDITOR_BACKGROUND),
);
self.input.paint(ctx, data, env);
let mut index = None;
let cursor_offset = buffer.editor.cursor.offset();
for i in 0..buffer.doc.find.borrow().occurrences().regions().len() {
let region = buffer.doc.find.borrow().occurrences().regions()[i];
if region.min() <= cursor_offset && cursor_offset <= region.max() {
index = Some(i);
}
}
let match_count = data
.search
.matches
.iter()
.map(|(_, matches)| matches.len())
.sum::<usize>();
let text_layout = ctx
.text()
.new_text_layout(if match_count > 0 {
match index {
Some(index) => format!("{}/{}", index + 1, match_count),
None => format!("{} results", match_count),
}
} else {
"No results".to_string()
})
.font(
data.config.ui.font_family(),
data.config.ui.font_size() as f64,
)
.text_color(
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
)
.max_width(self.result_width)
.build()
.unwrap();
let input_size = self.input.layout_rect().size();
ctx.draw_text(
&text_layout,
Point::new(input_size.width, text_layout.y_offset(input_size.height)),
);
let case_sensitive = data
.main_split
.active_editor()
.map(|editor| {
let editor_data = data.editor_view_content(editor.view_id);
editor_data.find.case_sensitive()
})
.unwrap_or_default();
for icon in self.icons.iter() {
if icon.icon == "case-sensitive.svg" && case_sensitive {
ctx.fill(
&icon.rect,
data.config
.get_color_unchecked(LapceTheme::LAPCE_ACTIVE_TAB),
);
} else if icon.rect.contains(self.mouse_pos)
&& icon.icon != "case-sensitive.svg"
{
ctx.fill(
&icon.rect,
data.config
.get_color_unchecked(LapceTheme::EDITOR_CURRENT_LINE),
);
}
let svg = get_svg(icon.icon).unwrap();
ctx.draw_svg(
&svg,
icon.rect.inflate(-7.0, -7.0),
Some(
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND),
),
);
}
}
}
pub fn new_search_panel(data: &LapceTabData) -> LapcePanel {
let editor_data = data
.main_split
.editors
.get(&data.search.editor_view_id)
.unwrap();
let input = LapceEditorView::new(editor_data.view_id, WidgetId::next(), None)
.hide_header()
.hide_gutter()
.padding((15.0, 15.0));
let search_bar = SearchInput::new(editor_data.view_id);
let split = LapceSplit::new(data.search.split_id)
.horizontal()
.with_child(input.boxed(), None, 100.0)
.with_child(search_bar.boxed(), None, 100.0)
.with_flex_child(
LapceScroll::new(SearchContent::new().boxed())
.vertical()
@ -44,6 +305,7 @@ pub fn new_search_panel(data: &LapceTabData) -> LapcePanel {
false,
)
.hide_border();
LapcePanel::new(
PanelKind::Search,
data.search.widget_id,
@ -174,6 +436,7 @@ fn layout(
.iter()
.map(|(_, matches)| matches.len() + 1)
.sum::<usize>();
let height = self.line_height * n as f64;
Size::new(bc.max().width, height)
}

View File

@ -787,14 +787,18 @@ fn handle_command_event(
Arc::make_mut(doc).reload(Rope::from(pattern), true);
}
}
LapceUICommand::UpdateSearch(pattern) => {
LapceUICommand::UpdateSearchWithCaseSensitivity {
pattern,
case_sensitive,
} => {
if pattern.is_empty() {
Arc::make_mut(&mut data.find).unset();
Arc::make_mut(&mut data.search).matches =
Arc::new(HashMap::new());
} else {
let find = Arc::make_mut(&mut data.find);
find.set_find(pattern, false, false, false);
find.set_case_sensitive(*case_sensitive);
find.set_find(pattern, false, false);
find.visual = true;
if data.focus_area == FocusArea::Panel(PanelKind::Search)
{
@ -816,6 +820,7 @@ fn handle_command_event(
let tab_id = data.id;
data.proxy.proxy_rpc.global_search(
pattern.clone(),
find.case_sensitive(),
Box::new(move |result| {
if let Ok(
ProxyResponse::GlobalSearchResponse {
@ -836,6 +841,17 @@ fn handle_command_event(
)
}
}
LapceUICommand::UpdateSearch(pattern) => {
let case_sensitive = data.find.case_sensitive();
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::UpdateSearchWithCaseSensitivity {
pattern: pattern.clone(),
case_sensitive,
},
Target::Widget(self.id),
))
}
LapceUICommand::OpenPluginInfo(volt) => {
data.main_split.open_plugin_info(ctx, volt);
}