Refactor global_search as a dynamic Picker

pull/9647/head
Michael Davis 9 months ago
parent 5622db6798
commit 1d023b05ac
No known key found for this signature in database

@ -3,6 +3,7 @@ pub(crate) mod lsp;
pub(crate) mod typed; pub(crate) mod typed;
pub use dap::*; pub use dap::*;
use futures_util::FutureExt;
use helix_event::status; use helix_event::status;
use helix_stdx::{ use helix_stdx::{
path::expand_tilde, path::expand_tilde,
@ -2255,222 +2256,204 @@ fn global_search(cx: &mut Context) {
} }
} }
struct GlobalSearchConfig {
smart_case: bool,
file_picker_config: helix_view::editor::FilePickerConfig,
}
let config = cx.editor.config(); let config = cx.editor.config();
let smart_case = config.search.smart_case; let config = GlobalSearchConfig {
let file_picker_config = config.file_picker.clone(); smart_case: config.search.smart_case,
file_picker_config: config.file_picker.clone(),
};
let reg = cx.register.unwrap_or('/'); let columns = vec![
let completions = search_completions(cx, Some(reg)); PickerColumn::new("path", |item: &FileResult, _| {
ui::raw_regex_prompt( let path = helix_stdx::path::get_relative_path(&item.path);
cx, format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into()
"global-search:".into(), }),
Some(reg), PickerColumn::new("contents", |item: &FileResult, _| {
move |_editor: &Editor, input: &str| { item.line_content.as_str().into()
completions })
.iter() .without_filtering(),
.filter(|comp| comp.starts_with(input)) ];
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect()
},
move |cx, _, input, event| {
if event != PromptEvent::Validate {
return;
}
cx.editor.registers.last_search_register = reg;
let current_path = doc_mut!(cx.editor).path().cloned(); let get_files = |query: &str,
let documents: Vec<_> = cx editor: &mut Editor,
.editor config: std::sync::Arc<GlobalSearchConfig>,
.documents() injector: &ui::picker::Injector<_, _>| {
.map(|doc| (doc.path().cloned(), doc.text().to_owned())) if query.is_empty() {
.collect(); return async { Ok(()) }.boxed();
}
if let Ok(matcher) = RegexMatcherBuilder::new() let search_root = helix_stdx::env::current_working_dir();
.case_smart(smart_case) if !search_root.exists() {
.build(input) return async { Err(anyhow::anyhow!("Current working directory does not exist")) }
{ .boxed();
let search_root = helix_stdx::env::current_working_dir(); }
if !search_root.exists() {
cx.editor let documents: Vec<_> = editor
.set_error("Current working directory does not exist"); .documents()
return; .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
} .collect();
let columns = vec![ let matcher = match RegexMatcherBuilder::new()
PickerColumn::new( .case_smart(config.smart_case)
"path", .build(query)
|item: &FileResult, current_path: &Option<PathBuf>| { {
let relative_path = helix_stdx::path::get_relative_path(&item.path) Ok(matcher) => {
.to_string_lossy() // Clear any "Failed to compile regex" errors out of the statusline.
.into_owned(); editor.clear_status();
if current_path matcher
}
Err(err) => {
log::info!("Failed to compile search pattern in global search: {}", err);
return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed();
}
};
let dedup_symlinks = config.file_picker_config.deduplicate_links;
let absolute_root = search_root
.canonicalize()
.unwrap_or_else(|_| search_root.clone());
let injector = injector.clone();
async move {
let searcher = SearcherBuilder::new()
.binary_detection(BinaryDetection::quit(b'\x00'))
.build();
WalkBuilder::new(search_root)
.hidden(config.file_picker_config.hidden)
.parents(config.file_picker_config.parents)
.ignore(config.file_picker_config.ignore)
.follow_links(config.file_picker_config.follow_symlinks)
.git_ignore(config.file_picker_config.git_ignore)
.git_global(config.file_picker_config.git_global)
.git_exclude(config.file_picker_config.git_exclude)
.max_depth(config.file_picker_config.max_depth)
.filter_entry(move |entry| {
filter_picker_entry(entry, &absolute_root, dedup_symlinks)
})
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"))
.add_custom_ignore_filename(".helix/ignore")
.build_parallel()
.run(|| {
let mut searcher = searcher.clone();
let matcher = matcher.clone();
let injector = injector.clone();
let documents = &documents;
Box::new(move |entry: Result<DirEntry, ignore::Error>| -> 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 mut stop = false;
let sink = sinks::UTF8(|line_num, line_content| {
stop = injector
.push(FileResult::new(
entry.path(),
line_num as usize - 1,
line_content.to_string(),
))
.is_err();
Ok(!stop)
});
let doc = documents.iter().find(|&(doc_path, _)| {
doc_path
.as_ref() .as_ref()
.map(|p| p == &item.path) .map_or(false, |doc_path| doc_path == entry.path())
.unwrap_or(false) });
{
format!("{} (*)", relative_path).into() let result = if let Some((_, doc)) = doc {
// there is already a buffer for this file
// search the buffer instead of the file because it's faster
// and captures new edits without requiring a save
if searcher.multi_line_with_matcher(&matcher) {
// in this case a continous buffer is required
// convert the rope to a string
let text = doc.to_string();
searcher.search_slice(&matcher, text.as_bytes(), sink)
} else { } else {
relative_path.into() searcher.search_reader(
&matcher,
RopeReader::new(doc.slice(..)),
sink,
)
} }
}, } else {
), searcher.search_path(&matcher, entry.path(), sink)
PickerColumn::new("contents", |item: &FileResult, _| { };
item.line_content.as_str().into()
}), if let Err(err) = result {
]; log::error!("Global search error: {}, {}", entry.path().display(), err);
let (picker, injector) = Picker::stream(columns, current_path); }
if stop {
let dedup_symlinks = file_picker_config.deduplicate_links; WalkState::Quit
let absolute_root = search_root } else {
.canonicalize() WalkState::Continue
.unwrap_or_else(|_| search_root.clone()); }
let injector_ = injector.clone(); })
std::thread::spawn(move || {
let searcher = SearcherBuilder::new()
.binary_detection(BinaryDetection::quit(b'\x00'))
.build();
let mut walk_builder = WalkBuilder::new(search_root);
walk_builder
.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)
})
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"))
.add_custom_ignore_filename(".helix/ignore")
.build_parallel()
.run(|| {
let mut searcher = searcher.clone();
let matcher = matcher.clone();
let injector = injector_.clone();
let documents = &documents;
Box::new(move |entry: Result<DirEntry, ignore::Error>| -> 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 mut stop = false;
let sink = sinks::UTF8(|line_num, line_content| {
stop = injector
.push(FileResult::new(
entry.path(),
line_num as usize - 1,
line_content.to_string(),
))
.is_err();
Ok(!stop)
});
let doc = documents.iter().find(|&(doc_path, _)| {
doc_path
.as_ref()
.map_or(false, |doc_path| doc_path == entry.path())
});
let result = if let Some((_, doc)) = doc {
// there is already a buffer for this file
// search the buffer instead of the file because it's faster
// and captures new edits without requiring a save
if searcher.multi_line_with_matcher(&matcher) {
// in this case a continous buffer is required
// convert the rope to a string
let text = doc.to_string();
searcher.search_slice(&matcher, text.as_bytes(), sink)
} else {
searcher.search_reader(
&matcher,
RopeReader::new(doc.slice(..)),
sink,
)
}
} else {
searcher.search_path(&matcher, entry.path(), sink)
};
if let Err(err) = result {
log::error!(
"Global search error: {}, {}",
entry.path().display(),
err
);
}
if stop {
WalkState::Quit
} else {
WalkState::Continue
}
})
});
}); });
Ok(())
}
.boxed()
};
cx.jobs.callback(async move { let mut picker = Picker::new(
let call = move |_: &mut Editor, compositor: &mut Compositor| { columns,
let picker = Picker::with_stream( 1, // contents
picker, vec![],
0, config,
injector, move |cx, FileResult { path, line_num, .. }, action| {
move |cx, FileResult { path, line_num, .. }, action| { let doc = match cx.editor.open(path, action) {
let doc = match cx.editor.open(path, action) { Ok(id) => doc_mut!(cx.editor, &id),
Ok(id) => doc_mut!(cx.editor, &id), Err(e) => {
Err(e) => { cx.editor
cx.editor.set_error(format!( .set_error(format!("Failed to open file '{}': {}", path.display(), e));
"Failed to open file '{}': {}", return;
path.display(), }
e };
));
return;
}
};
let line_num = *line_num; let line_num = *line_num;
let view = view_mut!(cx.editor); let view = view_mut!(cx.editor);
let text = doc.text(); let text = doc.text();
if line_num >= text.len_lines() { if line_num >= text.len_lines() {
cx.editor.set_error( cx.editor.set_error(
"The line you jumped to does not exist anymore because the file has changed.", "The line you jumped to does not exist anymore because the file has changed.",
); );
return; return;
} }
let start = text.line_to_char(line_num); let start = text.line_to_char(line_num);
let end = text.line_to_char((line_num + 1).min(text.len_lines())); let end = text.line_to_char((line_num + 1).min(text.len_lines()));
doc.set_selection(view.id, Selection::single(start, end)); doc.set_selection(view.id, Selection::single(start, end));
if action.align_view(view, doc.id()) { if action.align_view(view, doc.id()) {
align_view(doc, view, Align::Center); align_view(doc, view, Align::Center);
}
},
)
.with_preview(
|_editor, FileResult { path, line_num, .. }| {
Some((path.clone().into(), Some((*line_num, *line_num))))
},
);
compositor.push(Box::new(overlaid(picker)))
};
Ok(Callback::EditorCompositor(Box::new(call)))
})
} else {
// Otherwise do nothing
// log::warn!("Global Search Invalid Pattern")
} }
}, },
); )
.with_preview(|_editor, FileResult { path, line_num, .. }| {
Some((path.clone().into(), Some((*line_num, *line_num))))
})
.with_dynamic_query(get_files, Some(275));
if let Some((reg, line)) = cx
.register
.and_then(|reg| Some((reg, cx.editor.registers.first(reg, cx.editor)?)))
{
picker = picker.with_line(line.into_owned(), cx.editor);
cx.editor.registers.last_search_register = reg;
}
cx.push_layer(Box::new(overlaid(picker)));
} }
enum Extend { enum Extend {

Loading…
Cancel
Save