find view

This commit is contained in:
Dongdong Zhou 2023-05-16 20:59:12 +01:00
parent de56bc0a9c
commit c59fe3e816
19 changed files with 489 additions and 559 deletions

7
Cargo.lock generated
View File

@ -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",

View File

@ -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" }

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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 || {

View File

@ -538,6 +538,9 @@ pub enum InternalCommand {
start: usize,
position: Position,
},
Search {
pattern: String,
},
}
#[derive(Clone)]

View File

@ -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";

View File

@ -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);
}
}
}

View File

@ -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())
})
}

View File

@ -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) {

View File

@ -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)));
}
}

View File

@ -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>>> {

View File

@ -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,
}

View File

@ -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(

View File

@ -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,
);
}
}
}

View File

@ -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));
}

View File

@ -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")
}