From c59fe3e816bcef1f73d4652f6dcebbac469a2b40 Mon Sep 17 00:00:00 2001 From: Dongdong Zhou Date: Tue, 16 May 2023 20:59:12 +0100 Subject: [PATCH] find view --- Cargo.lock | 7 +- Cargo.toml | 2 +- defaults/settings.toml | 2 + icons/codicons/replace-all.svg | 1 + icons/codicons/replace.svg | 1 + lapce-app/Cargo.toml | 4 +- lapce-app/src/app.rs | 31 +- lapce-app/src/command.rs | 3 + lapce-app/src/config/icon.rs | 2 + lapce-app/src/editor.rs | 24 +- lapce-app/src/editor/view.rs | 321 +++++++++++++- lapce-app/src/find.rs | 501 +--------------------- lapce-app/src/global_search.rs | 27 +- lapce-app/src/main_split.rs | 35 +- lapce-app/src/palette.rs | 2 +- lapce-app/src/panel/global_search_view.rs | 12 +- lapce-app/src/text_input.rs | 52 ++- lapce-app/src/window_tab.rs | 19 +- lapce-core/src/meta.rs | 2 +- 19 files changed, 489 insertions(+), 559 deletions(-) create mode 100644 icons/codicons/replace-all.svg create mode 100644 icons/codicons/replace.svg diff --git a/Cargo.lock b/Cargo.lock index ac13aba9..6603e8ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1615,7 +1615,6 @@ dependencies = [ [[package]] name = "floem" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=8f00e4d530441982627dd62cbf20f2e3f0efac6d#8f00e4d530441982627dd62cbf20f2e3f0efac6d" dependencies = [ "bitflags 2.2.1", "crossbeam-channel", @@ -1643,7 +1642,6 @@ dependencies = [ [[package]] name = "floem_renderer" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=8f00e4d530441982627dd62cbf20f2e3f0efac6d#8f00e4d530441982627dd62cbf20f2e3f0efac6d" dependencies = [ "cosmic-text", "peniko", @@ -1653,7 +1651,6 @@ dependencies = [ [[package]] name = "floem_vger" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=8f00e4d530441982627dd62cbf20f2e3f0efac6d#8f00e4d530441982627dd62cbf20f2e3f0efac6d" dependencies = [ "anyhow", "floem_renderer", @@ -3439,9 +3436,9 @@ dependencies = [ [[package]] name = "lapce-xi-rope" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ae23edb8cf91f01edd9a87c88623eae3977c8d647a31c57cb12f1a125ca10a" +checksum = "6516aaa99c5059dc1a1bc02ed782d5e524699c1b4330028a6bed8259f9d9ff0a" dependencies = [ "bytecount", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 8209823f..e03e236d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ lsp-types = { version = "0.93", features = ["proposed"] } psp-types = { git = "https://github.com/lapce/psp-types" } -lapce-xi-rope = { version = "0.3.1", features = ["serde"] } +lapce-xi-rope = { version = "0.3.2", features = ["serde"] } lapce-core = { path = "./lapce-core" } lapce-rpc = { path = "./lapce-rpc" } diff --git a/defaults/settings.toml b/defaults/settings.toml index c1ad7de2..490cabf5 100644 --- a/defaults/settings.toml +++ b/defaults/settings.toml @@ -327,6 +327,8 @@ name = "" "search.case_sensitive" = "case-sensitive.svg" "search.whole_word" = "whole-word.svg" "search.regex" = "regex.svg" +"search.replace" = "replace.svg" +"search.replace_all" = "replace-all.svg" "symbol_kind.array" = "symbol-array.svg" "symbol_kind.boolean" = "symbol-boolean.svg" diff --git a/icons/codicons/replace-all.svg b/icons/codicons/replace-all.svg new file mode 100644 index 00000000..f5a51538 --- /dev/null +++ b/icons/codicons/replace-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/replace.svg b/icons/codicons/replace.svg new file mode 100644 index 00000000..65bfaacf --- /dev/null +++ b/icons/codicons/replace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lapce-app/Cargo.toml b/lapce-app/Cargo.toml index ed7faec9..547b5b99 100644 --- a/lapce-app/Cargo.toml +++ b/lapce-app/Cargo.toml @@ -36,8 +36,8 @@ fuzzy-matcher = "0.3.7" sled = "0.34.7" tokio = { version = "1.21", features = ["full"] } futures = "0.3.26" -floem = { git = "https://github.com/lapce/floem", rev = "8f00e4d530441982627dd62cbf20f2e3f0efac6d" } -# floem = { path = "../../workspaces/floem" } +# floem = { git = "https://github.com/lapce/floem", rev = "8f00e4d530441982627dd62cbf20f2e3f0efac6d" } +floem = { path = "../../workspaces/floem" } config = { version = "0.13.2", default-features = false, features = ["toml"] } structdesc = { git = "https://github.com/lapce/structdesc" } diff --git a/lapce-app/src/app.rs b/lapce-app/src/app.rs index 76761f59..38ba8ebd 100644 --- a/lapce-app/src/app.rs +++ b/lapce-app/src/app.rs @@ -345,6 +345,7 @@ struct Info { } fn editor_tab_content( + main_split: MainSplitData, workspace: Arc, active_editor_tab: ReadSignal>, editor_tab: RwSignal, @@ -378,6 +379,7 @@ fn editor_tab_content( }; container_box(|| { Box::new(editor_view( + main_split.clone(), workspace.clone(), is_active, editor_data, @@ -396,6 +398,7 @@ fn editor_tab_content( } fn editor_tab( + main_split: MainSplitData, workspace: Arc, active_editor_tab: ReadSignal>, editor_tab: RwSignal, @@ -407,6 +410,7 @@ fn editor_tab( ( editor_tab_header(active_editor_tab, editor_tab, editors, focus, config), editor_tab_content( + main_split.clone(), workspace.clone(), active_editor_tab, editor_tab, @@ -522,6 +526,7 @@ fn split_list( if let Some(editor_tab_data) = editor_tab_data { container_box(|| { Box::new(editor_tab( + main_split.clone(), workspace.clone(), active_editor_tab, editor_tab_data, @@ -1355,14 +1360,18 @@ fn palette_input(window_tab_data: Arc) -> impl View { let doc = window_tab_data.palette.input_editor.doc; let cursor = window_tab_data.palette.input_editor.cursor; let config = window_tab_data.common.config; + let focus = window_tab_data.common.focus; let cx = AppContext::get_current(); let cursor_x = create_rw_signal(cx.scope, 0.0); + let is_focused = move || focus.get() == Focus::Palette; container(move || { container(move || { scroll(move || { - text_input(doc, cursor, config).on_cursor_pos(move |point| { - cursor_x.set(point.x); - }) + text_input(doc, cursor, is_focused, config).on_cursor_pos( + move |point| { + cursor_x.set(point.x); + }, + ) }) .scroll_bar_color(move || { *config.get().get_color(LapceColor::LAPCE_SCROLL_BAR) @@ -1509,9 +1518,10 @@ fn palette_preview(palette_data: PaletteData) -> impl View { let preview_editor = palette_data.preview_editor; let has_preview = palette_data.has_preview; let config = palette_data.common.config; + let main_split = palette_data.main_split; container(|| { - container(|| editor_view(workspace, || true, preview_editor)).style( - move || { + container(|| editor_view(main_split, workspace, || true, preview_editor)) + .style(move || { let config = config.get(); Style::BASE .position(Position::Absolute) @@ -1519,8 +1529,7 @@ fn palette_preview(palette_data: PaletteData) -> impl View { .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) .size_pct(100.0, 100.0) .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) - }, - ) + }) }) .style(move || { Style::BASE @@ -1823,9 +1832,11 @@ fn rename(window_tab_data: Arc) -> impl View { container(|| { container(move || { scroll(move || { - text_input(doc, cursor, config).on_cursor_pos(move |point| { - cursor_x.set(point.x); - }) + text_input(doc, cursor, move || active.get(), config).on_cursor_pos( + move |point| { + cursor_x.set(point.x); + }, + ) }) .hide_bar(|| true) .on_ensure_visible(move || { diff --git a/lapce-app/src/command.rs b/lapce-app/src/command.rs index 697f3f61..6091723e 100644 --- a/lapce-app/src/command.rs +++ b/lapce-app/src/command.rs @@ -538,6 +538,9 @@ pub enum InternalCommand { start: usize, position: Position, }, + Search { + pattern: String, + }, } #[derive(Clone)] diff --git a/lapce-app/src/config/icon.rs b/lapce-app/src/config/icon.rs index 8d0728fb..8772bbe9 100644 --- a/lapce-app/src/config/icon.rs +++ b/lapce-app/src/config/icon.rs @@ -83,6 +83,8 @@ impl LapceIcons { pub const SEARCH_CASE_SENSITIVE: &'static str = "search.case_sensitive"; pub const SEARCH_WHOLE_WORD: &'static str = "search.whole_word"; pub const SEARCH_REGEX: &'static str = "search.regex"; + pub const SEARCH_REPLACE: &'static str = "search.replace"; + pub const SEARCH_REPLACE_ALL: &'static str = "search.replace_all"; pub const FILE_TYPE_CODE: &str = "file-code"; pub const FILE_TYPE_MEDIA: &str = "file-media"; diff --git a/lapce-app/src/editor.rs b/lapce-app/src/editor.rs index f47ea495..a8373601 100644 --- a/lapce-app/src/editor.rs +++ b/lapce-app/src/editor.rs @@ -121,6 +121,7 @@ pub struct EditorData { pub last_movement: RwSignal, pub inline_find: RwSignal>, pub last_inline_find: RwSignal>, + pub find_focus: RwSignal, pub common: CommonData, } @@ -159,6 +160,7 @@ pub fn new( let last_movement = create_rw_signal(cx, Movement::Left); let inline_find = create_rw_signal(cx, None); let last_inline_find = create_rw_signal(cx, None); + let find_focus = create_rw_signal(cx, false); Self { editor_tab_id, editor_id, @@ -173,6 +175,7 @@ pub fn new( last_movement, inline_find, last_inline_find, + find_focus, common, } } @@ -222,6 +225,7 @@ pub fn copy( editor.window_origin = create_rw_signal(cx, Point::ZERO); editor.confirmed = create_rw_signal(cx, true); editor.snippet = create_rw_signal(cx, None); + editor.find_focus = create_rw_signal(cx, false); editor.editor_tab_id = editor_tab_id; editor.editor_id = editor_id; editor @@ -592,6 +596,7 @@ fn run_focus_command( } FocusCommand::ClearSearch => { self.common.find.visual.set(false); + self.find_focus.set(false); } FocusCommand::Search => { self.search(); @@ -1513,7 +1518,9 @@ fn search_whole_word_forward(&self, cx: Scope, mods: Modifiers) { ) }); self.common.find.whole_words.set(true); - self.common.find.set_find(&word); + self.common + .internal_command + .set(Some(InternalCommand::Search { pattern: word })); let next = self.common.find.next(buffer.text(), offset, false, true); if let Some((start, _end)) = next { @@ -1662,7 +1669,7 @@ fn rename(&self, cx: Scope) { }); } - fn search(&self) { + pub fn word_at_cursor(&self) -> String { let region = self.cursor.with_untracked(|c| match &c.mode { lapce_core::cursor::CursorMode::Normal(offset) => { lapce_core::selection::SelRegion::caret(*offset) @@ -1687,7 +1694,7 @@ fn search(&self) { } }); - let pattern = if region.is_caret() { + if region.is_caret() { self.doc.with_untracked(|doc| { let (start, end) = doc.buffer().select_word(region.start); doc.buffer().slice_to_cow(start..end).to_string() @@ -1698,10 +1705,17 @@ fn search(&self) { .slice_to_cow(region.min()..region.max()) .to_string() }) - }; + } + } + + fn search(&self) { + let pattern = self.word_at_cursor(); if !pattern.contains('\n') { - self.common.find.set_find(&pattern); + self.common + .internal_command + .set(Some(InternalCommand::Search { pattern })); + self.find_focus.set(true); } } } diff --git a/lapce-app/src/editor/view.rs b/lapce-app/src/editor/view.rs index f172fda9..1d25926f 100644 --- a/lapce-app/src/editor/view.rs +++ b/lapce-app/src/editor/view.rs @@ -5,14 +5,15 @@ glazier::PointerType, peniko::kurbo::{Point, Rect, Size}, reactive::{ - create_effect, create_memo, ReadSignal, RwSignal, SignalGet, - SignalGetUntracked, SignalSet, SignalWith, SignalWithUntracked, + create_effect, create_memo, create_rw_signal, ReadSignal, RwSignal, + SignalGet, SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, + SignalWithUntracked, }, style::{CursorStyle, Dimension, Style}, view::View, views::{ - clip, container, label, list, rich_text, scroll, stack, svg, virtual_list, - Decorators, VirtualListDirection, VirtualListItemSize, + clip, container, empty, label, list, rich_text, scroll, stack, svg, + virtual_list, Decorators, VirtualListDirection, VirtualListItemSize, }, AppContext, }; @@ -21,10 +22,14 @@ mode::{Mode, VisualMode}, selection::Selection, }; +use lapce_xi_rope::find::CaseMatching; use crate::{ + app::clickable_icon, config::{color::LapceColor, icon::LapceIcons, LapceConfig}, doc::{DocLine, Document, LineExtraStyle}, + main_split::MainSplitData, + text_input::text_input, wave::wave_line, window_tab::Focus, workspace::LapceWorkspace, @@ -237,18 +242,25 @@ fn insert_cursor( } pub fn editor_view( + main_split: MainSplitData, workspace: Arc, is_active: impl Fn() -> bool + 'static + Copy, editor: RwSignal, ) -> impl View { - let (cursor, viewport, config) = editor.with_untracked(|editor| { + let (cursor, viewport, find_focus, config) = editor.with_untracked(|editor| { ( editor.cursor.read_only(), editor.viewport, + editor.find_focus, editor.common.config, ) }); + let find_editor = main_split.find_editor; + let replace_editor = main_split.replace_editor; + let replace_active = main_split.replace_active; + let replace_focus = main_split.replace_focus; + stack(move || { ( editor_breadcrumbs(workspace, editor, config), @@ -269,6 +281,14 @@ pub fn editor_view( ) }) .style(|| Style::BASE.size_pct(100.0, 100.0)), + find_view( + find_editor, + find_focus, + replace_editor, + replace_active, + replace_focus, + is_active, + ), ) }) .style(|| Style::BASE.absolute().size_pct(100.0, 100.0)) @@ -850,8 +870,8 @@ fn editor_content(editor: RwSignal) -> impl View { scroll(|| { let focus = editor.with_untracked(|e| e.common.focus); - container(|| { - virtual_list( + stack(|| { + (virtual_list( VirtualListDirection::Vertical, VirtualListItemSize::Fixed(Box::new(move || { config.get_untracked().editor.line_height() as f64 @@ -873,13 +893,13 @@ fn editor_content(editor: RwSignal) -> impl View { .padding_bottom_px(padding_bottom) .cursor(CursorStyle::Text) .min_width_pct(100.0) - }) + }),) }) .on_click(move |_| { focus.set(Focus::Workbench); true }) - .style(|| Style::BASE.min_size_pct(100.0, 100.0)) + .style(|| Style::BASE.min_size_pct(100.0, 100.0).flex_col()) }) .scroll_bar_color(move || *config.get().get_color(LapceColor::LAPCE_SCROLL_BAR)) .on_resize(move |point, _rect| { @@ -980,3 +1000,286 @@ fn editor_extra_style( ) .style(|| Style::BASE.absolute().size_pct(100.0, 100.0)) } + +fn search_editor_view( + find_editor: EditorData, + find_focus: RwSignal, + is_active: impl Fn() -> bool + 'static + Copy, +) -> impl View { + let cx = AppContext::get_current(); + let cursor_x = create_rw_signal(cx.scope, 0.0); + + let doc = find_editor.doc; + let cursor = find_editor.cursor; + let config = find_editor.common.config; + + let case_matching = find_editor.common.find.case_matching; + let whole_word = find_editor.common.find.whole_words; + let is_regex = find_editor.common.find.is_regex; + let visual = find_editor.common.find.visual; + + stack(|| { + ( + container(|| { + scroll(|| { + text_input( + doc, + cursor, + move || is_active() && visual.get() && find_focus.get(), + config, + ) + .on_cursor_pos(move |point| { + cursor_x.set(point.x); + }) + .style(|| Style::BASE.padding_horiz_px(1.0)) + }) + .hide_bar(|| true) + .on_ensure_visible(move || { + Size::new(20.0, 0.0) + .to_rect() + .with_origin(Point::new(cursor_x.get() - 10.0, 0.0)) + }) + .style(|| { + Style::BASE.absolute().size_pct(100.0, 100.0).items_center() + }) + }) + .style(|| Style::BASE.size_pct(100.0, 100.0)), + clickable_icon( + || LapceIcons::SEARCH_CASE_SENSITIVE, + move || { + let new = match case_matching.get_untracked() { + CaseMatching::Exact => CaseMatching::CaseInsensitive, + CaseMatching::CaseInsensitive => CaseMatching::Exact, + }; + case_matching.set(new); + }, + move || case_matching.get() == CaseMatching::CaseInsensitive, + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + clickable_icon( + || LapceIcons::SEARCH_WHOLE_WORD, + move || { + whole_word.update(|whole_word| { + *whole_word = !*whole_word; + }); + }, + move || whole_word.get(), + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + clickable_icon( + || LapceIcons::SEARCH_REGEX, + move || { + is_regex.update(|is_regex| { + *is_regex = !*is_regex; + }); + }, + move || is_regex.get(), + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + ) + }) + .on_click(move |_| { + find_focus.set(true); + true + }) + .style(move || { + let config = config.get(); + Style::BASE + .width_px(200.0) + .padding_horiz_px(6.0) + .padding_vert_px(4.0) + .border(1.0) + .border_radius(6.0) + .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + }) +} + +fn replace_editor_view( + replace_editor: EditorData, + replace_active: RwSignal, + replace_focus: RwSignal, + is_active: impl Fn() -> bool + 'static + Copy, +) -> impl View { + let cx = AppContext::get_current(); + let cursor_x = create_rw_signal(cx.scope, 0.0); + + let doc = replace_editor.doc; + let cursor = replace_editor.cursor; + let config = replace_editor.common.config; + let visual = replace_editor.common.find.visual; + + stack(|| { + ( + container(|| { + scroll(|| { + text_input( + doc, + cursor, + move || { + is_active() + && visual.get() + && replace_active.get() + && replace_focus.get() + }, + config, + ) + .on_cursor_pos(move |point| { + cursor_x.set(point.x); + }) + .style(|| Style::BASE.padding_horiz_px(1.0)) + }) + .hide_bar(|| true) + .on_ensure_visible(move || { + Size::new(20.0, 0.0) + .to_rect() + .with_origin(Point::new(cursor_x.get() - 10.0, 0.0)) + }) + .style(|| { + Style::BASE.absolute().size_pct(100.0, 100.0).items_center() + }) + }) + .style(|| Style::BASE.size_pct(100.0, 100.0)), + empty().style(move || { + let config = config.get(); + let size = config.ui.icon_size() as f32 + 10.0; + Style::BASE.size_px(0.0, size) + }), + ) + }) + .style(move || { + let config = config.get(); + Style::BASE + .width_px(200.0) + .padding_horiz_px(6.0) + .padding_vert_px(4.0) + .border(1.0) + .border_radius(6.0) + .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + }) +} + +fn find_view( + find_editor: EditorData, + find_focus: RwSignal, + replace_editor: EditorData, + replace_active: RwSignal, + replace_focus: RwSignal, + is_active: impl Fn() -> bool + 'static + Copy, +) -> impl View { + let config = find_editor.common.config; + let find_visual = find_editor.common.find.visual; + + container(|| { + stack(|| { + ( + stack(|| { + ( + clickable_icon( + move || { + if replace_active.get() { + LapceIcons::ITEM_OPENED + } else { + LapceIcons::ITEM_CLOSED + } + }, + move || { + replace_active.update(|active| *active = !*active); + }, + move || false, + || false, + config, + ) + .style(|| Style::BASE.padding_horiz_px(6.0)), + search_editor_view(find_editor, find_focus, is_active), + clickable_icon( + || LapceIcons::SEARCH_BACKWARD, + move || {}, + move || false, + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + clickable_icon( + || LapceIcons::SEARCH_FORWARD, + move || {}, + move || false, + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + clickable_icon( + || LapceIcons::CLOSE, + move || {}, + move || false, + || false, + config, + ) + .style(|| Style::BASE.padding_horiz_px(6.0)), + ) + }) + .style(|| Style::BASE.items_center()), + stack(|| { + ( + empty().style(move || { + let config = config.get(); + let width = + config.ui.icon_size() as f32 + 10.0 + 6.0 * 2.0; + Style::BASE.width_px(width) + }), + replace_editor_view( + replace_editor, + replace_active, + replace_focus, + is_active, + ), + clickable_icon( + || LapceIcons::SEARCH_REPLACE, + move || {}, + move || false, + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + clickable_icon( + || LapceIcons::SEARCH_REPLACE_ALL, + move || {}, + move || false, + || false, + config, + ) + .style(|| Style::BASE.padding_left_px(6.0)), + ) + }) + .style(move || { + Style::BASE + .items_center() + .margin_top_px(4.0) + .apply_if(!replace_active.get(), |s| s.hide()) + }), + ) + }) + .style(move || { + Style::BASE + .margin_right_px(50.0) + .background(*config.get().get_color(LapceColor::PANEL_BACKGROUND)) + .border_radius(6.0) + .padding_vert_px(4.0) + .flex_col() + }) + }) + .style(move || { + Style::BASE + .absolute() + .width_pct(100.0) + .justify_end() + .apply_if(!find_visual.get(), |s| s.hide()) + }) +} diff --git a/lapce-app/src/find.rs b/lapce-app/src/find.rs index 5671eda2..dd0f0cc3 100644 --- a/lapce-app/src/find.rs +++ b/lapce-app/src/find.rs @@ -2,16 +2,15 @@ use floem::reactive::{ create_effect, create_rw_signal, RwSignal, Scope, SignalGet, SignalGetUntracked, - SignalSet, SignalUpdate, SignalWith, SignalWithUntracked, + SignalSet, SignalWith, SignalWithUntracked, }; use lapce_core::{ - selection::{InsertDrift, SelRegion, Selection}, + selection::{SelRegion, Selection}, word::WordCursor, }; use lapce_xi_rope::{ - delta::DeltaRegion, find::{find, is_multiline_regex, CaseMatching}, - Cursor, Interval, LinesMetric, Metric, Rope, RopeDelta, + Cursor, Interval, Rope, }; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; @@ -55,499 +54,6 @@ pub struct FindStatus { lines: Vec, } -#[derive(Clone)] -pub struct OldFind { - /// Uniquely identifies this search query. - id: usize, - - /// The occurrences, which determine the highlights, have been updated. - hls_dirty: bool, - - pub visual: bool, - - /// The currently active search string. - pub search_string: Option, - - /// The case matching setting for the currently active search. - pub case_matching: CaseMatching, - - /// The search query should be considered as regular expression. - pub regex: Option, - - /// Query matches only whole words. - pub whole_words: bool, - - /// The set of all known find occurrences (highlights). - occurrences: Selection, -} - -impl OldFind { - pub fn new(id: usize) -> OldFind { - OldFind { - id, - hls_dirty: true, - search_string: None, - case_matching: CaseMatching::CaseInsensitive, - regex: None, - whole_words: false, - visual: false, - occurrences: Selection::new(), - } - } - - pub fn id(&self) -> usize { - self.id - } - - pub fn occurrences(&self) -> &Selection { - &self.occurrences - } - - pub fn hls_dirty(&self) -> bool { - self.hls_dirty - } - - 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() - && is_multiline_regex(self.search_string.as_ref().unwrap()) - } - - /// Unsets the search and removes all highlights from the view. - pub fn unset(&mut self) { - self.search_string = None; - self.occurrences = Selection::new(); - self.hls_dirty = true; - } - - /// 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 - }; - - 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 - && self.regex.is_some() == is_regex - && self.whole_words == whole_words - { - // search parameters did not change - } - } - - self.unset(); - - self.search_string = Some(search_string.to_string()); - self.whole_words = whole_words; - - // create regex from untrusted input - self.regex = match is_regex { - false => None, - true => RegexBuilder::new(search_string) - .size_limit(REGEX_SIZE_LIMIT) - .case_insensitive(!self.case_sensitive()) - .build() - .ok(), - }; - } - - pub fn next( - &self, - text: &Rope, - offset: usize, - reverse: bool, - wrap: bool, - ) -> Option<(usize, usize)> { - let search_string = self.search_string.as_ref()?; - if !reverse { - let mut raw_lines = text.lines_raw(offset..text.len()); - let mut find_cursor = Cursor::new(text, offset); - while let Some(start) = find( - &mut find_cursor, - &mut raw_lines, - self.case_matching, - search_string, - self.regex.as_ref(), - ) { - let end = find_cursor.pos(); - - if self.whole_words - && !self.is_matching_whole_words(text, start, end) - { - raw_lines = text.lines_raw(find_cursor.pos()..text.len()); - continue; - } - raw_lines = text.lines_raw(find_cursor.pos()..text.len()); - - if start > offset { - return Some((start, end)); - } - } - if wrap { - let mut raw_lines = text.lines_raw(0..offset); - let mut find_cursor = Cursor::new(text, 0); - while let Some(start) = find( - &mut find_cursor, - &mut raw_lines, - self.case_matching, - search_string, - self.regex.as_ref(), - ) { - let end = find_cursor.pos(); - - if self.whole_words - && !self.is_matching_whole_words(text, start, end) - { - raw_lines = text.lines_raw(find_cursor.pos()..offset); - continue; - } - return Some((start, end)); - } - } - } else { - let mut raw_lines = text.lines_raw(0..offset); - let mut find_cursor = Cursor::new(text, 0); - let mut regions = Vec::new(); - while let Some(start) = find( - &mut find_cursor, - &mut raw_lines, - self.case_matching, - search_string, - self.regex.as_ref(), - ) { - let end = find_cursor.pos(); - raw_lines = text.lines_raw(find_cursor.pos()..offset); - if self.whole_words - && !self.is_matching_whole_words(text, start, end) - { - continue; - } - if start < offset { - regions.push((start, end)); - } - } - if !regions.is_empty() { - return Some(regions[regions.len() - 1]); - } - if wrap { - let mut raw_lines = text.lines_raw(offset..text.len()); - let mut find_cursor = Cursor::new(text, offset); - let mut regions = Vec::new(); - while let Some(start) = find( - &mut find_cursor, - &mut raw_lines, - self.case_matching, - search_string, - self.regex.as_ref(), - ) { - let end = find_cursor.pos(); - - if self.whole_words - && !self.is_matching_whole_words(text, start, end) - { - raw_lines = text.lines_raw(find_cursor.pos()..text.len()); - continue; - } - raw_lines = text.lines_raw(find_cursor.pos()..text.len()); - - if start > offset { - regions.push((start, end)); - } - } - if !regions.is_empty() { - return Some(regions[regions.len() - 1]); - } - } - } - None - } - - /// Execute the search on the provided text in the range provided by `start` and `end`. - pub fn update_find( - &mut self, - text: &Rope, - start: usize, - end: usize, - include_slop: bool, - ) { - if self.search_string.is_none() { - return; - } - - // extend the search by twice the string length (twice, because case matching may increase - // the length of an occurrence) - let slop = if include_slop { - self.search_string.as_ref().unwrap().len() * 2 - } else { - 0 - }; - - let search_string = self.search_string.as_ref().unwrap(); - - // expand region to be able to find occurrences around the region's edges - let expanded_start = max(start, slop) - slop; - let expanded_end = min(end + slop, text.len()); - let from = text - .at_or_prev_codepoint_boundary(expanded_start) - .unwrap_or(0); - let to = text - .at_or_next_codepoint_boundary(expanded_end) - .unwrap_or_else(|| text.len()); - let mut to_cursor = Cursor::new(text, to); - let _ = to_cursor.next_leaf(); - - let sub_text = text.subseq(Interval::new(0, to_cursor.pos())); - let mut find_cursor = Cursor::new(&sub_text, from); - - let mut raw_lines = text.lines_raw(from..to); - - while let Some(start) = find( - &mut find_cursor, - &mut raw_lines, - self.case_matching, - search_string, - self.regex.as_ref(), - ) { - let end = find_cursor.pos(); - - if self.whole_words && !self.is_matching_whole_words(text, start, end) { - raw_lines = text.lines_raw(find_cursor.pos()..to); - continue; - } - - let region = SelRegion::new(start, end, None); - self.occurrences.add_region(region); - // in case of ambiguous search results (e.g. search "aba" in "ababa"), - // the search result closer to the beginning of the file wins - // if e != end { - // // Skip the search result and keep the occurrence that is closer to - // // the beginning of the file. Re-align the cursor to the kept - // // occurrence - // find_cursor.set(e); - // raw_lines = text.lines_raw(find_cursor.pos()..to); - // continue; - // } - // - // // in case current cursor matches search result (for example query a* matches) - // // all cursor positions, then cursor needs to be increased so that search - // // continues at next position. Otherwise, search will result in overflow since - // // search will always repeat at current cursor position. - // if start == end { - // // determine whether end of text is reached and stop search or increase - // // cursor manually - // if end + 1 >= text.len() { - // break; - // } else { - // find_cursor.set(end + 1); - // } - // } - - // update line iterator so that line starts at current cursor position - raw_lines = text.lines_raw(find_cursor.pos()..to); - } - - self.hls_dirty = true; - } - - pub fn update_highlights(&mut self, text: &Rope, delta: &RopeDelta) { - // update search highlights for changed regions - if self.search_string.is_some() { - // invalidate occurrences around deletion positions - for DeltaRegion { - old_offset, len, .. - } in delta.iter_deletions() - { - self.occurrences.delete_range(old_offset, old_offset + len); - } - - self.occurrences = - self.occurrences - .apply_delta(delta, false, InsertDrift::Default); - - // invalidate occurrences around insert positions - for DeltaRegion { - new_offset, len, .. - } in delta.iter_inserts() - { - // also invalidate previous occurrence since it might expand after insertion - // eg. for regex .* every insertion after match will be part of match - self.occurrences - .delete_range(new_offset.saturating_sub(1), new_offset + len); - } - - // update find for the whole delta and everything after - let (iv, new_len) = delta.summary(); - - // get last valid occurrence that was unaffected by the delta - let start = match self.occurrences.regions_in_range(0, iv.start()).last() - { - Some(reg) => reg.end, - None => 0, - }; - - // invalidate all search results from the point of the last valid search result until ... - let is_multiline = - LinesMetric::next(self.search_string.as_ref().unwrap(), 0).is_some(); - - if is_multiline || self.is_multiline_regex() { - // ... the end of the file - self.occurrences.delete_range(iv.start(), text.len()); - self.update_find(text, start, text.len(), false); - } else { - // ... the end of the line including line break - let mut cursor = Cursor::new(text, iv.end() + new_len); - - let end_of_line = match cursor.next::() { - Some(end) => end, - None if cursor.pos() == text.len() => cursor.pos(), - _ => return, - }; - - self.occurrences.delete_range(iv.start(), end_of_line); - self.update_find(text, start, end_of_line, false); - } - } - } - - /// Return the occurrence closest to the provided selection `sel`. If searched is reversed then - /// the occurrence closest to the start of the selection is returned. `wrapped` indicates that - /// if the end of the text is reached the search continues from the start. - pub fn next_occurrence( - &self, - text: &Rope, - reverse: bool, - wrapped: bool, - sel: &Selection, - ) -> Option { - if self.occurrences.is_empty() { - return None; - } - - let (sel_start, sel_end) = match sel.last() { - Some(last) if last.is_caret() => - // if last selection is caret then allow the current position to be part of the occurrence - { - (last.min(), last.max()) - } - Some(last) if !last.is_caret() => - // if the last selection is not a caret then continue searching after the caret - { - (last.min(), last.max() + 1) - } - _ => (0, 0), - }; - - if reverse { - let next_occurrence = match sel_start.checked_sub(1) { - Some(search_end) => { - self.occurrences.full_regions_in_range(0, search_end).last() - } - None => None, - }; - - if next_occurrence.is_none() && !wrapped { - // get previous unselected occurrence - return self - .occurrences - .regions_in_range(0, text.len()) - .iter() - .cloned() - .filter(|o| { - sel.full_regions_in_range(o.min(), o.max()).is_empty() - }) - .collect::>() - .last() - .cloned(); - } - - next_occurrence.cloned() - } else { - let next_occurrence = self - .occurrences - .full_regions_in_range(sel_end, text.len()) - .first(); - - if next_occurrence.is_none() && !wrapped { - // get next unselected occurrence - return self - .occurrences - .full_regions_in_range(0, text.len()) - .iter() - .cloned() - .filter(|o| { - sel.full_regions_in_range(o.min(), o.max()).is_empty() - }) - .collect::>() - .first() - .cloned(); - } - - next_occurrence.cloned() - } - } - - /// Checks if the start and end of a match is matching whole words. - fn is_matching_whole_words( - &self, - text: &Rope, - start: usize, - end: usize, - ) -> bool { - let mut word_end_cursor = WordCursor::new(text, end - 1); - let mut word_start_cursor = WordCursor::new(text, start + 1); - - if word_start_cursor.prev_code_boundary() != start { - return false; - } - - if word_end_cursor.next_code_boundary() != end { - return false; - } - - true - } -} - #[derive(Clone)] pub struct FindSearchString { pub content: String, @@ -869,7 +375,6 @@ pub fn update_find( search_string, search.regex.as_ref(), ) { - println!("find start {start}"); let end = find_cursor.pos(); if whole_words && !self.is_matching_whole_words(text, start, end) { diff --git a/lapce-app/src/global_search.rs b/lapce-app/src/global_search.rs index d5b91848..d0aee8e4 100644 --- a/lapce-app/src/global_search.rs +++ b/lapce-app/src/global_search.rs @@ -9,14 +9,16 @@ views::VirtualListVector, }; use indexmap::IndexMap; -use lapce_core::mode::Mode; +use lapce_core::{mode::Mode, selection::Selection}; use lapce_rpc::proxy::{ProxyResponse, SearchMatch}; +use lapce_xi_rope::Rope; use crate::{ command::{CommandExecuted, CommandKind}, editor::EditorData, id::EditorId, keypress::{condition::Condition, KeyPressFocus}, + main_split::MainSplitData, window_tab::CommonData, }; @@ -43,6 +45,7 @@ pub fn height(&self) -> f64 { pub struct GlobalSearchData { pub editor: EditorData, pub search_result: RwSignal>, + pub main_split: MainSplitData, pub common: CommonData, } @@ -110,13 +113,14 @@ fn slice(&mut self, _range: Range) -> Self::ItemIterator { } impl GlobalSearchData { - pub fn new(cx: Scope, common: CommonData) -> Self { + pub fn new(cx: Scope, main_split: MainSplitData, common: CommonData) -> Self { let editor = EditorData::new_local(cx, EditorId::next(), common.clone()); let search_result = create_rw_signal(cx, IndexMap::new()); let global_search = Self { editor, search_result, + main_split, common, }; @@ -156,6 +160,15 @@ pub fn new(cx: Scope, common: CommonData) -> Self { }); } + { + let global_search_doc = global_search.editor.doc; + let main_split = global_search.main_split.clone(); + create_effect(cx, move |_| { + let content = global_search_doc.with(|doc| doc.buffer().to_string()); + main_split.set_find_pattern(content); + }); + } + global_search } @@ -185,4 +198,14 @@ fn update_matches(&self, matches: IndexMap>) { .collect(), ); } + + pub fn set_pattern(&self, pattern: String) { + let pattern_len = pattern.len(); + self.editor + .doc + .update(|doc| doc.reload(Rope::from(pattern), true)); + self.editor + .cursor + .update(|cursor| cursor.set_insert(Selection::region(0, pattern_len))); + } } diff --git a/lapce-app/src/main_split.rs b/lapce-app/src/main_split.rs index 5d07286e..a2902aff 100644 --- a/lapce-app/src/main_split.rs +++ b/lapce-app/src/main_split.rs @@ -14,7 +14,7 @@ }, }; use itertools::Itertools; -use lapce_core::cursor::Cursor; +use lapce_core::{cursor::Cursor, selection::Selection}; use lapce_rpc::{plugin::PluginId, proxy::ProxyResponse}; use lapce_xi_rope::Rope; use lsp_types::{ @@ -189,6 +189,10 @@ pub struct MainSplitData { pub docs: RwSignal>>, pub diagnostics: RwSignal>, pub active_editor: Memo>>, + pub find_editor: EditorData, + pub replace_editor: EditorData, + pub replace_active: RwSignal, + pub replace_focus: RwSignal, locations: RwSignal>, current_location: RwSignal, pub common: CommonData, @@ -206,6 +210,12 @@ pub fn new(cx: Scope, common: CommonData) -> Self { let locations = create_rw_signal(cx, im::Vector::new()); let current_location = create_rw_signal(cx, 0); let diagnostics = create_rw_signal(cx, im::HashMap::new()); + let find_editor = + EditorData::new_local(cx, EditorId::next(), common.clone()); + let replace_editor = + EditorData::new_local(cx, EditorId::next(), common.clone()); + let replace_active = create_rw_signal(cx, false); + let replace_focus = create_rw_signal(cx, false); let active_editor = create_memo(cx, move |_| -> Option> { @@ -226,6 +236,15 @@ pub fn new(cx: Scope, common: CommonData) -> Self { Some(editor) }); + { + let find_editor_doc = find_editor.doc; + let find = common.find.clone(); + create_effect(cx, move |_| { + let content = find_editor_doc.with(|doc| doc.buffer().to_string()); + find.set_find(&content); + }); + } + Self { scope: cx, root_split: SplitId::next(), @@ -235,6 +254,10 @@ pub fn new(cx: Scope, common: CommonData) -> Self { editors, docs, active_editor, + find_editor, + replace_editor, + replace_active, + replace_focus, diagnostics, locations, current_location, @@ -1263,6 +1286,16 @@ pub fn open_file_changed(&self, path: &Path, content: &str) { doc.handle_file_changed(Rope::from(content)); }); } + + pub fn set_find_pattern(&self, pattern: String) { + let pattern_len = pattern.len(); + self.find_editor + .doc + .update(|doc| doc.reload(Rope::from(pattern), true)); + self.find_editor + .cursor + .update(|cursor| cursor.set_insert(Selection::region(0, pattern_len))); + } } fn workspace_edits(edit: &WorkspaceEdit) -> Option>> { diff --git a/lapce-app/src/palette.rs b/lapce-app/src/palette.rs index 5870ae17..45791cf1 100644 --- a/lapce-app/src/palette.rs +++ b/lapce-app/src/palette.rs @@ -93,7 +93,7 @@ pub struct PaletteData { pub clicked_index: RwSignal>, pub executed_commands: Rc>>, pub executed_run_configs: Rc>>, - main_split: MainSplitData, + pub main_split: MainSplitData, pub references: RwSignal>, pub common: CommonData, } diff --git a/lapce-app/src/panel/global_search_view.rs b/lapce-app/src/panel/global_search_view.rs index ba9ad200..ad938c46 100644 --- a/lapce-app/src/panel/global_search_view.rs +++ b/lapce-app/src/panel/global_search_view.rs @@ -24,11 +24,11 @@ focus_text::focus_text, global_search::{GlobalSearchData, SearchMatchData}, text_input::text_input, - window_tab::WindowTabData, + window_tab::{Focus, WindowTabData}, workspace::LapceWorkspace, }; -use super::position::PanelPosition; +use super::{kind::PanelKind, position::PanelPosition}; pub fn global_search_panel( window_tab_data: Arc, @@ -47,6 +47,9 @@ pub fn global_search_panel( let cx = AppContext::get_current(); let cursor_x = create_rw_signal(cx.scope, 0.0); + let focus = global_search.common.focus; + let is_focused = move || focus.get() == Focus::Panel(PanelKind::Search); + stack(|| { ( container(|| { @@ -54,7 +57,7 @@ pub fn global_search_panel( ( container(|| { scroll(|| { - text_input(doc, cursor, config) + text_input(doc, cursor, is_focused, config) .on_cursor_pos(move |point| { cursor_x.set(point.x); }) @@ -123,7 +126,8 @@ pub fn global_search_panel( .style(move || { Style::BASE .width_pct(100.0) - .padding_px(6.0) + .padding_horiz_px(6.0) + .padding_vert_px(4.0) .border(1.0) .border_radius(6.0) .border_color( diff --git a/lapce-app/src/text_input.rs b/lapce-app/src/text_input.rs index aca9fa8c..e0dce128 100644 --- a/lapce-app/src/text_input.rs +++ b/lapce-app/src/text_input.rs @@ -29,6 +29,7 @@ pub fn text_input( doc: RwSignal, cursor: RwSignal, + is_focused: impl Fn() -> bool + 'static, config: ReadSignal>, ) -> TextInput { let cx = AppContext::get_current(); @@ -44,10 +45,16 @@ pub fn text_input( id.update_state(TextInputState::Cursor(cursor), false); }); + create_effect(cx.scope, move |_| { + let focus = is_focused(); + id.update_state(TextInputState::Focus(focus), false); + }); + TextInput { id, config, content: "".to_string(), + focus: false, text_node: None, text_layout: None, cursor: Cursor::origin(false), @@ -65,12 +72,14 @@ pub fn text_input( enum TextInputState { Content(String), Cursor(Cursor), + Focus(bool), } pub struct TextInput { id: Id, content: String, cursor: Cursor, + focus: bool, text_node: Option, text_layout: Option, color: Option, @@ -152,6 +161,9 @@ fn update( TextInputState::Cursor(cursor) => { self.cursor = cursor; } + TextInputState::Focus(focus) => { + self.focus = focus; + } } cx.request_layout(self.id); ChangeFlags::LAYOUT @@ -254,25 +266,27 @@ fn paint(&mut self, cx: &mut floem::context::PaintCx) { cx.draw_text(text_layout, point); - let offset = self.cursor.offset(); - let hit_position = text_layout.hit_position(offset); - let cursor_point = hit_position.point + point.to_vec2(); - cx.stroke( - &Line::new( - Point::new( - cursor_point.x, - cursor_point.y - hit_position.glyph_ascent, + if self.focus { + let offset = self.cursor.offset(); + let hit_position = text_layout.hit_position(offset); + let cursor_point = hit_position.point + point.to_vec2(); + cx.stroke( + &Line::new( + Point::new( + cursor_point.x, + cursor_point.y - hit_position.glyph_ascent, + ), + Point::new( + cursor_point.x, + cursor_point.y + hit_position.glyph_descent, + ), ), - Point::new( - cursor_point.x, - cursor_point.y + hit_position.glyph_descent, - ), - ), - *self - .config - .get_untracked() - .get_color(LapceColor::EDITOR_CARET), - 2.0, - ); + *self + .config + .get_untracked() + .get_color(LapceColor::EDITOR_CARET), + 2.0, + ); + } } } diff --git a/lapce-app/src/window_tab.rs b/lapce-app/src/window_tab.rs index 912265cb..1ea853e5 100644 --- a/lapce-app/src/window_tab.rs +++ b/lapce-app/src/window_tab.rs @@ -266,7 +266,8 @@ pub fn new( TerminalPanelData::new(cx, workspace.clone(), None, common.clone()); let rename = RenameData::new(cx, common.clone()); - let global_search = GlobalSearchData::new(cx, common.clone()); + let global_search = + GlobalSearchData::new(cx, main_split.clone(), common.clone()); { let notification = create_signal_from_channel(cx, term_notification_rx); @@ -734,6 +735,9 @@ pub fn run_internal_command(&self, cmd: InternalCommand) { } => { self.rename.start(path, placeholder, start, position); } + InternalCommand::Search { pattern } => { + self.main_split.set_find_pattern(pattern); + } } } @@ -1101,6 +1105,19 @@ pub fn show_panel(&self, kind: PanelKind) { self.terminal.new_tab(self.scope, None); } self.panel.show_panel(&kind); + if kind == PanelKind::Search + && self.common.focus.get_untracked() == Focus::Workbench + { + let active_editor = self.main_split.active_editor.get_untracked(); + let word = active_editor.map(|editor| { + editor.with_untracked(|editor| editor.word_at_cursor()) + }); + if let Some(word) = word { + if !word.is_empty() { + self.global_search.set_pattern(word); + } + } + } self.common.focus.set(Focus::Panel(kind)); } diff --git a/lapce-core/src/meta.rs b/lapce-core/src/meta.rs index 37c7cb0f..8ee0541c 100644 --- a/lapce-core/src/meta.rs +++ b/lapce-core/src/meta.rs @@ -24,7 +24,7 @@ fn version() -> &'static str { .unwrap_or("") .starts_with("nightly") { - option_env!("RELEASE_TAG_NAME").unwrap() + option_env!("RELEASE_TAG_NAME").unwrap_or("") } else { env!("CARGO_PKG_VERSION") }