@@ -31,7 +31,7 @@ use crate::{
33use crate::job::{self, Job, Jobs};
33use crate::job::{self, Job, Jobs};
34−use futures_util::FutureExt;
34+use futures_util::{FutureExt, StreamExt};
35use std::num::NonZeroUsize;
35use std::num::NonZeroUsize;
36use std::{fmt, future::Future};
36use std::{fmt, future::Future};
@@ -43,6 +43,11 @@ use std::{
43use once_cell::sync::Lazy;
43use once_cell::sync::Lazy;
44use serde::de::{self, Deserialize, Deserializer};
44use serde::de::{self, Deserialize, Deserializer};
46+use grep_regex::RegexMatcher;
47+use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
48+use ignore::{DirEntry, WalkBuilder, WalkState};
49+use tokio_stream::wrappers::UnboundedReceiverStream;
46pub struct Context<'a> {
51pub struct Context<'a> {
47 pub register: Option<char>,
52 pub register: Option<char>,
48 pub count: Option<NonZeroUsize>,
53 pub count: Option<NonZeroUsize>,
@@ -209,6 +214,7 @@ impl Command {
209 search_next, "Select next search match",
214 search_next, "Select next search match",
210 extend_search_next, "Add next search match to selection",
215 extend_search_next, "Add next search match to selection",
211 search_selection, "Use current selection as search pattern",
216 search_selection, "Use current selection as search pattern",
217+ global_search, "Global Search in workspace folder",
212 extend_line, "Select current line, if already selected, extend to next line",
218 extend_line, "Select current line, if already selected, extend to next line",
213 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
219 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
214 delete_selection, "Delete selection",
220 delete_selection, "Delete selection",
@@ -1061,24 +1067,41 @@ fn select_all(cx: &mut Context) {
1062fn select_regex(cx: &mut Context) {
1068fn select_regex(cx: &mut Context) {
1063 let reg = cx.register.unwrap_or('/');
1069 let reg = cx.register.unwrap_or('/');
1064− let prompt = ui::regex_prompt(cx, "select:".into(), Some(reg), move |view, doc, regex| {
1070+ let prompt = ui::regex_prompt(
1074+ move |view, doc, regex, event| {
1075+ if event != PromptEvent::Update {
1065− let text = doc.text().slice(..);
1078+ let text = doc.text().slice(..);
1079+ if let Some(selection) =
1066− if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex)
1080+ selection::select_on_matches(text, doc.selection(view.id), ®ex)
1068− doc.set_selection(view.id, selection);
1082+ doc.set_selection(view.id, selection);
1072 cx.push_layer(Box::new(prompt));
1087 cx.push_layer(Box::new(prompt));
1075fn split_selection(cx: &mut Context) {
1090fn split_selection(cx: &mut Context) {
1076 let reg = cx.register.unwrap_or('/');
1091 let reg = cx.register.unwrap_or('/');
1077− let prompt = ui::regex_prompt(cx, "split:".into(), Some(reg), move |view, doc, regex| {
1092+ let prompt = ui::regex_prompt(
1096+ move |view, doc, regex, event| {
1097+ if event != PromptEvent::Update {
1078− let text = doc.text().slice(..);
1100+ let text = doc.text().slice(..);
1079− let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex);
1101+ let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex);
1080− doc.set_selection(view.id, selection);
1102+ doc.set_selection(view.id, selection);
1083 cx.push_layer(Box::new(prompt));
1106 cx.push_layer(Box::new(prompt));
@@ -1141,9 +1164,17 @@ fn search(cx: &mut Context) {
1141 // feed chunks into the regex yet
1164 // feed chunks into the regex yet
1142 let contents = doc.text().slice(..).to_string();
1165 let contents = doc.text().slice(..).to_string();
1144− let prompt = ui::regex_prompt(cx, "search:".into(), Some(reg), move |view, doc, regex| {
1167+ let prompt = ui::regex_prompt(
1171+ move |view, doc, regex, event| {
1172+ if event != PromptEvent::Update {
1145− search_impl(doc, view, &contents, ®ex, false);
1175+ search_impl(doc, view, &contents, ®ex, false);
1148 cx.push_layer(Box::new(prompt));
1179 cx.push_layer(Box::new(prompt));
@@ -1192,6 +1223,111 @@ fn search_selection(cx: &mut Context) {
1192 cx.editor.set_status(msg);
1223 cx.editor.set_status(msg);
1226+fn global_search(cx: &mut Context) {
1227+ let (all_matches_sx, all_matches_rx) =
1228+ tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1229+ let prompt = ui::regex_prompt(
1231+ "global search:".into(),
1233+ move |_view, _doc, regex, event| {
1234+ if event != PromptEvent::Validate {
1237+ if let Ok(matcher) = RegexMatcher::new_line_matcher(regex.as_str()) {
1238+ let searcher = SearcherBuilder::new()
1239+ .binary_detection(BinaryDetection::quit(b'\x00'))
1242+ let search_root = std::env::current_dir()
1243+ .expect("Global search error: Failed to get current dir");
1244+ WalkBuilder::new(search_root).build_parallel().run(|| {
1245+ let mut searcher_cl = searcher.clone();
1246+ let matcher_cl = matcher.clone();
1247+ let all_matches_sx_cl = all_matches_sx.clone();
1248+ Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1249+ let dent = match dent {
1251+ Err(_) => return WalkState::Continue,
1254+ match dent.file_type() {
1257+ return WalkState::Continue;
1260+ None => return WalkState::Continue,
1263+ let result_sink = sinks::UTF8(|line_num, _| {
1264+ match all_matches_sx_cl
1265+ .send((line_num as usize - 1, dent.path().to_path_buf()))
1268+ Err(_) => Ok(false),
1271+ let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1273+ if let Err(err) = result {
1274+ log::error!("Global search error: {}, {}", dent.path().display(), err);
1276+ WalkState::Continue
1280+ // Otherwise do nothing
1281+ // log::warn!("Global Search Invalid Pattern")
1286+ cx.push_layer(Box::new(prompt));
1288+ let show_picker = async move {
1289+ let all_matches: Vec<(usize, PathBuf)> =
1290+ UnboundedReceiverStream::new(all_matches_rx).collect().await;
1291+ let call: job::Callback =
1292+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1293+ if all_matches.is_empty() {
1294+ editor.set_status("No matches found".to_string());
1297+ let picker = FilePicker::new(
1299+ move |(_line_num, path)| path.to_str().unwrap().into(),
1300+ move |editor: &mut Editor, (line_num, path), action| {
1301+ match editor.open(path.into(), action) {
1304+ editor.set_error(format!(
1305+ "Failed to open file '{}': {}",
1313+ let line_num = *line_num;
1314+ let (view, doc) = current!(editor);
1315+ let text = doc.text();
1316+ let start = text.line_to_char(line_num);
1317+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
1319+ doc.set_selection(view.id, Selection::single(start, end));
1320+ align_view(doc, view, Align::Center);
1322+ |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
1324+ compositor.push(Box::new(picker));
1328+ cx.jobs.callback(show_picker);
1195fn extend_line(cx: &mut Context) {
1331fn extend_line(cx: &mut Context) {
1196 let count = cx.count();
1332 let count = cx.count();
1197 let (view, doc) = current!(cx.editor);
1333 let (view, doc) = current!(cx.editor);
@@ -3847,13 +3983,21 @@ fn join_selections(cx: &mut Context) {
3847fn keep_selections(cx: &mut Context) {
3983fn keep_selections(cx: &mut Context) {
3848 // keep selections matching regex
3984 // keep selections matching regex
3849 let reg = cx.register.unwrap_or('/');
3985 let reg = cx.register.unwrap_or('/');
3850− let prompt = ui::regex_prompt(cx, "keep:".into(), Some(reg), move |view, doc, regex| {
3986+ let prompt = ui::regex_prompt(
3990+ move |view, doc, regex, event| {
3991+ if event != PromptEvent::Update {
3851− let text = doc.text().slice(..);
3994+ let text = doc.text().slice(..);
3853− if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) {
3996+ if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) {
3854− doc.set_selection(view.id, selection);
3997+ doc.set_selection(view.id, selection);
3858 cx.push_layer(Box::new(prompt));
4002 cx.push_layer(Box::new(prompt));