From 5b9d791269b6b3b1f2e89a7c420231bb0f622a5a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 28 Jan 2022 16:15:18 +0100 Subject: [PATCH] Implement regex search with multiline support Co-Authored-By: Nathan Sobo --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index 889b620198a3c3240f96c64480e9cc8cf28cc352..e00df6c0d0675eb55543abd409d844ec3474a60d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1724,10 +1724,12 @@ name = "find" version = "0.1.0" dependencies = [ "aho-corasick", + "anyhow", "collections", "editor", "gpui", "postage", + "regex", "smol", "theme", "workspace", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dc662969dc5a74740f71f2c0dd74a6a26d3477a0..146985bcc23ee6b48cda44096c4025d591fdea4e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1b1ae4649191a5a482ecf2788ae78c743be0962d..ecff2b93d1c23729db867ba57e4ff3ebec0a9f47 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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); } } diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 8efce7024ecf7156aa1c11ec9ec6d171a5b59c15..eb3d99ce346a2553d6f15f0b7eeadd02353676ca 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -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" } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index a144019f28c307e61f17bdff1e220a201d95d645..90412fb99b60466120125ca1baeefce916ca2e3a 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -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) -> 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) { - 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::(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::( + 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::(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> { + 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>> { + 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) +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e4274653e84dc532911c9474cd2bdb4f1c0f8081..d9fab286b00f2e78ba3a997c7ef60a2292c818eb 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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, diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index b52e80e400d827094eb25204a5adea35d2430984..c88c4bf3d868c4d5426281c2e89f74642ee80335 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -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" }