Cargo.lock 🔗
@@ -1724,10 +1724,12 @@ name = "find"
version = "0.1.0"
dependencies = [
"aho-corasick",
+ "anyhow",
"collections",
"editor",
"gpui",
"postage",
+ "regex",
"smol",
"theme",
"workspace",
Antonio Scandurra and Nathan Sobo created
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Cargo.lock | 2
crates/editor/src/editor.rs | 5
crates/editor/src/element.rs | 9 +
crates/find/Cargo.toml | 2
crates/find/src/find.rs | 173 +++++++++++++++++++++---------
crates/theme/src/theme.rs | 1
crates/zed/assets/themes/_base.toml | 6
7 files changed, 137 insertions(+), 61 deletions(-)
@@ -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 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 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;
+ let editor = editor.downgrade();
+ 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,
+ )
+ });
+ });
}
}
-
- ranges.push(
- buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()),
- );
- }
-
- ranges
- });
-
- 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)
+ 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" }