- Renamed local_search to local_search_grep for clarity

- Added fuzzy searching to local buffer search
pull/13053/head^2
Ivan 2025-04-03 18:23:20 +02:00
parent 42a70b2f35
commit ccbaadda45
3 changed files with 156 additions and 5 deletions

View File

@ -80,7 +80,8 @@
| `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 `` |
| `local_search_grep` | Local search in buffer | normal: `` <space>l ``, select: `` <space>l `` |
| `local_search_fuzzy` | Fuzzy 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

@ -379,7 +379,8 @@ 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",
local_search_grep, "Local search in buffer",
local_search_fuzzy, "Fuzzy 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",
@ -2647,8 +2648,8 @@ fn global_search(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(picker)));
}
/// Local search in buffer
fn local_search(cx: &mut Context) {
/// Local grep search in buffer
fn local_search_grep(cx: &mut Context) {
#[derive(Debug)]
struct FileResult {
path: PathBuf,
@ -2889,6 +2890,154 @@ fn local_search(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(picker)));
}
fn local_search_fuzzy(cx: &mut Context) {
#[derive(Debug)]
struct FileResult {
path: std::sync::Arc<PathBuf>,
line_num: usize,
file_contents_byte_start: usize,
file_contents_byte_end: usize,
}
struct LocalSearchData {
file_contents: String,
number_style: Style,
}
let current_document = doc!(cx.editor);
let Some(current_document_path) = current_document.path() else {
cx.editor.set_error("Failed to get current document path");
return;
};
let file_contents = std::fs::read_to_string(current_document_path).unwrap();
let current_document_path = std::sync::Arc::new(current_document_path.clone());
let file_results: Vec<FileResult> = file_contents
.lines()
.enumerate()
.filter_map(|(line_num, line)| {
if !line.trim().is_empty() {
// SAFETY: The offsets will be used to index back into the original `file_contents` String
// as a byte slice. Since the `file_contents` will be moved into the `Picker` as part of
// `editor_data`, we know that the `Picker` will take ownership of the underlying String,
// so it will be valid for displaying the `Span` as long as the user uses the `Picker`
// (the `Picker` gets dropped only when a new `Picker` is created). Furthermore, the
// process of reconstructing a `&str` back requires that we have access to the original
// `String` anyways so we can index into it, as is the case when we construct the `Span`
// when creating the `PickerColumn`s, so we know that we are returning the correct
// substring from the original `file_contents`.
// In fact, since we only store offsets, and accessing them from safe rust, there is
// no risk of memory safety (like our &str not living long enough). The only real
// bug would be moving out the original underlying `String` (which we obviously
// don't do). This would lead to an out of bounds crash in the `PickerColumn` function
// call, or a crash when we recreate back the &str if the new underlying `String`
// makes it so that our byte offsets index into the middle of a Unicode grapheme cluster.
// Last but not least, it could make it so that we do display the lines correctly,
// but these are from a different underlying `String` than the original, which would be
// different from the lines in the current buffer.
let beg =
unsafe { line.as_ptr().byte_offset_from(file_contents.as_ptr()) } as usize;
let end = beg + line.len();
let result = FileResult {
path: current_document_path.clone(),
line_num,
file_contents_byte_start: beg,
file_contents_byte_end: end,
};
Some(result)
} else {
None
}
})
.collect();
let config = LocalSearchData {
number_style: cx.editor.theme.get("constant.numeric.integer"),
file_contents,
};
let columns = [
PickerColumn::new("line", |item: &FileResult, config: &LocalSearchData| {
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: &LocalSearchData| {
// extract line content to be displayed in the picker
let slice = &config.file_contents.as_bytes()
[item.file_contents_byte_start..item.file_contents_byte_end];
let content = std::str::from_utf8(slice).unwrap();
// create column value to be displayed in the picker
Cell::from(Spans::from(vec![Span::raw(content)]))
}),
];
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));
let injector = picker.injector();
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
for file_result in file_results {
if injector.push(file_result).is_err() {
break;
}
if std::time::Instant::now() >= timeout {
break;
}
}
cx.push_layer(Box::new(overlaid(picker)));
}
enum Extend {
Above,
Below,

View File

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