From 6d9b003634aef7c3011c64b6a2a586f5a77bd43a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Feb 2022 19:07:00 +0100 Subject: [PATCH] WIP: Start sketching in `ProjectFindView` Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/find/src/buffer_find.rs | 946 +++++++++++++++++++++++++++++++ crates/find/src/find.rs | 952 +------------------------------- crates/find/src/project_find.rs | 124 ++++- crates/project/src/project.rs | 14 +- crates/project/src/search.rs | 2 +- 5 files changed, 1083 insertions(+), 955 deletions(-) create mode 100644 crates/find/src/buffer_find.rs diff --git a/crates/find/src/buffer_find.rs b/crates/find/src/buffer_find.rs new file mode 100644 index 0000000000..60349ad741 --- /dev/null +++ b/crates/find/src/buffer_find.rs @@ -0,0 +1,946 @@ +use crate::SearchOption; +use aho_corasick::AhoCorasickBuilder; +use anyhow::Result; +use collections::HashMap; +use editor::{ + char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings, + MultiBufferSnapshot, +}; +use gpui::{ + action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use postage::watch; +use regex::RegexBuilder; +use smol::future::yield_now; +use std::{ + cmp::{self, Ordering}, + ops::Range, + sync::Arc, +}; +use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; + +action!(Deploy, bool); +action!(Dismiss); +action!(FocusEditor); +action!(ToggleMode, SearchOption); +action!(GoToMatch, Direction); + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), + Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), + Binding::new("escape", Dismiss, Some("FindBar")), + Binding::new("cmd-f", FocusEditor, Some("FindBar")), + Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), + Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), + Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), + Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), + ]); + 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::go_to_match); + cx.add_action(FindBar::go_to_match_on_pane); +} + +struct FindBar { + settings: watch::Receiver, + query_editor: ViewHandle, + active_editor: Option>, + active_match_index: Option, + active_editor_subscription: Option, + editors_with_matches: HashMap, Vec>>, + pending_search: Option>, + case_sensitive_mode: bool, + whole_word_mode: bool, + regex_mode: bool, + query_contains_error: bool, + dismissed: bool, +} + +impl Entity for FindBar { + type Event = (); +} + +impl View for FindBar { + fn ui_name() -> &'static str { + "FindBar" + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); + } + + fn render(&mut 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_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)) + .contained() + .with_style(theme.find.mode_button_group) + .aligned() + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + ) + .with_children(self.active_editor.as_ref().and_then(|editor| { + let matches = self.editors_with_matches.get(&editor.downgrade())?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, matches.len()) + } else { + "No matches".to_string() + }; + + Some( + Label::new(message, theme.find.match_index.text.clone()) + .contained() + .with_style(theme.find.match_index.container) + .aligned() + .boxed(), + ) + })) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } +} + +impl Toolbar for FindBar { + fn active_item_changed( + &mut self, + item: Option>, + cx: &mut ViewContext, + ) -> bool { + self.active_editor_subscription.take(); + self.active_editor.take(); + self.pending_search.take(); + + if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { + self.active_editor_subscription = + Some(cx.subscribe(&editor, Self::on_active_editor_event)); + self.active_editor = Some(editor); + self.update_matches(false, cx); + true + } else { + false + } + } + + fn on_dismiss(&mut self, cx: &mut ViewContext) { + self.dismissed = true; + for (editor, _) in &self.editors_with_matches { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } + } + } +} + +impl FindBar { + fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::auto_height( + 2, + { + let settings = settings.clone(); + Arc::new(move |_| { + let settings = settings.borrow(); + EditorSettings { + style: settings.theme.find.editor.input.as_editor(), + tab_size: settings.tab_size, + soft_wrap: editor::SoftWrap::None, + } + }) + }, + cx, + ) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + + Self { + query_editor, + active_editor: None, + active_editor_subscription: None, + active_match_index: None, + editors_with_matches: Default::default(), + case_sensitive_mode: false, + whole_word_mode: false, + regex_mode: false, + settings, + pending_search: None, + query_contains_error: false, + dismissed: false, + } + } + + fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.read(cx).len(); + query_buffer.edit([0..len], query, cx); + }); + }); + } + + fn render_mode_button( + &self, + icon: &str, + mode: 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 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, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + enum NavButton {} + MouseEventHandler::new::(direction as usize, cx, |state, _| { + let style = if state.hovered { + &theme.hovered_mode_button + } else { + &theme.mode_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { + let settings = workspace.settings(); + workspace.active_pane().update(cx, |pane, cx| { + pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + + if let Some(find_bar) = pane + .active_toolbar() + .and_then(|toolbar| toolbar.downcast::()) + { + find_bar.update(cx, |find_bar, _| find_bar.dismissed = false); + let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); + let display_map = editor + .update(cx, |editor, cx| editor.snapshot(cx)) + .display_snapshot; + let selection = editor + .read(cx) + .newest_selection::(&display_map.buffer_snapshot); + + let mut text: String; + if selection.start == selection.end { + let point = selection.start.to_display_point(&display_map); + let range = editor::movement::surrounding_word(&display_map, point); + let range = range.start.to_offset(&display_map, Bias::Left) + ..range.end.to_offset(&display_map, Bias::Right); + text = display_map.buffer_snapshot.text_for_range(range).collect(); + if text.trim().is_empty() { + text = String::new(); + } + } else { + text = display_map + .buffer_snapshot + .text_for_range(selection.start..selection.end) + .collect(); + } + + if !text.is_empty() { + find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + } + + if *focus { + let query_editor = find_bar.read(cx).query_editor.clone(); + query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&editor::SelectAll, cx); + }); + cx.focus(&find_bar); + } + } + }); + } + + fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { + if pane.toolbar::().is_some() { + pane.dismiss_toolbar(cx); + } + } + + fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + if let Some(active_editor) = self.active_editor.as_ref() { + cx.focus(active_editor); + } + } + + 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 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, + }; + *value = !*value; + self.update_matches(true, cx); + cx.notify(); + } + + fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { + if let Some(mut index) = self.active_match_index { + if let Some(editor) = self.active_editor.as_ref() { + editor.update(cx, |editor, cx| { + let newest_selection = editor.newest_anchor_selection().clone(); + if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { + let position = newest_selection.head(); + let buffer = editor.buffer().read(cx).read(cx); + if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { + if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { + if *direction == Direction::Next { + index = 0; + } + } else if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } else if *direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } + } + + let range_to_select = ranges[index].clone(); + drop(buffer); + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + } + }); + } + } + } + + fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { + if let Some(find_bar) = pane.toolbar::() { + find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => { + self.query_contains_error = false; + self.clear_matches(cx); + self.update_matches(true, cx); + cx.notify(); + } + _ => {} + } + } + + fn on_active_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(false, cx), + editor::Event::SelectionsChanged => self.update_match_index(cx), + _ => {} + } + } + + fn clear_matches(&mut self, cx: &mut ViewContext) { + let mut active_editor_matches = None; + for (editor, ranges) in self.editors_with_matches.drain() { + if let Some(editor) = editor.upgrade(cx) { + if Some(&editor) == self.active_editor.as_ref() { + active_editor_matches = Some((editor.downgrade(), ranges)); + } else { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } + } + } + self.editors_with_matches.extend(active_editor_matches); + } + + fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext) { + let query = self.query_editor.read(cx).text(cx); + self.pending_search.take(); + if let Some(editor) = self.active_editor.as_ref() { + if query.is_empty() { + self.active_match_index.take(); + 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)) + } 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(|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; + + 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(); + }); + } + } + })); + } + } + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + self.active_match_index = self.active_match_index(cx); + cx.notify(); + } + + fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { + let editor = self.active_editor.as_ref()?; + let ranges = self.editors_with_matches.get(&editor.downgrade())?; + let editor = editor.read(cx); + let position = editor.newest_anchor_selection().head(); + if ranges.is_empty() { + None + } else { + let buffer = editor.buffer().read(cx).read(cx); + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + } + } + } +} + +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::*; + use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer}; + use gpui::{color::Color, TestAppContext}; + use std::sync::Arc; + use unindent::Unindent as _; + + #[gpui::test] + async fn test_find_simple(mut cx: TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); + theme.find.match_background = Color::red(); + let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); + + let buffer = cx.update(|cx| { + MultiBuffer::build_simple( + &r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + cx, + ) + }); + let editor = cx.add_view(Default::default(), |cx| { + Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx) + }); + + let find_bar = cx.add_view(Default::default(), |cx| { + let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx); + find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); + find_bar + }); + + // Search for a string that appears with different casing. + // By default, search is case-insensitive. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("us", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ), + ] + ); + }); + + // Switch to a case sensitive search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchOption::CaseSensitive), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("or", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + Color::red(), + ), + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + Color::red(), + ), + ( + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ( + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), + Color::red(), + ), + ] + ); + }); + + // Switch to a whole word search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchOption::WholeWord), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ] + ); + }); + + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor before the first match and ensure that going to the previous match + // selects the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + } +} diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 4be4216c37..caf8b7a843 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,954 +1,16 @@ +use gpui::MutableAppContext; + +mod buffer_find; mod project_find; -use aho_corasick::AhoCorasickBuilder; -use anyhow::Result; -use collections::HashMap; -use editor::{ - char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings, - MultiBufferSnapshot, -}; -use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, - RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use postage::watch; -use regex::RegexBuilder; -use smol::future::yield_now; -use std::{ - cmp::{self, Ordering}, - ops::Range, - sync::Arc, -}; -use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; - -action!(Deploy, bool); -action!(Dismiss); -action!(FocusEditor); -action!(ToggleMode, SearchMode); -action!(GoToMatch, Direction); - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, +pub fn init(cx: &mut MutableAppContext) { + buffer_find::init(cx); + project_find::init(cx); } #[derive(Clone, Copy)] -pub enum SearchMode { +pub enum SearchOption { WholeWord, CaseSensitive, Regex, } - -pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), - Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), - Binding::new("escape", Dismiss, Some("FindBar")), - Binding::new("cmd-f", FocusEditor, Some("FindBar")), - Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), - Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), - Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), - Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), - ]); - 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::go_to_match); - cx.add_action(FindBar::go_to_match_on_pane); -} - -struct FindBar { - settings: watch::Receiver, - query_editor: ViewHandle, - active_editor: Option>, - active_match_index: Option, - active_editor_subscription: Option, - editors_with_matches: HashMap, Vec>>, - pending_search: Option>, - case_sensitive_mode: bool, - whole_word_mode: bool, - regex_mode: bool, - query_contains_error: bool, - dismissed: bool, -} - -impl Entity for FindBar { - type Event = (); -} - -impl View for FindBar { - fn ui_name() -> &'static str { - "FindBar" - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); - } - - fn render(&mut 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_mode_button("Case", SearchMode::CaseSensitive, cx)) - .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx)) - .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx)) - .contained() - .with_style(theme.find.mode_button_group) - .aligned() - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .aligned() - .boxed(), - ) - .with_children(self.active_editor.as_ref().and_then(|editor| { - let matches = self.editors_with_matches.get(&editor.downgrade())?; - let message = if let Some(match_ix) = self.active_match_index { - format!("{}/{}", match_ix + 1, matches.len()) - } else { - "No matches".to_string() - }; - - Some( - Label::new(message, theme.find.match_index.text.clone()) - .contained() - .with_style(theme.find.match_index.container) - .aligned() - .boxed(), - ) - })) - .contained() - .with_style(theme.find.container) - .constrained() - .with_height(theme.workspace.toolbar.height) - .named("find bar") - } -} - -impl Toolbar for FindBar { - fn active_item_changed( - &mut self, - item: Option>, - cx: &mut ViewContext, - ) -> bool { - self.active_editor_subscription.take(); - self.active_editor.take(); - self.pending_search.take(); - - if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { - self.active_editor_subscription = - Some(cx.subscribe(&editor, Self::on_active_editor_event)); - self.active_editor = Some(editor); - self.update_matches(false, cx); - true - } else { - false - } - } - - fn on_dismiss(&mut self, cx: &mut ViewContext) { - self.dismissed = true; - for (editor, _) in &self.editors_with_matches { - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); - } - } - } -} - -impl FindBar { - fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::auto_height( - 2, - { - let settings = settings.clone(); - Arc::new(move |_| { - let settings = settings.borrow(); - EditorSettings { - style: settings.theme.find.editor.input.as_editor(), - tab_size: settings.tab_size, - soft_wrap: editor::SoftWrap::None, - } - }) - }, - cx, - ) - }); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); - - Self { - query_editor, - active_editor: None, - active_editor_subscription: None, - active_match_index: None, - editors_with_matches: Default::default(), - case_sensitive_mode: false, - whole_word_mode: false, - regex_mode: false, - settings, - pending_search: None, - query_contains_error: false, - dismissed: false, - } - } - - fn set_query(&mut self, query: &str, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.read(cx).len(); - query_buffer.edit([0..len], query, cx); - }); - }); - } - - fn render_mode_button( - &self, - icon: &str, - mode: SearchMode, - 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 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, - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() - } - - fn render_nav_button( - &self, - icon: &str, - direction: Direction, - cx: &mut RenderContext, - ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; - enum NavButton {} - MouseEventHandler::new::(direction as usize, cx, |state, _| { - let style = if state.hovered { - &theme.hovered_mode_button - } else { - &theme.mode_button - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() - } - - fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { - let settings = workspace.settings(); - workspace.active_pane().update(cx, |pane, cx| { - pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); - - if let Some(find_bar) = pane - .active_toolbar() - .and_then(|toolbar| toolbar.downcast::()) - { - find_bar.update(cx, |find_bar, _| find_bar.dismissed = false); - let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); - let display_map = editor - .update(cx, |editor, cx| editor.snapshot(cx)) - .display_snapshot; - let selection = editor - .read(cx) - .newest_selection::(&display_map.buffer_snapshot); - - let mut text: String; - if selection.start == selection.end { - let point = selection.start.to_display_point(&display_map); - let range = editor::movement::surrounding_word(&display_map, point); - let range = range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Right); - text = display_map.buffer_snapshot.text_for_range(range).collect(); - if text.trim().is_empty() { - text = String::new(); - } - } else { - text = display_map - .buffer_snapshot - .text_for_range(selection.start..selection.end) - .collect(); - } - - if !text.is_empty() { - find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); - } - - if *focus { - let query_editor = find_bar.read(cx).query_editor.clone(); - query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&editor::SelectAll, cx); - }); - cx.focus(&find_bar); - } - } - }); - } - - fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { - if pane.toolbar::().is_some() { - pane.dismiss_toolbar(cx); - } - } - - fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { - if let Some(active_editor) = self.active_editor.as_ref() { - cx.focus(active_editor); - } - } - - fn is_mode_enabled(&self, mode: SearchMode) -> bool { - match mode { - SearchMode::WholeWord => self.whole_word_mode, - SearchMode::CaseSensitive => self.case_sensitive_mode, - SearchMode::Regex => self.regex_mode, - } - } - - fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { - let value = match mode { - SearchMode::WholeWord => &mut self.whole_word_mode, - SearchMode::CaseSensitive => &mut self.case_sensitive_mode, - SearchMode::Regex => &mut self.regex_mode, - }; - *value = !*value; - self.update_matches(true, cx); - cx.notify(); - } - - fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { - if let Some(mut index) = self.active_match_index { - if let Some(editor) = self.active_editor.as_ref() { - editor.update(cx, |editor, cx| { - let newest_selection = editor.newest_anchor_selection().clone(); - if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { - let position = newest_selection.head(); - let buffer = editor.buffer().read(cx).read(cx); - if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { - if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } - } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { - if *direction == Direction::Next { - index = 0; - } - } else if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } else if *direction == Direction::Next { - if index == ranges.len() - 1 { - index = 0 - } else { - index += 1; - } - } - - let range_to_select = ranges[index].clone(); - drop(buffer); - editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); - } - }); - } - } - } - - fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { - if let Some(find_bar) = pane.toolbar::() { - find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); - } - } - - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Edited => { - self.query_contains_error = false; - self.clear_matches(cx); - self.update_matches(true, cx); - cx.notify(); - } - _ => {} - } - } - - fn on_active_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Edited => self.update_matches(false, cx), - editor::Event::SelectionsChanged => self.update_match_index(cx), - _ => {} - } - } - - fn clear_matches(&mut self, cx: &mut ViewContext) { - let mut active_editor_matches = None; - for (editor, ranges) in self.editors_with_matches.drain() { - if let Some(editor) = editor.upgrade(cx) { - if Some(&editor) == self.active_editor.as_ref() { - active_editor_matches = Some((editor.downgrade(), ranges)); - } else { - editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); - } - } - } - self.editors_with_matches.extend(active_editor_matches); - } - - fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext) { - let query = self.query_editor.read(cx).text(cx); - self.pending_search.take(); - if let Some(editor) = self.active_editor.as_ref() { - if query.is_empty() { - self.active_match_index.take(); - 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)) - } 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(|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; - - 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(); - }); - } - } - })); - } - } - } - - fn update_match_index(&mut self, cx: &mut ViewContext) { - self.active_match_index = self.active_match_index(cx); - cx.notify(); - } - - fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { - let editor = self.active_editor.as_ref()?; - let ranges = self.editors_with_matches.get(&editor.downgrade())?; - let editor = editor.read(cx); - let position = editor.newest_anchor_selection().head(); - if ranges.is_empty() { - None - } else { - let buffer = editor.buffer().read(cx).read(cx); - match ranges.binary_search_by(|probe| { - if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { - Ordering::Less - } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { - Ordering::Greater - } else { - Ordering::Equal - } - }) { - Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), - } - } - } -} - -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::*; - use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer}; - use gpui::{color::Color, TestAppContext}; - use std::sync::Arc; - use unindent::Unindent as _; - - #[gpui::test] - async fn test_find_simple(mut cx: TestAppContext) { - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); - theme.find.match_background = Color::red(); - let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); - - let buffer = cx.update(|cx| { - MultiBuffer::build_simple( - &r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - cx, - ) - }); - let editor = cx.add_view(Default::default(), |cx| { - Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx) - }); - - let find_bar = cx.add_view(Default::default(), |cx| { - let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx); - find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); - find_bar - }); - - // Search for a string that appears with different casing. - // By default, search is case-insensitive. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("us", cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[ - ( - DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - Color::red(), - ), - ( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - ), - ] - ); - }); - - // Switch to a case sensitive search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - )] - ); - }); - - // Search for a string that appears both as a whole word and - // within other words. By default, all results are found. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("or", cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[ - ( - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), - Color::red(), - ), - ( - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), - ), - ( - DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), - Color::red(), - ), - ( - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), - Color::red(), - ), - ( - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), - ), - ( - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), - ), - ( - DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), - Color::red(), - ), - ] - ); - }); - - // Switch to a whole word search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[ - ( - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), - ), - ( - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), - ), - ( - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), - ), - ] - ); - }); - - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the previous match selects - // the closest match to the left. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the next match selects the - // closest match to the right. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); - }); - - // Park the cursor after the last match and ensure that going to the previous match selects - // the last match. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - - // Park the cursor after the last match and ensure that going to the next match selects the - // first match. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - // Park the cursor before the first match and ensure that going to the previous match - // selects the last match. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - } -} diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 8bc2777a6f..fee56502ce 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,7 +1,24 @@ -use crate::SearchMode; -use editor::MultiBuffer; -use gpui::{Entity, ModelContext, ModelHandle, Task}; +use anyhow::Result; +use editor::{Editor, MultiBuffer}; +use gpui::{ + action, elements::*, keymap::Binding, ElementBox, Entity, Handle, ModelContext, ModelHandle, + MutableAppContext, Task, View, ViewContext, ViewHandle, +}; use project::Project; +use std::{borrow::Borrow, sync::Arc}; +use workspace::Workspace; + +action!(Deploy); +action!(Search); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-shift-f", Deploy, None), + Binding::new("enter", Search, Some("ProjectFindView")), + ]); + cx.add_action(ProjectFindView::deploy); + cx.add_async_action(ProjectFindView::search); +} struct ProjectFind { last_search: SearchParams, @@ -20,6 +37,8 @@ struct SearchParams { struct ProjectFindView { model: ModelHandle, + query_editor: ViewHandle, + results_editor: ViewHandle, } impl Entity for ProjectFind { @@ -44,3 +63,102 @@ impl ProjectFind { }); } } + +impl workspace::Item for ProjectFind { + type View = ProjectFindView; + + fn build_view( + model: ModelHandle, + workspace: &workspace::Workspace, + nav_history: workspace::ItemNavHistory, + cx: &mut gpui::ViewContext, + ) -> Self::View { + let settings = workspace.settings(); + let excerpts = model.read(cx).excerpts.clone(); + let build_settings = editor::settings_builder(excerpts.downgrade(), workspace.settings()); + ProjectFindView { + model, + query_editor: cx.add_view(|cx| Editor::single_line(build_settings.clone(), cx)), + results_editor: cx.add_view(|cx| { + Editor::for_buffer( + excerpts, + build_settings, + Some(workspace.project().clone()), + cx, + ) + }), + } + } + + fn project_path(&self) -> Option { + None + } +} + +impl Entity for ProjectFindView { + type Event = (); +} + +impl View for ProjectFindView { + fn ui_name() -> &'static str { + "ProjectFindView" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + Flex::column() + .with_child(ChildView::new(&self.query_editor).boxed()) + .with_child(ChildView::new(&self.results_editor).boxed()) + .boxed() + } +} + +impl workspace::ItemView for ProjectFindView { + fn item_id(&self, cx: &gpui::AppContext) -> usize { + self.model.id() + } + + fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + Label::new("Project Find".to_string(), style.label.clone()).boxed() + } + + fn project_path(&self, cx: &gpui::AppContext) -> Option { + None + } + + fn can_save(&self, _: &gpui::AppContext) -> bool { + true + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn can_save_as(&self, cx: &gpui::AppContext) -> bool { + false + } + + fn save_as( + &mut self, + project: ModelHandle, + abs_path: std::path::PathBuf, + cx: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } +} + +impl ProjectFindView { + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); + } + + fn search(&mut self, _: &Search, cx: &mut ViewContext) -> Option>> { + todo!() + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index aa9e47fcb7..a78b6356b8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2108,7 +2108,7 @@ impl Project { let matches = if let Some(file) = fs.open_sync(&path).await.log_err() { - query.is_contained_in_stream(file).unwrap_or(false) + query.detect(file).unwrap_or(false) } else { false }; @@ -2132,7 +2132,7 @@ impl Project { .detach(); let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); - let buffers = self + let open_buffers = self .buffers_state .borrow() .open_buffers @@ -2140,9 +2140,9 @@ impl Project { .filter_map(|b| b.upgrade(cx)) .collect::>(); cx.spawn(|this, mut cx| async move { - for buffer in buffers { + for buffer in &open_buffers { let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; + buffers_tx.send((buffer.clone(), snapshot)).await?; } while let Some(project_path) = matching_paths_rx.next().await { @@ -2151,8 +2151,10 @@ impl Project { .await .log_err() { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; + if !open_buffers.contains(&buffer) { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer, snapshot)).await?; + } } } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 69be605c93..548a4c71dc 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -42,7 +42,7 @@ impl SearchQuery { Ok(Self::Regex { multiline, regex }) } - pub fn is_contained_in_stream(&self, stream: T) -> Result { + pub fn detect(&self, stream: T) -> Result { match self { SearchQuery::Text { search } => { let mat = search.stream_find_iter(stream).next();