mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
Implement regex search with multiline support
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
5c862bfe98
commit
5b9d791269
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1724,10 +1724,12 @@ name = "find"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"postage",
|
||||
"regex",
|
||||
"smol",
|
||||
"theme",
|
||||
"workspace",
|
||||
|
@ -29,10 +29,11 @@ use language::{
|
||||
AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point,
|
||||
Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use multi_buffer::MultiBufferChunks;
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset, ToPoint,
|
||||
};
|
||||
use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
|
||||
use postage::watch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -323,6 +323,7 @@ impl EditorElement {
|
||||
end_row,
|
||||
*color,
|
||||
0.,
|
||||
0.15 * layout.line_height,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
@ -344,6 +345,7 @@ impl EditorElement {
|
||||
end_row,
|
||||
style.selection,
|
||||
corner_radius,
|
||||
corner_radius * 2.,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
@ -400,6 +402,7 @@ impl EditorElement {
|
||||
end_row: u32,
|
||||
color: Color,
|
||||
corner_radius: f32,
|
||||
line_end_overshoot: f32,
|
||||
layout: &LayoutState,
|
||||
content_origin: Vector2F,
|
||||
scroll_top: f32,
|
||||
@ -414,7 +417,7 @@ impl EditorElement {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
|
||||
};
|
||||
|
||||
let selection = HighlightedRange {
|
||||
let highlighted_range = HighlightedRange {
|
||||
color,
|
||||
line_height: layout.line_height,
|
||||
corner_radius,
|
||||
@ -437,7 +440,7 @@ impl EditorElement {
|
||||
+ line_layout.x_for_index(range.end.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() + line_layout.width() + corner_radius * 2.0
|
||||
content_origin.x() + line_layout.width() + line_end_overshoot
|
||||
- scroll_left
|
||||
},
|
||||
}
|
||||
@ -445,7 +448,7 @@ impl EditorElement {
|
||||
.collect(),
|
||||
};
|
||||
|
||||
selection.paint(bounds, cx.scene);
|
||||
highlighted_range.paint(bounds, cx.scene);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,5 +13,7 @@ gpui = { path = "../gpui" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
aho-corasick = "0.7"
|
||||
anyhow = "1.0"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
regex = "1.5"
|
||||
smol = { version = "1.2" }
|
||||
|
@ -1,13 +1,15 @@
|
||||
use aho_corasick::AhoCorasickBuilder;
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use editor::{char_kind, Editor, EditorSettings};
|
||||
use editor::{char_kind, Anchor, Editor, EditorSettings, MultiBufferSnapshot};
|
||||
use gpui::{
|
||||
action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, Subscription,
|
||||
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use regex::RegexBuilder;
|
||||
use smol::future::yield_now;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use workspace::{ItemViewHandle, Settings, Toolbar, Workspace};
|
||||
|
||||
action!(Deploy);
|
||||
@ -41,6 +43,7 @@ struct FindBar {
|
||||
case_sensitive_mode: bool,
|
||||
whole_word_mode: bool,
|
||||
regex_mode: bool,
|
||||
query_contains_error: bool,
|
||||
}
|
||||
|
||||
impl Entity for FindBar {
|
||||
@ -58,11 +61,16 @@ impl View for FindBar {
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.find;
|
||||
let editor_container = if self.query_contains_error {
|
||||
theme.invalid_editor
|
||||
} else {
|
||||
theme.editor.input.container
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor)
|
||||
.contained()
|
||||
.with_style(theme.editor.input.container)
|
||||
.with_style(editor_container)
|
||||
.constrained()
|
||||
.with_max_width(theme.editor.max_width)
|
||||
.boxed(),
|
||||
@ -135,6 +143,7 @@ impl FindBar {
|
||||
regex_mode: false,
|
||||
settings,
|
||||
pending_search: None,
|
||||
query_contains_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,7 +223,9 @@ impl FindBar {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.query_contains_error = false;
|
||||
self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@ -233,70 +244,122 @@ impl FindBar {
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let search = self.query_editor.read(cx).text(cx);
|
||||
let query = self.query_editor.read(cx).text(cx);
|
||||
self.pending_search.take();
|
||||
if let Some(editor) = self.active_editor.as_ref() {
|
||||
if search.is_empty() {
|
||||
if query.is_empty() {
|
||||
editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
|
||||
} else {
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let case_sensitive_mode = self.case_sensitive_mode;
|
||||
let whole_word_mode = self.whole_word_mode;
|
||||
let ranges = cx.background().spawn(async move {
|
||||
const YIELD_INTERVAL: usize = 20000;
|
||||
|
||||
let search = AhoCorasickBuilder::new()
|
||||
.auto_configure(&[&search])
|
||||
.ascii_case_insensitive(!case_sensitive_mode)
|
||||
.build(&[&search]);
|
||||
let mut ranges = Vec::new();
|
||||
for (ix, mat) in search
|
||||
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
|
||||
.enumerate()
|
||||
{
|
||||
if (ix + 1) % YIELD_INTERVAL == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
let mat = mat.unwrap();
|
||||
|
||||
if whole_word_mode {
|
||||
let prev_kind =
|
||||
buffer.reversed_chars_at(mat.start()).next().map(char_kind);
|
||||
let start_kind =
|
||||
char_kind(buffer.chars_at(mat.start()).next().unwrap());
|
||||
let end_kind =
|
||||
char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap());
|
||||
let next_kind = buffer.chars_at(mat.end()).next().map(char_kind);
|
||||
if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ranges.push(
|
||||
buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()),
|
||||
);
|
||||
}
|
||||
|
||||
ranges
|
||||
});
|
||||
let case_sensitive = self.case_sensitive_mode;
|
||||
let whole_word = self.whole_word_mode;
|
||||
let ranges = if self.regex_mode {
|
||||
cx.background()
|
||||
.spawn(regex_search(buffer, query, case_sensitive, whole_word))
|
||||
} else {
|
||||
cx.background().spawn(async move {
|
||||
Ok(search(buffer, query, case_sensitive, whole_word).await)
|
||||
})
|
||||
};
|
||||
|
||||
let editor = editor.downgrade();
|
||||
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||
let ranges = ranges.await;
|
||||
if let Some((this, editor)) =
|
||||
cx.read(|cx| this.upgrade(cx).zip(editor.upgrade(cx)))
|
||||
{
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let theme = &this.settings.borrow().theme.find;
|
||||
this.highlighted_editors.insert(editor.downgrade());
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.highlight_ranges::<Self>(ranges, theme.match_background, cx)
|
||||
self.pending_search = Some(cx.spawn(|this, mut cx| async move {
|
||||
match ranges.await {
|
||||
Ok(ranges) => {
|
||||
if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let theme = &this.settings.borrow().theme.find;
|
||||
this.highlighted_editors.insert(editor.downgrade());
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.highlight_ranges::<Self>(
|
||||
ranges,
|
||||
theme.match_background,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.query_contains_error = true;
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const YIELD_INTERVAL: usize = 20000;
|
||||
|
||||
async fn search(
|
||||
buffer: MultiBufferSnapshot,
|
||||
query: String,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
) -> Vec<Range<Anchor>> {
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
let search = AhoCorasickBuilder::new()
|
||||
.auto_configure(&[&query])
|
||||
.ascii_case_insensitive(!case_sensitive)
|
||||
.build(&[&query]);
|
||||
for (ix, mat) in search
|
||||
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
|
||||
.enumerate()
|
||||
{
|
||||
if (ix + 1) % YIELD_INTERVAL == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
let mat = mat.unwrap();
|
||||
|
||||
if whole_word {
|
||||
let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind);
|
||||
let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap());
|
||||
let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap());
|
||||
let next_kind = buffer.chars_at(mat.end()).next().map(char_kind);
|
||||
if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
async fn regex_search(
|
||||
buffer: MultiBufferSnapshot,
|
||||
mut query: String,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
) -> Result<Vec<Range<Anchor>>> {
|
||||
if whole_word {
|
||||
let mut word_query = String::new();
|
||||
word_query.push_str("\\b");
|
||||
word_query.push_str(&query);
|
||||
word_query.push_str("\\b");
|
||||
query = word_query;
|
||||
}
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
let regex = RegexBuilder::new(&query)
|
||||
.case_insensitive(!case_sensitive)
|
||||
.multi_line(true)
|
||||
.build()?;
|
||||
for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() {
|
||||
if (ix + 1) % YIELD_INTERVAL == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ pub struct Find {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub editor: FindEditor,
|
||||
pub invalid_editor: ContainerStyle,
|
||||
pub mode_button_group: ContainerStyle,
|
||||
pub mode_button: ContainedText,
|
||||
pub active_mode_button: ContainedText,
|
||||
|
@ -185,7 +185,7 @@ corner_radius = 6
|
||||
|
||||
[project_panel]
|
||||
extends = "$panel"
|
||||
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
||||
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
||||
|
||||
[project_panel.entry]
|
||||
text = "$text.1"
|
||||
@ -352,3 +352,7 @@ text = "$text.0"
|
||||
placeholder_text = "$text.2"
|
||||
selection = "$selection.host"
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
||||
[find.invalid_editor]
|
||||
extends = "$find.editor"
|
||||
border = { width = 1, color = "$status.bad" }
|
||||
|
Loading…
Reference in New Issue
Block a user