diff --git a/Cargo.lock b/Cargo.lock index fc41bbe900..2dd0f7c036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1776,15 +1776,13 @@ dependencies = [ name = "find" version = "0.1.0" dependencies = [ - "aho-corasick", "anyhow", "collections", "editor", "gpui", + "language", "postage", "project", - "regex", - "smol", "theme", "unindent", "workspace", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 87b3814589..dcbbe76872 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -30,14 +30,14 @@ use gpui::{ }; use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; +pub use language::{char_kind, CharKind}; use language::{ AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ - char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, MultiBuffer, MultiBufferSnapshot, - ToOffset, ToPoint, + Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; use postage::watch; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4fc4488af9..f447350ec5 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,8 +7,9 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline, - OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, + Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, + TransactionId, }; use std::{ cell::{Ref, RefCell}, @@ -50,14 +51,6 @@ struct History { group_interval: Duration, } -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] -pub enum CharKind { - Newline, - Punctuation, - Whitespace, - Word, -} - #[derive(Clone)] struct Transaction { id: TransactionId, @@ -102,6 +95,7 @@ pub struct MultiBufferSnapshot { } pub struct ExcerptBoundary { + pub id: ExcerptId, pub row: u32, pub buffer: BufferSnapshot, pub range: Range, @@ -773,6 +767,21 @@ impl MultiBuffer { ids } + pub fn clear(&mut self, cx: &mut ModelContext) { + self.buffers.borrow_mut().clear(); + let mut snapshot = self.snapshot.borrow_mut(); + let prev_len = snapshot.len(); + snapshot.excerpts = Default::default(); + snapshot.trailing_excerpt_update_count += 1; + snapshot.is_dirty = false; + snapshot.has_conflict = false; + self.subscriptions.publish_mut([Edit { + old: 0..prev_len, + new: 0..0, + }]); + cx.notify(); + } + pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle) -> Vec { self.buffers .borrow() @@ -1342,9 +1351,12 @@ impl MultiBufferSnapshot { (start..end, word_kind) } - fn as_singleton(&self) -> Option<&Excerpt> { + pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> { if self.singleton { - self.excerpts.iter().next() + self.excerpts + .iter() + .next() + .map(|e| (&e.id, e.buffer_id, &e.buffer)) } else { None } @@ -1359,8 +1371,8 @@ impl MultiBufferSnapshot { } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_offset(offset, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset(offset, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1378,8 +1390,8 @@ impl MultiBufferSnapshot { } pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_point(point, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point(point, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1397,8 +1409,8 @@ impl MultiBufferSnapshot { } pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_point_utf16(point, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point_utf16(point, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1466,8 +1478,8 @@ impl MultiBufferSnapshot { } pub fn offset_to_point(&self, offset: usize) -> Point { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.offset_to_point(offset); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point(offset); } let mut cursor = self.excerpts.cursor::<(usize, Point)>(); @@ -1487,8 +1499,8 @@ impl MultiBufferSnapshot { } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.offset_to_point_utf16(offset); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point_utf16(offset); } let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>(); @@ -1508,8 +1520,8 @@ impl MultiBufferSnapshot { } pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_to_point_utf16(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_point_utf16(point); } let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>(); @@ -1529,8 +1541,8 @@ impl MultiBufferSnapshot { } pub fn point_to_offset(&self, point: Point) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_to_offset(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_offset(point); } let mut cursor = self.excerpts.cursor::<(Point, usize)>(); @@ -1550,8 +1562,8 @@ impl MultiBufferSnapshot { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_utf16_to_offset(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_utf16_to_offset(point); } let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); @@ -1711,9 +1723,8 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, I: 'a + IntoIterator, { - if let Some(excerpt) = self.as_singleton() { - return excerpt - .buffer + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) .collect(); } @@ -1878,11 +1889,11 @@ impl MultiBufferSnapshot { pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); - if let Some(excerpt) = self.as_singleton() { + if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { return Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor: excerpt.buffer.anchor_at(offset, bias), + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: buffer.anchor_at(offset, bias), }; } @@ -1989,6 +2000,7 @@ impl MultiBufferSnapshot { let excerpt = cursor.item()?; let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; let boundary = ExcerptBoundary { + id: excerpt.id.clone(), row: cursor.start().1.row, buffer: excerpt.buffer.clone(), range: excerpt.range.clone(), @@ -2090,7 +2102,7 @@ impl MultiBufferSnapshot { { self.as_singleton() .into_iter() - .flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id)) + .flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id)) } pub fn diagnostics_in_range<'a, T, O>( @@ -2101,11 +2113,11 @@ impl MultiBufferSnapshot { T: 'a + ToOffset, O: 'a + text::FromAnchor, { - self.as_singleton().into_iter().flat_map(move |excerpt| { - excerpt - .buffer - .diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) - }) + self.as_singleton() + .into_iter() + .flat_map(move |(_, _, buffer)| { + buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) + }) } pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { @@ -2147,16 +2159,16 @@ impl MultiBufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let excerpt = self.as_singleton()?; - let outline = excerpt.buffer.outline(theme)?; + let (excerpt_id, _, buffer) = self.as_singleton()?; + let outline = buffer.outline(theme)?; Some(Outline::new( outline .items .into_iter() .map(|item| OutlineItem { depth: item.depth, - range: self.anchor_in_excerpt(excerpt.id.clone(), item.range.start) - ..self.anchor_in_excerpt(excerpt.id.clone(), item.range.end), + range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) + ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, @@ -2764,18 +2776,6 @@ impl ToPointUtf16 for PointUtf16 { } } -pub fn char_kind(c: char) -> CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { - CharKind::Whitespace - } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 39570334d6..1c07815116 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -10,14 +10,12 @@ path = "src/find.rs" collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } +language = { path = "../language" } project = { path = "../project" } 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" } [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/find/src/buffer_find.rs b/crates/find/src/buffer_find.rs index 94ca1d9357..41aa96fa68 100644 --- a/crates/find/src/buffer_find.rs +++ b/crates/find/src/buffer_find.rs @@ -1,17 +1,13 @@ use crate::SearchOption; -use aho_corasick::AhoCorasickBuilder; -use anyhow::Result; use collections::HashMap; -use editor::{ - char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, MultiBufferSnapshot, -}; +use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::AnchorRangeExt; use postage::watch; -use regex::RegexBuilder; -use smol::future::yield_now; +use project::search::SearchQuery; use std::{ cmp::{self, Ordering}, ops::Range, @@ -21,7 +17,7 @@ use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Dismiss); action!(FocusEditor); -action!(ToggleMode, SearchOption); +action!(ToggleSearchOption, SearchOption); action!(GoToMatch, Direction); #[derive(Clone, Copy, PartialEq, Eq)] @@ -44,7 +40,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(FindBar::deploy); cx.add_action(FindBar::dismiss); cx.add_action(FindBar::focus_editor); - cx.add_action(FindBar::toggle_mode); + cx.add_action(FindBar::toggle_search_option); cx.add_action(FindBar::go_to_match); cx.add_action(FindBar::go_to_match_on_pane); } @@ -57,9 +53,9 @@ struct FindBar { active_editor_subscription: Option, editors_with_matches: HashMap, Vec>>, pending_search: Option>, - case_sensitive_mode: bool, - whole_word_mode: bool, - regex_mode: bool, + case_sensitive: bool, + whole_word: bool, + regex: bool, query_contains_error: bool, dismissed: bool, } @@ -96,11 +92,11 @@ impl View for FindBar { ) .with_child( Flex::row() - .with_child(self.render_mode_button("Case", SearchOption::CaseSensitive, cx)) - .with_child(self.render_mode_button("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_mode_button("Regex", SearchOption::Regex, cx)) + .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) .contained() - .with_style(theme.find.mode_button_group) + .with_style(theme.find.option_button_group) .aligned() .boxed(), ) @@ -185,9 +181,9 @@ impl FindBar { active_editor_subscription: None, active_match_index: None, editors_with_matches: Default::default(), - case_sensitive_mode: false, - whole_word_mode: false, - regex_mode: false, + case_sensitive: false, + whole_word: false, + regex: false, settings, pending_search: None, query_contains_error: false, @@ -204,27 +200,27 @@ impl FindBar { }); } - fn render_mode_button( + fn render_search_option( &self, icon: &str, - mode: SearchOption, + search_option: SearchOption, cx: &mut RenderContext, ) -> ElementBox { let theme = &self.settings.borrow().theme.find; - let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(mode as usize, cx, |state, _| { + let is_active = self.is_search_option_enabled(search_option); + MouseEventHandler::new::(search_option as usize, cx, |state, _| { let style = match (is_active, state.hovered) { - (false, false) => &theme.mode_button, - (false, true) => &theme.hovered_mode_button, - (true, false) => &theme.active_mode_button, - (true, true) => &theme.active_hovered_mode_button, + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, }; Label::new(icon.to_string(), style.text.clone()) .contained() .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -239,9 +235,9 @@ impl FindBar { enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, _| { let style = if state.hovered { - &theme.hovered_mode_button + &theme.hovered_option_button } else { - &theme.mode_button + &theme.option_button }; Label::new(icon.to_string(), style.text.clone()) .contained() @@ -315,19 +311,23 @@ impl FindBar { } } - fn is_mode_enabled(&self, mode: SearchOption) -> bool { - match mode { - SearchOption::WholeWord => self.whole_word_mode, - SearchOption::CaseSensitive => self.case_sensitive_mode, - SearchOption::Regex => self.regex_mode, + fn is_search_option_enabled(&self, search_option: SearchOption) -> bool { + match search_option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, } } - fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { - let value = match mode { - SearchOption::WholeWord => &mut self.whole_word_mode, - SearchOption::CaseSensitive => &mut self.case_sensitive_mode, - SearchOption::Regex => &mut self.regex_mode, + fn toggle_search_option( + &mut self, + ToggleSearchOption(search_option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match search_option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, }; *value = !*value; self.update_matches(true, cx); @@ -436,56 +436,81 @@ impl FindBar { editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); } else { let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - 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)) + let query = if self.regex { + match SearchQuery::regex(query, self.whole_word, self.case_sensitive) { + Ok(query) => query, + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return; + } + } } else { - cx.background().spawn(async move { - Ok(search(buffer, query, case_sensitive, whole_word).await) - }) + SearchQuery::text(query, self.whole_word, self.case_sensitive) }; + let ranges = cx.background().spawn(async move { + let mut ranges = Vec::new(); + if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { + ranges.extend( + query + .search(excerpt_buffer.as_rope()) + .await + .into_iter() + .map(|range| { + buffer.anchor_after(range.start) + ..buffer.anchor_before(range.end) + }), + ); + } else { + for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { + let excerpt_range = excerpt.range.to_offset(&excerpt.buffer); + let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone()); + ranges.extend(query.search(&rope).await.into_iter().map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + })); + } + } + ranges + }); + let editor = editor.downgrade(); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - match ranges.await { - Ok(ranges) => { - if let Some(editor) = editor.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.editors_with_matches - .insert(editor.downgrade(), ranges.clone()); - this.update_match_index(cx); - if !this.dismissed { - editor.update(cx, |editor, cx| { - let theme = &this.settings.borrow().theme.find; + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let ranges = ranges.await; + if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) { + this.update(&mut cx, |this, cx| { + this.editors_with_matches + .insert(editor.downgrade(), ranges.clone()); + this.update_match_index(cx); + if !this.dismissed { + editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.find; - if select_closest_match { - if let Some(match_ix) = this.active_match_index { - editor.select_ranges( - [ranges[match_ix].clone()], - Some(Autoscroll::Fit), - cx, - ); - } - } - - editor.highlight_ranges::( - ranges, - theme.match_background, + if select_closest_match { + if let Some(match_ix) = this.active_match_index { + editor.select_ranges( + [ranges[match_ix].clone()], + Some(Autoscroll::Fit), cx, ); - }); + } } + + editor.highlight_ranges::( + ranges, + theme.match_background, + cx, + ); }); } - } - Err(_) => { - this.update(&mut cx, |this, cx| { - this.query_contains_error = true; - cx.notify(); - }); - } + }); } })); } @@ -521,110 +546,6 @@ impl FindBar { } } -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(); - - if query.contains("\n") || query.contains("\\n") { - 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())); - } - } else { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .build()?; - - let mut line = String::new(); - let mut line_offset = 0; - for (chunk_ix, chunk) in buffer - .chunks(0..buffer.len(), false) - .map(|c| c.text) - .chain(["\n"]) - .enumerate() - { - if (chunk_ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - for mat in regex.find_iter(&line) { - let start = line_offset + mat.start(); - let end = line_offset + mat.end(); - ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); - } - - line_offset += line.len() + 1; - line.clear(); - } - line.push_str(text); - } - } - } - - Ok(ranges) -} - #[cfg(test)] mod tests { use super::*; @@ -687,7 +608,7 @@ mod tests { // Switch to a case sensitive search. find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchOption::CaseSensitive), cx); + find_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -744,7 +665,7 @@ mod tests { // Switch to a whole word search. find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchOption::WholeWord), cx); + find_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index f43aa3bc07..5c84c233c5 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,43 +1,45 @@ -use anyhow::Result; -use editor::{Editor, MultiBuffer}; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer}; use gpui::{ - action, elements::*, keymap::Binding, ElementBox, Entity, ModelContext, ModelHandle, - MutableAppContext, Task, View, ViewContext, ViewHandle, + action, elements::*, keymap::Binding, platform::CursorStyle, ElementBox, Entity, ModelContext, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, }; -use project::Project; -use workspace::Workspace; +use postage::watch; +use project::{search::SearchQuery, Project}; +use std::{any::TypeId, ops::Range}; +use workspace::{Settings, Workspace}; + +use crate::SearchOption; action!(Deploy); action!(Search); +action!(ToggleSearchOption, SearchOption); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ - Binding::new("cmd-shift-f", Deploy, None), + Binding::new("cmd-shift-F", Deploy, None), Binding::new("enter", Search, Some("ProjectFindView")), ]); cx.add_action(ProjectFindView::deploy); - cx.add_async_action(ProjectFindView::search); + cx.add_action(ProjectFindView::search); + cx.add_action(ProjectFindView::toggle_search_option); } struct ProjectFind { - last_search: SearchParams, project: ModelHandle, excerpts: ModelHandle, pending_search: Task>, -} - -#[derive(Default)] -struct SearchParams { - query: String, - regex: bool, - whole_word: bool, - case_sensitive: bool, + highlighted_ranges: Vec>, } struct ProjectFindView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, + case_sensitive: bool, + whole_word: bool, + regex: bool, + query_contains_error: bool, + settings: watch::Receiver, } impl Entity for ProjectFind { @@ -49,15 +51,39 @@ impl ProjectFind { let replica_id = project.read(cx).replica_id(); Self { project, - last_search: Default::default(), excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), pending_search: Task::ready(None), + highlighted_ranges: Default::default(), } } - fn search(&mut self, params: SearchParams, cx: &mut ModelContext) { - self.pending_search = cx.spawn_weak(|this, cx| async move { - // + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query, cx)); + self.pending_search = cx.spawn_weak(|this, mut cx| async move { + let matches = search.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.highlighted_ranges.clear(); + let mut matches = matches.into_iter().collect::>(); + matches + .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + for (buffer, buffer_matches) in matches { + let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( + buffer, + buffer_matches.clone(), + 1, + cx, + ); + this.highlighted_ranges.extend(ranges_to_highlight); + } + }); + cx.notify(); + }); + } None }); } @@ -74,6 +100,8 @@ impl workspace::Item for ProjectFind { ) -> Self::View { let settings = workspace.settings(); let excerpts = model.read(cx).excerpts.clone(); + cx.observe(&model, ProjectFindView::on_model_changed) + .detach(); ProjectFindView { model, query_editor: cx.add_view(|cx| { @@ -84,13 +112,20 @@ impl workspace::Item for ProjectFind { ) }), results_editor: cx.add_view(|cx| { - Editor::for_buffer( + let mut editor = Editor::for_buffer( excerpts, Some(workspace.project().clone()), settings.clone(), cx, - ) + ); + editor.set_nav_history(Some(nav_history)); + editor }), + case_sensitive: false, + whole_word: false, + regex: false, + query_contains_error: false, + settings, } } @@ -108,24 +143,43 @@ impl View for ProjectFindView { "ProjectFindView" } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { Flex::column() - .with_child(ChildView::new(&self.query_editor).boxed()) - .with_child(ChildView::new(&self.results_editor).boxed()) + .with_child(self.render_query_editor(cx)) + .with_child( + ChildView::new(&self.results_editor) + .flexible(1., true) + .boxed(), + ) .boxed() } } impl workspace::ItemView for ProjectFindView { - fn item_id(&self, cx: &gpui::AppContext) -> usize { + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &gpui::AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.results_editor).into()) + } else { + None + } + } + + fn item_id(&self, _: &gpui::AppContext) -> usize { self.model.id() } - fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + fn tab_content(&self, style: &theme::Tab, _: &gpui::AppContext) -> ElementBox { Label::new("Project Find".to_string(), style.label.clone()).boxed() } - fn project_path(&self, cx: &gpui::AppContext) -> Option { + fn project_path(&self, _: &gpui::AppContext) -> Option { None } @@ -142,15 +196,15 @@ impl workspace::ItemView for ProjectFindView { .update(cx, |editor, cx| editor.save(project, cx)) } - fn can_save_as(&self, cx: &gpui::AppContext) -> bool { + fn can_save_as(&self, _: &gpui::AppContext) -> bool { false } fn save_as( &mut self, - project: ModelHandle, - abs_path: std::path::PathBuf, - cx: &mut ViewContext, + _: ModelHandle, + _: std::path::PathBuf, + _: &mut ViewContext, ) -> Task> { unreachable!("save_as should not have been called") } @@ -162,7 +216,116 @@ impl ProjectFindView { workspace.open_item(model, cx); } - fn search(&mut self, _: &Search, cx: &mut ViewContext) -> Option>> { - todo!() + fn search(&mut self, _: &Search, cx: &mut ViewContext) { + let text = self.query_editor.read(cx).text(cx); + let query = if self.regex { + match SearchQuery::regex(text, self.case_sensitive, self.whole_word) { + Ok(query) => query, + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return; + } + } + } else { + SearchQuery::text(text, self.case_sensitive, self.whole_word) + }; + + self.model.update(cx, |model, cx| model.search(query, cx)); + } + + fn toggle_search_option( + &mut self, + ToggleSearchOption(option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, + }; + *value = !*value; + self.search(&Search, cx); + cx.notify(); + } + + fn on_model_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + let theme = &self.settings.borrow().theme.find; + self.results_editor.update(cx, |editor, cx| { + let model = self.model.read(cx); + editor.highlight_ranges::( + model.highlighted_ranges.clone(), + theme.match_background, + cx, + ); + editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + }); + } + + fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.find.invalid_editor + } else { + theme.find.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.find.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.find.option_button_group) + .aligned() + .boxed(), + ) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } + + fn render_option_button( + &self, + icon: &str, + option: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + let is_active = self.is_option_enabled(option); + MouseEventHandler::new::(option as usize, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn is_option_enabled(&self, option: SearchOption) -> bool { + match option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, + } } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 336ad737c3..3d26da9827 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -365,6 +365,14 @@ pub(crate) struct DiagnosticEndpoint { severity: DiagnosticSeverity, } +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] +pub enum CharKind { + Newline, + Punctuation, + Whitespace, + Word, +} + impl Buffer { pub fn new>>( replica_id: ReplicaId, @@ -2659,3 +2667,15 @@ pub fn contiguous_ranges( } }) } + +pub fn char_kind(c: char) -> CharKind { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a78b6356b8..0be7c643e6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,7 +1,7 @@ pub mod fs; mod ignore; mod lsp_command; -mod search; +pub mod search; pub mod worktree; use anyhow::{anyhow, Context, Result}; @@ -2175,12 +2175,7 @@ impl Project { let mut buffers_rx = buffers_rx.clone(); scope.spawn(async move { while let Some((buffer, snapshot)) = buffers_rx.next().await { - for range in query - .search( - snapshot.as_rope().bytes_in_range(0..snapshot.len()), - ) - .unwrap() - { + for range in query.search(snapshot.as_rope()).await { let range = snapshot.anchor_before(range.start) ..snapshot.anchor_after(range.end); worker_matched_buffers @@ -4893,7 +4888,7 @@ mod tests { .await; assert_eq!( - search(&project, SearchQuery::text("TWO"), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, false), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]) @@ -4911,7 +4906,7 @@ mod tests { }); assert_eq!( - search(&project, SearchQuery::text("TWO"), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, false), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]), diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 548a4c71dc..688374b8bb 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,8 +1,9 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use anyhow::Result; +use language::{char_kind, Rope}; use regex::{Regex, RegexBuilder}; +use smol::future::yield_now; use std::{ - borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, sync::Arc, @@ -10,28 +11,39 @@ use std::{ #[derive(Clone)] pub enum SearchQuery { - Text { search: Arc> }, - Regex { multiline: bool, regex: Regex }, + Text { + search: Arc>, + query: String, + whole_word: bool, + }, + Regex { + multiline: bool, + regex: Regex, + }, } impl SearchQuery { - pub fn text(query: &str) -> Self { + pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { + let query = query.to_string(); let search = AhoCorasickBuilder::new() - .auto_configure(&[query]) - .build(&[query]); + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); Self::Text { search: Arc::new(search), + query, + whole_word, } } - pub fn regex(query: &str, whole_word: bool, case_sensitive: bool) -> Result { - let mut query = Cow::Borrowed(query); + pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { + let mut query = query.to_string(); 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 = Cow::Owned(word_query); + query = word_query } let multiline = query.contains("\n") || query.contains("\\n"); @@ -44,7 +56,7 @@ impl SearchQuery { pub fn detect(&self, stream: T) -> Result { match self { - SearchQuery::Text { search } => { + SearchQuery::Text { search, .. } => { let mat = search.stream_find_iter(stream).next(); match mat { Some(Ok(_)) => Ok(true), @@ -74,35 +86,70 @@ impl SearchQuery { } } - pub fn search<'a, T: 'a + Read>(&'a self, stream: T) -> Result>> { + pub async fn search(&self, rope: &Rope) -> Vec> { + const YIELD_INTERVAL: usize = 20000; + let mut matches = Vec::new(); match self { - SearchQuery::Text { search } => { - for mat in search.stream_find_iter(stream) { - let mat = mat?; + SearchQuery::Text { + search, whole_word, .. + } => { + for (ix, mat) in search + .stream_find_iter(rope.bytes_in_range(0..rope.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + if *whole_word { + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } matches.push(mat.start()..mat.end()) } } SearchQuery::Regex { multiline, regex } => { - let mut reader = BufReader::new(stream); if *multiline { - let mut text = String::new(); - reader.read_to_string(&mut text)?; - matches.extend(regex.find_iter(&text).map(|mat| mat.start()..mat.end())); + let text = rope.to_string(); + for (ix, mat) in regex.find_iter(&text).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + matches.push(mat.start()..mat.end()); + } } else { - let mut line_ix = 0; - for line in reader.lines() { - let line = line?; - matches.extend( - regex - .find_iter(&line) - .map(|mat| (line_ix + mat.start())..(line_ix + mat.end())), - ); - line_ix += line.len(); + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + matches.push(start..end); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } } } } } - Ok(matches) + matches } } diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index cd474cc4da..3d1cb28eb1 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -48,6 +48,12 @@ impl Rope { *self = new_rope; } + pub fn slice(&self, range: Range) -> Rope { + let mut cursor = self.cursor(0); + cursor.seek_forward(range.start); + cursor.slice(range.end) + } + pub fn push(&mut self, text: &str) { let mut new_chunks = SmallVec::<[_; 16]>::new(); let mut new_chunk = ArrayString::new(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 34640211f6..483d399276 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -100,11 +100,11 @@ pub struct Find { pub container: ContainerStyle, pub editor: FindEditor, pub invalid_editor: ContainerStyle, - pub mode_button_group: ContainerStyle, - pub mode_button: ContainedText, - pub active_mode_button: ContainedText, - pub hovered_mode_button: ContainedText, - pub active_hovered_mode_button: ContainedText, + pub option_button_group: ContainerStyle, + pub option_button: ContainedText, + pub active_option_button: ContainedText, + pub hovered_option_button: ContainedText, + pub active_hovered_option_button: ContainedText, pub match_background: Color, pub match_index: ContainedText, } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index de1344239a..479b577366 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -352,7 +352,7 @@ tab_summary_spacing = 10 match_background = "$state.highlighted_line" background = "$surface.1" -[find.mode_button] +[find.option_button] extends = "$text.1" padding = { left = 6, right = 6, top = 1, bottom = 1 } corner_radius = 6 @@ -361,19 +361,19 @@ border = { width = 1, color = "$border.0" } margin.left = 1 margin.right = 1 -[find.mode_button_group] +[find.option_button_group] padding = { left = 2, right = 2 } -[find.active_mode_button] -extends = "$find.mode_button" +[find.active_option_button] +extends = "$find.option_button" background = "$surface.2" -[find.hovered_mode_button] -extends = "$find.mode_button" +[find.hovered_option_button] +extends = "$find.option_button" background = "$surface.2" -[find.active_hovered_mode_button] -extends = "$find.mode_button" +[find.active_hovered_option_button] +extends = "$find.option_button" background = "$surface.2" [find.match_index]