Skip to content

Commit

Permalink
Initial implementation of global search (#651)
Browse files Browse the repository at this point in the history
* initial implementation of global search

* use tokio::sync::mpsc::unbounded_channel instead of Arc, Mutex, Waker poll_fn

* use tokio_stream::wrappers::UnboundedReceiverStream to collect all search matches

* regex_prompt: unified callback; refactor

* global search doc
  • Loading branch information
pppKin authored Sep 21, 2021
1 parent a512f48 commit 9456d5c
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 25 deletions.
74 changes: 74 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,10 @@ This layer is a kludge of mappings, mostly pickers.
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |


> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
#### Unimpaired

Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
Expand Down
5 changes: 5 additions & 0 deletions helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,10 @@ toml = "0.5"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }

# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
tokio-stream = "0.1.7"

[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
188 changes: 166 additions & 22 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::{
};

use crate::job::{self, Job, Jobs};
use futures_util::FutureExt;
use futures_util::{FutureExt, StreamExt};
use std::num::NonZeroUsize;
use std::{fmt, future::Future};

Expand All @@ -43,6 +43,11 @@ use std::{
use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer};

use grep_regex::RegexMatcher;
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
use ignore::{DirEntry, WalkBuilder, WalkState};
use tokio_stream::wrappers::UnboundedReceiverStream;

pub struct Context<'a> {
pub register: Option<char>,
pub count: Option<NonZeroUsize>,
Expand Down Expand Up @@ -209,6 +214,7 @@ impl Command {
search_next, "Select next search match",
extend_search_next, "Add next search match to selection",
search_selection, "Use current selection as search pattern",
global_search, "Global Search in workspace folder",
extend_line, "Select current line, if already selected, extend to next line",
extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
delete_selection, "Delete selection",
Expand Down Expand Up @@ -1061,24 +1067,41 @@ fn select_all(cx: &mut Context) {

fn select_regex(cx: &mut Context) {
let reg = cx.register.unwrap_or('/');
let prompt = ui::regex_prompt(cx, "select:".into(), Some(reg), move |view, doc, regex| {
let text = doc.text().slice(..);
if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), &regex)
{
doc.set_selection(view.id, selection);
}
});
let prompt = ui::regex_prompt(
cx,
"select:".into(),
Some(reg),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
}
let text = doc.text().slice(..);
if let Some(selection) =
selection::select_on_matches(text, doc.selection(view.id), &regex)
{
doc.set_selection(view.id, selection);
}
},
);

cx.push_layer(Box::new(prompt));
}

fn split_selection(cx: &mut Context) {
let reg = cx.register.unwrap_or('/');
let prompt = ui::regex_prompt(cx, "split:".into(), Some(reg), move |view, doc, regex| {
let text = doc.text().slice(..);
let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
doc.set_selection(view.id, selection);
});
let prompt = ui::regex_prompt(
cx,
"split:".into(),
Some(reg),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
}
let text = doc.text().slice(..);
let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
doc.set_selection(view.id, selection);
},
);

cx.push_layer(Box::new(prompt));
}
Expand Down Expand Up @@ -1141,9 +1164,17 @@ fn search(cx: &mut Context) {
// feed chunks into the regex yet
let contents = doc.text().slice(..).to_string();

let prompt = ui::regex_prompt(cx, "search:".into(), Some(reg), move |view, doc, regex| {
search_impl(doc, view, &contents, &regex, false);
});
let prompt = ui::regex_prompt(
cx,
"search:".into(),
Some(reg),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
}
search_impl(doc, view, &contents, &regex, false);
},
);

cx.push_layer(Box::new(prompt));
}
Expand Down Expand Up @@ -1192,6 +1223,111 @@ fn search_selection(cx: &mut Context) {
cx.editor.set_status(msg);
}

fn global_search(cx: &mut Context) {
let (all_matches_sx, all_matches_rx) =
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
let prompt = ui::regex_prompt(
cx,
"global search:".into(),
None,
move |_view, _doc, regex, event| {
if event != PromptEvent::Validate {
return;
}
if let Ok(matcher) = RegexMatcher::new_line_matcher(regex.as_str()) {
let searcher = SearcherBuilder::new()
.binary_detection(BinaryDetection::quit(b'\x00'))
.build();

let search_root = std::env::current_dir()
.expect("Global search error: Failed to get current dir");
WalkBuilder::new(search_root).build_parallel().run(|| {
let mut searcher_cl = searcher.clone();
let matcher_cl = matcher.clone();
let all_matches_sx_cl = all_matches_sx.clone();
Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
let dent = match dent {
Ok(dent) => dent,
Err(_) => return WalkState::Continue,
};

match dent.file_type() {
Some(fi) => {
if !fi.is_file() {
return WalkState::Continue;
}
}
None => return WalkState::Continue,
}

let result_sink = sinks::UTF8(|line_num, _| {
match all_matches_sx_cl
.send((line_num as usize - 1, dent.path().to_path_buf()))
{
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
});
let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);

if let Err(err) = result {
log::error!("Global search error: {}, {}", dent.path().display(), err);
}
WalkState::Continue
})
});
} else {
// Otherwise do nothing
// log::warn!("Global Search Invalid Pattern")
}
},
);

cx.push_layer(Box::new(prompt));

let show_picker = async move {
let all_matches: Vec<(usize, PathBuf)> =
UnboundedReceiverStream::new(all_matches_rx).collect().await;
let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
if all_matches.is_empty() {
editor.set_status("No matches found".to_string());
return;
}
let picker = FilePicker::new(
all_matches,
move |(_line_num, path)| path.to_str().unwrap().into(),
move |editor: &mut Editor, (line_num, path), action| {
match editor.open(path.into(), action) {
Ok(_) => {}
Err(e) => {
editor.set_error(format!(
"Failed to open file '{}': {}",
path.display(),
e
));
return;
}
}

let line_num = *line_num;
let (view, doc) = current!(editor);
let text = doc.text();
let start = text.line_to_char(line_num);
let end = text.line_to_char((line_num + 1).min(text.len_lines()));

doc.set_selection(view.id, Selection::single(start, end));
align_view(doc, view, Align::Center);
},
|_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
);
compositor.push(Box::new(picker));
});
Ok(call)
};
cx.jobs.callback(show_picker);
}

fn extend_line(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
Expand Down Expand Up @@ -3847,13 +3983,21 @@ fn join_selections(cx: &mut Context) {
fn keep_selections(cx: &mut Context) {
// keep selections matching regex
let reg = cx.register.unwrap_or('/');
let prompt = ui::regex_prompt(cx, "keep:".into(), Some(reg), move |view, doc, regex| {
let text = doc.text().slice(..);
let prompt = ui::regex_prompt(
cx,
"keep:".into(),
Some(reg),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
}
let text = doc.text().slice(..);

if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
doc.set_selection(view.id, selection);
}
});
if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
doc.set_selection(view.id, selection);
}
},
);

cx.push_layer(Box::new(prompt));
}
Expand Down
Loading

0 comments on commit 9456d5c

Please sign in to comment.