diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 774926b2c43a..d49bb8114e90 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -53,13 +53,13 @@ use crate::{ filter_picker_entry, job::Callback, ui::{ - self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker, - Popup, Prompt, PromptEvent, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, + DynamicPicker, Picker, Popup, Prompt, PromptEvent, }, }; use crate::job::{self, Jobs}; -use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt}; +use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryStreamExt}; use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashSet, num::NonZeroUsize}; @@ -2015,152 +2015,165 @@ fn global_search(cx: &mut Context) { path: PathBuf, /// 0 indexed lines line_num: usize, + line_content: String, } impl FileResult { - fn new(path: &Path, line_num: usize) -> Self { + fn new(path: &Path, line_num: usize, line_content: String) -> Self { Self { path: path.to_path_buf(), line_num, + line_content, } } } - impl ui::menu::Item for FileResult { - type Data = Option; - - fn format(&self, current_path: &Self::Data) -> Row { - let relative_path = helix_core::path::get_relative_path(&self.path) - .to_string_lossy() - .into_owned(); - if current_path - .as_ref() - .map(|p| p == &self.path) - .unwrap_or(false) - { - format!("{} (*)", relative_path).into() - } else { - relative_path.into() - } - } - } - - let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::(); let config = cx.editor.config(); let smart_case = config.search.smart_case; let file_picker_config = config.file_picker.clone(); - let reg = cx.register.unwrap_or('/'); + let get_files = move |query: String, _editor: &mut Editor| { + if query.is_empty() { + return async { Ok(Vec::new()) }.boxed(); + } - let completions = search_completions(cx, Some(reg)); - ui::regex_prompt( - cx, - "global-search:".into(), - Some(reg), - move |_editor: &Editor, input: &str| { - completions - .iter() - .filter(|comp| comp.starts_with(input)) - .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) - .collect() - }, - move |_editor, regex, event| { - if event != PromptEvent::Validate { - return; + let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::(); + let matcher = match RegexMatcherBuilder::new() + .case_smart(smart_case) + .build(&query) + { + Ok(matcher) => matcher, + Err(err) => { + log::info!("Failed to compile search pattern in global search: {}", err); + return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed(); } + }; - if let Ok(matcher) = RegexMatcherBuilder::new() - .case_smart(smart_case) - .build(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"); - let dedup_symlinks = file_picker_config.deduplicate_links; - let absolute_root = search_root - .canonicalize() - .unwrap_or_else(|_| search_root.clone()); - - WalkBuilder::new(search_root) - .hidden(file_picker_config.hidden) - .parents(file_picker_config.parents) - .ignore(file_picker_config.ignore) - .follow_links(file_picker_config.follow_symlinks) - .git_ignore(file_picker_config.git_ignore) - .git_global(file_picker_config.git_global) - .git_exclude(file_picker_config.git_exclude) - .max_depth(file_picker_config.max_depth) - .filter_entry(move |entry| { - filter_picker_entry(entry, &absolute_root, dedup_symlinks) - }) - .build_parallel() - .run(|| { - let mut searcher = searcher.clone(); - let matcher = matcher.clone(); - let all_matches_sx = all_matches_sx.clone(); - Box::new(move |entry: Result| -> WalkState { - let entry = match entry { - Ok(entry) => entry, - Err(_) => return WalkState::Continue, - }; - - match entry.file_type() { - Some(entry) if entry.is_file() => {} - // skip everything else - _ => return WalkState::Continue, - }; - - let result = searcher.search_path( - &matcher, - entry.path(), - sinks::UTF8(|line_num, _| { - all_matches_sx - .send(FileResult::new(entry.path(), line_num as usize - 1)) - .unwrap(); - - Ok(true) - }), - ); - - if let Err(err) = result { - log::error!( - "Global search error: {}, {}", - entry.path().display(), - err - ); - } - WalkState::Continue - }) - }); - } else { - // Otherwise do nothing - // log::warn!("Global Search Invalid Pattern") - } - }, - ); + 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"); + let dedup_symlinks = file_picker_config.deduplicate_links; + let absolute_root = search_root + .canonicalize() + .unwrap_or_else(|_| search_root.clone()); + + WalkBuilder::new(search_root) + .hidden(file_picker_config.hidden) + .parents(file_picker_config.parents) + .ignore(file_picker_config.ignore) + .follow_links(file_picker_config.follow_symlinks) + .git_ignore(file_picker_config.git_ignore) + .git_global(file_picker_config.git_global) + .git_exclude(file_picker_config.git_exclude) + .max_depth(file_picker_config.max_depth) + .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks)) + .build_parallel() + .run(|| { + let mut searcher = searcher.clone(); + let matcher = matcher.clone(); + let all_matches_sx = all_matches_sx.clone(); + Box::new(move |entry: Result| -> WalkState { + let entry = match entry { + Ok(entry) => entry, + Err(_) => return WalkState::Continue, + }; + + match entry.file_type() { + Some(entry) if entry.is_file() => {} + // skip everything else + _ => return WalkState::Continue, + }; - let current_path = doc_mut!(cx.editor).path().cloned(); + let result = searcher.search_path( + &matcher, + entry.path(), + sinks::UTF8(|line_num, line_content| { + all_matches_sx + .send(FileResult::new( + entry.path(), + line_num as usize - 1, + line_content.to_string(), + )) + .unwrap(); + + Ok(true) + }), + ); + + if let Err(err) = result { + log::error!("Global search error: {}, {}", entry.path().display(), err); + } + WalkState::Continue + }) + }); + + async move { + let all_matches: Vec = + UnboundedReceiverStream::new(all_matches_rx).collect().await; + Ok(all_matches) + } + .boxed() + }; + + let initial_query = cx + .register + .and_then(|reg| cx.editor.registers.last(reg)) + .cloned() + .unwrap_or_default(); + let files_query = get_files(initial_query.clone(), cx.editor); let show_picker = async move { - let all_matches: Vec = - UnboundedReceiverStream::new(all_matches_rx).collect().await; + let query_result = files_query.await; + let call: job::Callback = Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - if all_matches.is_empty() { - editor.set_status("No matches found"); - return; - } + let all_matches = match query_result { + Ok(matches) => matches, + Err(err) => { + editor.set_error(format!("Failed to run search: {}", err)); + return; + } + }; - let columns = vec![]; + let current_path = doc!(editor).path().cloned(); + + let columns = vec![ + ui::PickerColumn::new( + "path", + |item: &FileResult, current_path: &Option| { + let relative_path = helix_core::path::get_truncated_path(&item.path) + .to_string_lossy() + .into_owned(); + if current_path + .as_ref() + .map(|p| p == &item.path) + .unwrap_or(false) + { + format!("{} (*)", relative_path).into() + } else { + relative_path.into() + } + }, + ) + .with_filter_text(|item: &FileResult, _| { + helix_core::path::get_relative_path(&item.path) + .to_string_lossy() + .into_owned() + .into() + }), + ui::PickerColumn::new("contents", |item: &FileResult, _| { + item.line_content.as_str().into() + }), + ]; let picker = Picker::new( columns, all_matches, current_path, - move |cx, FileResult { path, line_num }, action| { + move |cx, FileResult { path, line_num, .. }, action| { match cx.editor.open(path, action) { Ok(_) => {} Err(e) => { @@ -2185,10 +2198,12 @@ fn global_search(cx: &mut Context) { doc.set_selection(view.id, Selection::single(start, end)); align_view(doc, view, Align::Center); - }).with_preview(|_editor, FileResult { path, line_num }| { + }).with_preview(|_editor, FileResult { path, line_num, .. }| { Some((path.clone().into(), Some((*line_num, *line_num)))) - }); - compositor.push(Box::new(overlaid(picker))); + }).with_line(initial_query, editor); + + let dyn_picker = DynamicPicker::new(picker, 1, Box::new(get_files)); + compositor.push(Box::new(overlaid(dyn_picker))); }, )); Ok(call)