Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.22" }
nucleo = "0.2.0"
ignore = "0.4"
globset = "0.4.14"

[workspace.package]
version = "23.10.0"
Expand Down
2 changes: 1 addition & 1 deletion helix-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ textwrap = "0.16.1"

nucleo.workspace = true
parking_lot = "0.12"
globset = "0.4.14"
globset.workspace = true

[dev-dependencies]
quickcheck = { version = "1", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion helix-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ helix-parsec = { path = "../helix-parsec" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
globset = "0.4.14"
globset.workspace = true
log = "0.4"
lsp-types = { version = "0.95" }
serde = { version = "1.0", features = ["derive"] }
Expand Down
6 changes: 4 additions & 2 deletions helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ log = "0.4"

# File picker
nucleo.workspace = true
ignore = "0.4"
ignore.workspace = true
# markdown doc rendering
pulldown-cmark = { version = "0.10", default-features = false }
# file type detection
Expand All @@ -71,7 +71,9 @@ serde = { version = "1.0", features = ["derive"] }
grep-regex = "0.1.12"
grep-searcher = "0.1.13"

[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
globset.workspace = true

[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.153"

Expand Down
20 changes: 5 additions & 15 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ use url::Url;

use grep_regex::RegexMatcherBuilder;
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
use ignore::{DirEntry, WalkBuilder, WalkState};
use ignore::{DirEntry, WalkState};

pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;

Expand Down Expand Up @@ -2278,26 +2278,16 @@ fn global_search(cx: &mut Context) {
.canonicalize()
.unwrap_or_else(|_| search_root.clone());
let injector_ = injector.clone();
let mut walk_builder = file_picker_config.walk_builder(search_root);

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)
});
walk_builder.filter_entry(move |entry| {
filter_picker_entry(entry, &absolute_root, dedup_symlinks)
});

walk_builder
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
Expand Down
133 changes: 112 additions & 21 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ use crate::job::Job;

use super::*;

use globset::{Glob, GlobBuilder, GlobSet};
use helix_core::fuzzy::fuzzy_match;
use helix_core::indent::MAX_INDENT;
use helix_core::{encoding, line_ending, shellwords::Shellwords};
use helix_stdx::env::current_working_dir;
use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent};
use serde_json::Value;
use ui::completers::{self, Completer};

// The maximum number of files to open with globbing to avoid freezing while trying to open too many files.
// It is kind of arbitrary and can be raised or offered as a configuration option.
const GLOBBING_MAX_N_FILES: usize = 32;

#[derive(Clone)]
pub struct TypableCommand {
pub name: &'static str,
Expand Down Expand Up @@ -108,30 +114,115 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
}

ensure!(!args.is_empty(), "wrong argument count");

fn open_file(cx: &mut compositor::Context, path: &Path, pos: Position) -> anyhow::Result<()> {
let _ = cx.editor.open(path, Action::Replace)?;
let (view, doc) = current!(cx.editor);
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
doc.set_selection(view.id, pos);
// does not affect opening a buffer without pos
align_view(doc, view, Align::Center);
Ok(())
}

fn glob(path: &Path) -> anyhow::Result<Glob> {
let path_str = path.to_str().context("invalid unicode")?;
GlobBuilder::new(path_str)
.literal_separator(true)
.empty_alternates(true)
.build()
.context("invalid glob")
}

for arg in args {
let (path, pos) = args::parse_file(arg);
let path = helix_stdx::path::expand_tilde(path);
// If the path is a directory, open a file picker on that directory and update the status
// message
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path.into_owned(), &editor.config());
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
let path = helix_stdx::path::canonicalize(path);
if let Ok(metadata) = path.metadata() {
// Path exists
// Shortcut for opening a path without globbing
if metadata.is_dir() {
// If the path is a directory, open a file picker on that directory
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config());
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
};
cx.jobs.callback(callback);
} else {
open_file(cx, &path, pos)?;
}
continue;
}

let mut glob_set_builder = GlobSet::builder();
glob_set_builder.add(glob(&path)?);

let mut root = None;
let mut comps = path.components();

// Iterate over all parents
while comps.next_back().is_some() {
let parent = comps.as_path();
if parent.as_os_str().is_empty() {
// No parents left
break;
}

// Add each parent as a glob for filtering in the recursive walker
glob_set_builder.add(glob(parent)?);

if root.is_none() && parent.exists() {
// Found the first parent that exists
root = Some(parent.to_path_buf());
}
}

let glob_set = glob_set_builder.build().context("invalid glob")?;
let root = root.unwrap_or_else(current_working_dir);

let mut walk = cx
.editor
.config()
.file_picker
.walk_builder(root)
.filter_entry(move |entry| glob_set.is_match(entry.path()))
.build();

// Call `next` to ignore the root directory which is yielded first
if walk.next().is_none() {
continue;
}

let mut to_open = Vec::with_capacity(GLOBBING_MAX_N_FILES);
for entry in walk {
let entry = entry.context("recursive walking failed")?;
let Some(file_type) = entry.file_type() else {
// Entry is stdin
continue;
};
cx.jobs.callback(callback);
} else {
// Otherwise, just open the file
let _ = cx.editor.open(&path, Action::Replace)?;
let (view, doc) = current!(cx.editor);
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
doc.set_selection(view.id, pos);
// does not affect opening a buffer without pos
align_view(doc, view, Align::Center);
// Don't open directories when globbing
if file_type.is_dir() {
continue;
}
if to_open.len() == GLOBBING_MAX_N_FILES {
bail!("tried to open more than {GLOBBING_MAX_N_FILES} files at once");
}
to_open.push(entry.into_path());
}

if to_open.is_empty() {
// Nothing found to open after globbing.
// Open a new file.
open_file(cx, &path, pos)?;
continue;
}

for path in to_open {
open_file(cx, &path, pos)?;
}
}
Ok(())
Expand Down
12 changes: 2 additions & 10 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,25 +173,17 @@ pub fn raw_regex_prompt(
}

pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use ignore::types::TypesBuilder;
use std::time::Instant;

let now = Instant::now();

let dedup_symlinks = config.file_picker.deduplicate_links;
let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone());

let mut walk_builder = WalkBuilder::new(&root);
let mut walk_builder = config.file_picker.walk_builder(&root);
walk_builder
.hidden(config.file_picker.hidden)
.parents(config.file_picker.parents)
.ignore(config.file_picker.ignore)
.follow_links(config.file_picker.follow_symlinks)
.git_ignore(config.file_picker.git_ignore)
.git_global(config.file_picker.git_global)
.git_exclude(config.file_picker.git_exclude)
.sort_by_file_name(|name1, name2| name1.cmp(name2))
.max_depth(config.file_picker.max_depth)
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));

walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
Expand Down
1 change: 1 addition & 0 deletions helix-view/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ log = "~0.4"

parking_lot = "0.12.1"

ignore.workspace = true

[target.'cfg(windows)'.dependencies]
clipboard-win = { version = "5.3", features = ["std"] }
Expand Down
20 changes: 20 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
use helix_lsp::Call;
use ignore::WalkBuilder;
use tokio_stream::wrappers::UnboundedReceiverStream;

use std::{
Expand Down Expand Up @@ -212,6 +213,25 @@ impl Default for FilePickerConfig {
}
}

impl FilePickerConfig {
pub fn walk_builder<P>(&self, path: P) -> WalkBuilder
where
P: AsRef<Path>,
{
let mut builder = WalkBuilder::new(path);
builder
.hidden(self.hidden)
.follow_links(self.follow_symlinks)
.parents(self.parents)
.ignore(self.ignore)
.git_ignore(self.git_ignore)
.git_global(self.git_global)
.git_exclude(self.git_exclude)
.max_depth(self.max_depth);
builder
}
}

fn deserialize_alphabet<'de, D>(deserializer: D) -> Result<Vec<char>, D::Error>
where
D: Deserializer<'de>,
Expand Down