This commit is contained in:
oxcrow 2025-04-01 09:48:49 +02:00 committed by GitHub
commit ac4c7a92b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 245 additions and 0 deletions

View file

@ -80,6 +80,7 @@
| `search_selection_detect_word_boundaries` | Use current selection as the search pattern, automatically wrapping with `\b` on word boundaries | normal: `` * ``, select: `` * `` |
| `make_search_word_bounded` | Modify current search to make it word bounded | |
| `global_search` | Global search in workspace folder | normal: `` <space>/ ``, select: `` <space>/ `` |
| `local_search` | Local search in buffer | normal: `` <space>l ``, select: `` <space>l `` |
| `extend_line` | Select current line, if already selected, extend to another line based on the anchor | |
| `extend_line_below` | Select current line, if already selected, extend to next line | normal: `` x ``, select: `` x `` |
| `extend_line_above` | Select current line, if already selected, extend to previous line | |

View file

@ -378,6 +378,7 @@ impl MappableCommand {
search_selection_detect_word_boundaries, "Use current selection as the search pattern, automatically wrapping with `\\b` on word boundaries",
make_search_word_bounded, "Modify current search to make it word bounded",
global_search, "Global search in workspace folder",
local_search, "Local search in buffer",
extend_line, "Select current line, if already selected, extend to another line based on the anchor",
extend_line_below, "Select current line, if already selected, extend to next line",
extend_line_above, "Select current line, if already selected, extend to previous line",
@ -2645,6 +2646,248 @@ fn global_search(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(picker)));
}
/// Local search in buffer
fn local_search(cx: &mut Context) {
#[derive(Debug)]
struct FileResult {
path: PathBuf,
line_num: usize,
line_content: String,
}
impl FileResult {
fn new(path: &Path, line_num: usize, line_content: String) -> Self {
Self {
path: path.to_path_buf(),
line_num,
line_content,
}
}
}
struct LocalSearchConfig {
smart_case: bool,
file_picker_config: helix_view::editor::FilePickerConfig,
number_style: Style,
}
let editor_config = cx.editor.config();
let config = LocalSearchConfig {
smart_case: editor_config.search.smart_case,
file_picker_config: editor_config.file_picker.clone(),
number_style: cx.editor.theme.get("constant.numeric.integer"),
};
let columns = [
PickerColumn::new("line", |item: &FileResult, config: &LocalSearchConfig| {
let line_num = (item.line_num + 1).to_string();
// files can never contain more than 99_999_999 lines
// thus using maximum line length to be 8 for this formatter is valid
let max_line_num_length = 8;
// whitespace padding to align results after the line number
let padding_length = max_line_num_length - line_num.len();
let padding = " ".repeat(padding_length);
// create column value to be displayed in the picker
Cell::from(Spans::from(vec![
Span::styled(line_num, config.number_style),
Span::raw(padding),
]))
}),
PickerColumn::new("", |item: &FileResult, _config: &LocalSearchConfig| {
// extract line content to be displayed in the picker
let line_content = item.line_content.clone();
// create column value to be displayed in the picker
Cell::from(Spans::from(vec![Span::raw(line_content)]))
}),
];
let get_files = |query: &str,
editor: &mut Editor,
config: std::sync::Arc<LocalSearchConfig>,
injector: &ui::picker::Injector<_, _>| {
if query.is_empty() {
return async { Ok(()) }.boxed();
}
let search_root = helix_stdx::env::current_working_dir();
if !search_root.exists() {
return async { Err(anyhow::anyhow!("Current working directory does not exist")) }
.boxed();
}
// Only read the current document (not other documents opened in the buffer)
let doc = doc!(editor);
let documents = vec![(doc.path().cloned(), doc.text().to_owned())];
let matcher = match RegexMatcherBuilder::new()
.case_smart(config.smart_case)
.build(query)
{
Ok(matcher) => {
// Clear any "Failed to compile regex" errors out of the statusline.
editor.clear_status();
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;
// Maximum line length of the content displayed within the result picker.
// User should be allowed to control this to accomodate their monitor width.
// TODO: Expose this setting to the user so they can control it.
let local_search_result_line_length = 80;
let sink = sinks::UTF8(|line_num, line_content| {
stop = injector
.push(FileResult::new(
entry.path(),
line_num as usize - 1,
line_content[0..std::cmp::min(
local_search_result_line_length,
line_content.len(),
)]
.to_string(),
))
.is_err();
Ok(!stop)
});
let doc = documents.iter().find(|&(doc_path, _)| {
doc_path
.as_ref()
.is_some_and(|doc_path| doc_path == entry.path())
});
// search in current document
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 continuous 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 {
// Note: This is a hack!
// We ignore all other files.
// We only search an empty string (to satisfy rust's return type).
searcher.search_slice(&matcher, "".to_owned().as_bytes(), sink)
};
if let Err(err) = result {
log::error!("Local search error: {}, {}", entry.path().display(), err);
}
if stop {
WalkState::Quit
} else {
WalkState::Continue
}
})
});
Ok(())
}
.boxed()
};
let reg = cx.register.unwrap_or('/');
cx.editor.registers.last_search_register = reg;
let picker = Picker::new(
columns,
1, // contents
[],
config,
move |cx, FileResult { path, line_num, .. }, action| {
let doc = match cx.editor.open(path, action) {
Ok(id) => doc_mut!(cx.editor, &id),
Err(e) => {
cx.editor
.set_error(format!("Failed to open file '{}': {}", path.display(), e));
return;
}
};
let line_num = *line_num;
let view = view_mut!(cx.editor);
let text = doc.text();
if line_num >= text.len_lines() {
cx.editor.set_error(
"The line you jumped to does not exist anymore because the file has changed.",
);
return;
}
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));
if action.align_view(view, doc.id()) {
align_view(doc, view, Align::Center);
}
},
)
.with_preview(|_editor, FileResult { path, line_num, .. }| {
Some((path.as_path().into(), Some((*line_num, *line_num))))
})
.with_history_register(Some(reg))
.with_dynamic_query(get_files, Some(275));
cx.push_layer(Box::new(overlaid(picker)));
}
enum Extend {
Above,
Below,

View file

@ -282,6 +282,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"P" => paste_clipboard_before,
"R" => replace_selections_with_clipboard,
"/" => global_search,
"l" => local_search,
"k" => hover,
"r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor,