Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add local search in buffer #13053

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/generated/static-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand Down
218 changes: 218 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,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",
Expand Down Expand Up @@ -2646,6 +2647,223 @@ 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,
}

impl FileResult {
fn new(path: &Path, line_num: usize) -> Self {
Self {
path: path.to_path_buf(),
line_num,
}
}
}

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("path", |item: &FileResult, config: &LocalSearchConfig| {
Cell::from(Spans::from(
// only show line numbers in the left picker column
vec![Span::styled(
(item.line_num + 1).to_string(),
config.number_style,
)],
))
}),
PickerColumn::hidden("contents"),
];

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();
}

let documents: Vec<_> = editor
.documents()
.map(|doc| (doc.path().cloned(), doc.text().to_owned()))
.collect();

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;
let sink = sinks::UTF8(|line_num, _line_content| {
stop = injector
.push(FileResult::new(entry.path(), line_num as usize - 1))
.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,
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down