mirror of https://github.com/lapce/lapce.git
find view
This commit is contained in:
parent
de56bc0a9c
commit
c59fe3e816
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.6 2.677c.147-.31.356-.465.626-.465.248 0 .44.118.573.353.134.236.201.557.201.966 0 .443-.078.798-.235 1.067-.156.268-.365.402-.627.402-.237 0-.416-.125-.537-.374h-.008v.31H11V1h.593v1.677h.008zm-.016 1.1a.78.78 0 0 0 .107.426c.071.113.163.169.274.169.136 0 .24-.072.314-.216.075-.145.113-.35.113-.615 0-.22-.035-.39-.104-.514-.067-.124-.164-.187-.29-.187-.12 0-.219.062-.297.185a.886.886 0 0 0-.117.48v.272zM4.12 7.695L2 5.568l.662-.662 1.006 1v-1.51A1.39 1.39 0 0 1 5.055 3H7.4v.905H5.055a.49.49 0 0 0-.468.493l.007 1.5.949-.944.656.656-2.08 2.085zM9.356 4.93H10V3.22C10 2.408 9.685 2 9.056 2c-.135 0-.285.024-.45.073a1.444 1.444 0 0 0-.388.167v.665c.237-.203.487-.304.75-.304.261 0 .392.156.392.469l-.6.103c-.506.086-.76.406-.76.961 0 .263.061.473.183.631A.61.61 0 0 0 8.69 5c.29 0 .509-.16.657-.48h.009v.41zm.004-1.355v.193a.75.75 0 0 1-.12.436.368.368 0 0 1-.313.17.276.276 0 0 1-.22-.095.38.38 0 0 1-.08-.248c0-.222.11-.351.332-.389l.4-.067zM7 12.93h-.644v-.41h-.009c-.148.32-.367.48-.657.48a.61.61 0 0 1-.507-.235c-.122-.158-.183-.368-.183-.63 0-.556.254-.876.76-.962l.6-.103c0-.313-.13-.47-.392-.47-.263 0-.513.102-.75.305v-.665c.095-.063.224-.119.388-.167.165-.049.315-.073.45-.073.63 0 .944.407.944 1.22v1.71zm-.64-1.162v-.193l-.4.068c-.222.037-.333.166-.333.388 0 .1.027.183.08.248a.276.276 0 0 0 .22.095.368.368 0 0 0 .312-.17c.08-.116.12-.26.12-.436zM9.262 13c.321 0 .568-.058.738-.173v-.71a.9.9 0 0 1-.552.207.619.619 0 0 1-.5-.215c-.12-.145-.181-.345-.181-.598 0-.26.063-.464.189-.612a.644.644 0 0 1 .516-.223c.194 0 .37.069.528.207v-.749c-.129-.09-.338-.134-.626-.134-.417 0-.751.14-1.001.422-.249.28-.373.662-.373 1.148 0 .42.116.764.349 1.03.232.267.537.4.913.4zM2 9l1-1h9l1 1v5l-1 1H3l-1-1V9zm1 0v5h9V9H3zm3-2l1-1h7l1 1v5l-1 1V7H6z"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.221 3.739l2.261 2.269L7.7 3.784l-.7-.7-1.012 1.007-.008-1.6a.523.523 0 0 1 .5-.526H8V1H6.48A1.482 1.482 0 0 0 5 2.489V4.1L3.927 3.033l-.706.706zm6.67 1.794h.01c.183.311.451.467.806.467.393 0 .706-.168.94-.503.236-.335.353-.78.353-1.333 0-.511-.1-.913-.301-1.207-.201-.295-.488-.442-.86-.442-.405 0-.718.194-.938.581h-.01V1H9v4.919h.89v-.386zm-.015-1.061v-.34c0-.248.058-.448.175-.601a.54.54 0 0 1 .445-.23.49.49 0 0 1 .436.233c.104.154.155.368.155.643 0 .33-.056.587-.169.768a.524.524 0 0 1-.47.27.495.495 0 0 1-.411-.211.853.853 0 0 1-.16-.532zM9 12.769c-.256.154-.625.231-1.108.231-.563 0-1.02-.178-1.369-.533-.349-.355-.523-.813-.523-1.374 0-.648.186-1.158.56-1.53.374-.376.875-.563 1.5-.563.433 0 .746.06.94.179v.998a1.26 1.26 0 0 0-.792-.276c-.325 0-.583.1-.774.298-.19.196-.283.468-.283.816 0 .338.09.603.272.797.182.191.431.287.749.287.282 0 .558-.092.828-.276v.946zM4 7L3 8v6l1 1h7l1-1V8l-1-1H4zm0 1h7v6H4V8z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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" }
|
||||
|
||||
|
|
|
@ -345,6 +345,7 @@ struct Info {
|
|||
}
|
||||
|
||||
fn editor_tab_content(
|
||||
main_split: MainSplitData,
|
||||
workspace: Arc<LapceWorkspace>,
|
||||
active_editor_tab: ReadSignal<Option<EditorTabId>>,
|
||||
editor_tab: RwSignal<EditorTabData>,
|
||||
|
@ -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<LapceWorkspace>,
|
||||
active_editor_tab: ReadSignal<Option<EditorTabId>>,
|
||||
editor_tab: RwSignal<EditorTabData>,
|
||||
|
@ -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<WindowTabData>) -> 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<WindowTabData>) -> 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 || {
|
||||
|
|
|
@ -538,6 +538,9 @@ pub enum InternalCommand {
|
|||
start: usize,
|
||||
position: Position,
|
||||
},
|
||||
Search {
|
||||
pattern: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -121,6 +121,7 @@ pub struct EditorData {
|
|||
pub last_movement: RwSignal<Movement>,
|
||||
pub inline_find: RwSignal<Option<InlineFindDirection>>,
|
||||
pub last_inline_find: RwSignal<Option<(InlineFindDirection, String)>>,
|
||||
pub find_focus: RwSignal<bool>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<LapceWorkspace>,
|
||||
is_active: impl Fn() -> bool + 'static + Copy,
|
||||
editor: RwSignal<EditorData>,
|
||||
) -> 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<EditorData>) -> 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<EditorData>) -> 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<bool>,
|
||||
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<bool>,
|
||||
replace_focus: RwSignal<bool>,
|
||||
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<bool>,
|
||||
replace_editor: EditorData,
|
||||
replace_active: RwSignal<bool>,
|
||||
replace_focus: RwSignal<bool>,
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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<usize>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
|
||||
/// 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<Regex>,
|
||||
|
||||
/// 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::<LinesMetric>() {
|
||||
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<SelRegion> {
|
||||
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::<Vec<SelRegion>>()
|
||||
.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::<Vec<SelRegion>>()
|
||||
.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) {
|
||||
|
|
|
@ -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<IndexMap<PathBuf, SearchMatchData>>,
|
||||
pub main_split: MainSplitData,
|
||||
pub common: CommonData,
|
||||
}
|
||||
|
||||
|
@ -110,13 +113,14 @@ fn slice(&mut self, _range: Range<usize>) -> 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<PathBuf, Vec<SearchMatch>>) {
|
|||
.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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<im::HashMap<PathBuf, RwSignal<Document>>>,
|
||||
pub diagnostics: RwSignal<im::HashMap<PathBuf, DiagnosticData>>,
|
||||
pub active_editor: Memo<Option<RwSignal<EditorData>>>,
|
||||
pub find_editor: EditorData,
|
||||
pub replace_editor: EditorData,
|
||||
pub replace_active: RwSignal<bool>,
|
||||
pub replace_focus: RwSignal<bool>,
|
||||
locations: RwSignal<im::Vector<EditorLocation>>,
|
||||
current_location: RwSignal<usize>,
|
||||
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<RwSignal<EditorData>> {
|
||||
|
@ -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<HashMap<Url, Vec<TextEdit>>> {
|
||||
|
|
|
@ -93,7 +93,7 @@ pub struct PaletteData {
|
|||
pub clicked_index: RwSignal<Option<usize>>,
|
||||
pub executed_commands: Rc<RefCell<HashMap<String, Instant>>>,
|
||||
pub executed_run_configs: Rc<RefCell<HashMap<(RunDebugMode, String), Instant>>>,
|
||||
main_split: MainSplitData,
|
||||
pub main_split: MainSplitData,
|
||||
pub references: RwSignal<Vec<EditorLocation>>,
|
||||
pub common: CommonData,
|
||||
}
|
||||
|
|
|
@ -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<WindowTabData>,
|
||||
|
@ -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(
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
pub fn text_input(
|
||||
doc: RwSignal<Document>,
|
||||
cursor: RwSignal<Cursor>,
|
||||
is_focused: impl Fn() -> bool + 'static,
|
||||
config: ReadSignal<Arc<LapceConfig>>,
|
||||
) -> 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<Node>,
|
||||
text_layout: Option<TextLayout>,
|
||||
color: Option<Color>,
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue