diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index c31c97fa4..2f1db4b6c 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1360,6 +1360,32 @@ mod cmd {
editor.set_status(editor.clipboard_provider.name().into());
}
+ fn change_current_directory(editor: &mut Editor, args: &[&str], _: PromptEvent) {
+ let dir = match args.first() {
+ Some(dir) => dir,
+ None => {
+ editor.set_error("target directory not provided".into());
+ return;
+ }
+ };
+
+ if let Err(e) = std::env::set_current_dir(dir) {
+ editor.set_error(format!(
+ "Couldn't change the current working directory: {:?}",
+ e
+ ));
+ return;
+ }
+
+ match std::env::current_dir() {
+ Ok(cwd) => editor.set_status(format!(
+ "Current working directory is now {}",
+ cwd.display()
+ )),
+ Err(e) => editor.set_error(format!("Couldn't get the new working directory: {}", e)),
+ }
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -1529,6 +1555,13 @@ mod cmd {
fun: show_clipboard_provider,
completer: None,
},
+ TypableCommand {
+ name: "change-current-directory",
+ alias: Some("cd"),
+ doc: "Change the current working directory (:cd
).",
+ fun: change_current_directory,
+ completer: Some(completers::directory),
+ },
];
pub static COMMANDS: Lazy> = Lazy::new(|| {
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index f60152c94..594dabdd7 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -152,8 +152,46 @@ pub mod completers {
names
}
- // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
pub fn filename(input: &str) -> Vec {
+ filename_impl(input, |entry| {
+ let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
+
+ if is_dir {
+ FileMatch::AcceptIncomplete
+ } else {
+ FileMatch::Accept
+ }
+ })
+ }
+
+ pub fn directory(input: &str) -> Vec {
+ filename_impl(input, |entry| {
+ let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
+
+ if is_dir {
+ FileMatch::Accept
+ } else {
+ FileMatch::Reject
+ }
+ })
+ }
+
+ #[derive(Copy, Clone, PartialEq, Eq)]
+ enum FileMatch {
+ /// Entry should be ignored
+ Reject,
+ /// Entry is usable but can't be the end (for instance if the entry is a directory and we
+ /// try to match a file)
+ AcceptIncomplete,
+ /// Entry is usable and can be the end of the match
+ Accept,
+ }
+
+ // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
+ fn filename_impl(input: &str, filter_fn: F) -> Vec
+ where
+ F: Fn(&ignore::DirEntry) -> FileMatch,
+ {
// Rust's filename handling is really annoying.
use ignore::WalkBuilder;
@@ -184,7 +222,13 @@ pub mod completers {
.max_depth(Some(1))
.build()
.filter_map(|file| {
- file.ok().map(|entry| {
+ file.ok().and_then(|entry| {
+ let fmatch = filter_fn(&entry);
+
+ if fmatch == FileMatch::Reject {
+ return None;
+ }
+
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
let path = entry.path();
@@ -199,11 +243,12 @@ pub mod completers {
path.strip_prefix(&dir).unwrap_or(path).to_path_buf()
};
- if is_dir {
+ if fmatch == FileMatch::AcceptIncomplete {
path.push("");
}
+
let path = path.to_str().unwrap().to_owned();
- (end.clone(), Cow::from(path))
+ Some((end.clone(), Cow::from(path)))
})
}) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO