mirror of
https://github.com/Llewellynvdm/zoxide.git
synced 2025-01-24 15:48:24 +00:00
Remove deleted entries if not accessed in the last 90 days (#213)
This commit is contained in:
parent
98f1d7277a
commit
22a9f6c32d
64
Cargo.lock
generated
64
Cargo.lock
generated
@ -335,12 +335,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.2.0"
|
||||
@ -356,6 +350,15 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
|
||||
dependencies = [
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.10"
|
||||
@ -554,6 +557,28 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
@ -561,10 +586,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "seq-macro"
|
||||
version = "0.2.2"
|
||||
name = "semver"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
|
||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||
dependencies = [
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@ -653,6 +690,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.7.1"
|
||||
@ -754,10 +797,9 @@ dependencies = [
|
||||
"dirs-next",
|
||||
"dunce",
|
||||
"glob",
|
||||
"once_cell",
|
||||
"ordered-float",
|
||||
"rand 0.7.3",
|
||||
"seq-macro",
|
||||
"rstest",
|
||||
"serde",
|
||||
"tempfile",
|
||||
]
|
||||
|
@ -26,8 +26,7 @@ rand = "0.7.3"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "1.0.1"
|
||||
once_cell = "1.4.1"
|
||||
seq-macro = "0.2.1"
|
||||
rstest = "0.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
clap = "3.0.0-beta.2"
|
||||
|
@ -8,6 +8,7 @@ zoxide is a smarter replacement for your cd command. It keeps track of the
|
||||
directories you use most frequently, and uses a ranking algorithm to navigate
|
||||
to the best match.
|
||||
.SH USAGE
|
||||
.nf
|
||||
\fBz\fR \fIfoo\fR # cd into highest ranked directory matching foo
|
||||
\fBz\fR \fIfoo bar\fR # cd into highest ranked directory matching foo and bar
|
||||
.sp
|
||||
@ -17,6 +18,7 @@ to the best match.
|
||||
\fBz\fR \fI-\fR # cd into previous directory
|
||||
.sp
|
||||
\fBzi\fR \fIfoo\fR # cd with interactive selection (using fzf)
|
||||
.fi
|
||||
.SH SUBCOMMANDS
|
||||
.TP
|
||||
\fBzoxide-add\fR(1)
|
||||
|
7
rustfmt.toml
Normal file
7
rustfmt.toml
Normal file
@ -0,0 +1,7 @@
|
||||
# group_imports = "StdExternalCrate"
|
||||
# imports_granularity = "Module"
|
||||
newline_style = "Native"
|
||||
use_field_init_shorthand = true
|
||||
use_small_heuristics = "Max"
|
||||
use_try_shorthand = true
|
||||
# wrap_comments = true
|
@ -116,7 +116,7 @@ pub struct Query {
|
||||
pub score: bool,
|
||||
|
||||
/// Exclude a path from results
|
||||
#[clap(long, hidden = true)]
|
||||
#[clap(long, value_name = "path")]
|
||||
pub exclude: Option<String>,
|
||||
}
|
||||
|
||||
@ -126,9 +126,6 @@ pub struct Remove {
|
||||
// Use interactive selection
|
||||
#[clap(conflicts_with = "path", long, short, value_name = "keywords")]
|
||||
pub interactive: Option<Vec<String>>,
|
||||
#[clap(
|
||||
conflicts_with = "interactive",
|
||||
required_unless_present = "interactive"
|
||||
)]
|
||||
#[clap(conflicts_with = "interactive", required_unless_present = "interactive")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
@ -13,10 +13,7 @@ impl Run for Add {
|
||||
util::resolve_path(&self.path)
|
||||
}?;
|
||||
|
||||
if config::zo_exclude_dirs()?
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches_path(&path))
|
||||
{
|
||||
if config::zo_exclude_dirs()?.iter().any(|pattern| pattern.matches_path(&path)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -31,11 +31,8 @@ fn from_autojump<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
|
||||
let buffer = fs::read_to_string(path)
|
||||
.with_context(|| format!("could not open autojump database: {}", path.display()))?;
|
||||
|
||||
let mut dirs = db
|
||||
.dirs
|
||||
.iter()
|
||||
.map(|dir| (dir.path.as_ref(), dir.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let mut dirs =
|
||||
db.dirs.iter().map(|dir| (dir.path.as_ref(), dir.clone())).collect::<HashMap<_, _>>();
|
||||
|
||||
for line in buffer.lines() {
|
||||
if line.is_empty() {
|
||||
@ -43,28 +40,20 @@ fn from_autojump<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
|
||||
}
|
||||
let mut split = line.splitn(2, '\t');
|
||||
|
||||
let rank = split
|
||||
.next()
|
||||
.with_context(|| format!("invalid entry: {}", line))?;
|
||||
let mut rank = rank
|
||||
.parse::<f64>()
|
||||
.with_context(|| format!("invalid rank: {}", rank))?;
|
||||
let rank = split.next().with_context(|| format!("invalid entry: {}", line))?;
|
||||
let mut rank = rank.parse::<f64>().with_context(|| format!("invalid rank: {}", rank))?;
|
||||
// Normalize the rank using a sigmoid function. Don't import actual
|
||||
// ranks from autojump, since its scoring algorithm is very different,
|
||||
// and might take a while to get normalized.
|
||||
rank = 1.0 / (1.0 + (-rank).exp());
|
||||
|
||||
let path = split
|
||||
.next()
|
||||
.with_context(|| format!("invalid entry: {}", line))?;
|
||||
let path = split.next().with_context(|| format!("invalid entry: {}", line))?;
|
||||
|
||||
dirs.entry(path)
|
||||
.and_modify(|dir| dir.rank += rank)
|
||||
.or_insert_with(|| Dir {
|
||||
path: path.to_string().into(),
|
||||
rank,
|
||||
last_accessed: 0,
|
||||
});
|
||||
dirs.entry(path).and_modify(|dir| dir.rank += rank).or_insert_with(|| Dir {
|
||||
path: path.to_string().into(),
|
||||
rank,
|
||||
last_accessed: 0,
|
||||
});
|
||||
}
|
||||
|
||||
db.dirs = DirList(dirs.into_iter().map(|(_, dir)| dir).collect());
|
||||
@ -78,11 +67,8 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
|
||||
let buffer = fs::read_to_string(path)
|
||||
.with_context(|| format!("could not open z database: {}", path.display()))?;
|
||||
|
||||
let mut dirs = db
|
||||
.dirs
|
||||
.iter()
|
||||
.map(|dir| (dir.path.as_ref(), dir.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let mut dirs =
|
||||
db.dirs.iter().map(|dir| (dir.path.as_ref(), dir.clone())).collect::<HashMap<_, _>>();
|
||||
|
||||
for line in buffer.lines() {
|
||||
if line.is_empty() {
|
||||
@ -90,23 +76,14 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
|
||||
}
|
||||
let mut split = line.rsplitn(3, '|');
|
||||
|
||||
let last_accessed = split
|
||||
.next()
|
||||
.with_context(|| format!("invalid entry: {}", line))?;
|
||||
let last_accessed = last_accessed
|
||||
.parse()
|
||||
.with_context(|| format!("invalid epoch: {}", last_accessed))?;
|
||||
let last_accessed = split.next().with_context(|| format!("invalid entry: {}", line))?;
|
||||
let last_accessed =
|
||||
last_accessed.parse().with_context(|| format!("invalid epoch: {}", last_accessed))?;
|
||||
|
||||
let rank = split
|
||||
.next()
|
||||
.with_context(|| format!("invalid entry: {}", line))?;
|
||||
let rank = rank
|
||||
.parse()
|
||||
.with_context(|| format!("invalid rank: {}", rank))?;
|
||||
let rank = split.next().with_context(|| format!("invalid entry: {}", line))?;
|
||||
let rank = rank.parse().with_context(|| format!("invalid rank: {}", rank))?;
|
||||
|
||||
let path = split
|
||||
.next()
|
||||
.with_context(|| format!("invalid entry: {}", line))?;
|
||||
let path = split.next().with_context(|| format!("invalid entry: {}", line))?;
|
||||
|
||||
dirs.entry(path)
|
||||
.and_modify(|dir| {
|
||||
@ -115,11 +92,7 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
|
||||
dir.last_accessed = last_accessed;
|
||||
}
|
||||
})
|
||||
.or_insert(Dir {
|
||||
path: path.to_string().into(),
|
||||
rank,
|
||||
last_accessed,
|
||||
});
|
||||
.or_insert(Dir { path: path.to_string().into(), rank, last_accessed });
|
||||
}
|
||||
|
||||
db.dirs = DirList(dirs.into_iter().map(|(_, dir)| dir).collect());
|
||||
|
@ -10,21 +10,12 @@ use std::io::{self, Write};
|
||||
|
||||
impl Run for Init {
|
||||
fn run(&self) -> Result<()> {
|
||||
let cmd = if self.no_aliases {
|
||||
None
|
||||
} else {
|
||||
Some(self.cmd.as_str())
|
||||
};
|
||||
let cmd = if self.no_aliases { None } else { Some(self.cmd.as_str()) };
|
||||
|
||||
let echo = config::zo_echo();
|
||||
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||
|
||||
let opts = &Opts {
|
||||
cmd,
|
||||
hook: self.hook,
|
||||
echo,
|
||||
resolve_symlinks,
|
||||
};
|
||||
let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks };
|
||||
|
||||
let source = match self.shell {
|
||||
InitShell::Bash => shell::Bash(opts).render(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::app::{Query, Run};
|
||||
use crate::config;
|
||||
use crate::db::{DatabaseFile, Matcher};
|
||||
use crate::db::DatabaseFile;
|
||||
use crate::error::BrokenPipeHandler;
|
||||
use crate::fzf::Fzf;
|
||||
use crate::util;
|
||||
@ -16,19 +16,18 @@ impl Run for Query {
|
||||
let mut db = db.open()?;
|
||||
let now = util::current_time()?;
|
||||
|
||||
let mut matcher = Matcher::new().with_keywords(&self.keywords);
|
||||
let mut stream = db.stream(now).with_keywords(&self.keywords);
|
||||
if !self.all {
|
||||
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||
matcher = matcher.with_exists(resolve_symlinks);
|
||||
stream = stream.with_exists(resolve_symlinks);
|
||||
}
|
||||
if let Some(path) = &self.exclude {
|
||||
stream = stream.with_exclude(path);
|
||||
}
|
||||
|
||||
let mut matches = db
|
||||
.iter(&matcher, now)
|
||||
.filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref());
|
||||
|
||||
if self.interactive {
|
||||
let mut fzf = Fzf::new(false)?;
|
||||
for dir in matches {
|
||||
while let Some(dir) = stream.next() {
|
||||
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
|
||||
}
|
||||
|
||||
@ -36,9 +35,7 @@ impl Run for Query {
|
||||
if self.score {
|
||||
print!("{}", selection);
|
||||
} else {
|
||||
let path = selection
|
||||
.get(5..)
|
||||
.context("could not read selection from fzf")?;
|
||||
let path = selection.get(5..).context("could not read selection from fzf")?;
|
||||
print!("{}", path)
|
||||
}
|
||||
} else if self.list {
|
||||
@ -49,7 +46,7 @@ impl Run for Query {
|
||||
let stdout = stdout.lock();
|
||||
let mut handle = BufWriter::new(stdout);
|
||||
|
||||
for dir in matches {
|
||||
while let Some(dir) = stream.next() {
|
||||
if self.score {
|
||||
writeln!(handle, "{}", dir.display_score(now))
|
||||
} else {
|
||||
@ -59,7 +56,7 @@ impl Run for Query {
|
||||
}
|
||||
handle.flush().pipe_exit("stdout")?;
|
||||
} else {
|
||||
let dir = matches.next().context("no match found")?;
|
||||
let dir = stream.next().context("no match found")?;
|
||||
if self.score {
|
||||
writeln!(io::stdout(), "{}", dir.display_score(now))
|
||||
} else {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::app::{Remove, Run};
|
||||
use crate::config;
|
||||
use crate::db::{DatabaseFile, Matcher};
|
||||
use crate::db::DatabaseFile;
|
||||
use crate::error::BrokenPipeHandler;
|
||||
use crate::fzf::Fzf;
|
||||
use crate::util;
|
||||
@ -18,11 +18,11 @@ impl Run for Remove {
|
||||
let selection;
|
||||
match &self.interactive {
|
||||
Some(keywords) => {
|
||||
let matcher = Matcher::new().with_keywords(keywords);
|
||||
let now = util::current_time()?;
|
||||
let mut stream = db.stream(now).with_keywords(keywords);
|
||||
|
||||
let mut fzf = Fzf::new(true)?;
|
||||
for dir in db.iter(&matcher, now) {
|
||||
while let Some(dir) = stream.next() {
|
||||
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,7 @@ pub fn zo_exclude_dirs() -> Result<Vec<Pattern>> {
|
||||
match env::var_os("_ZO_EXCLUDE_DIRS") {
|
||||
Some(dirs_osstr) => env::split_paths(&dirs_osstr)
|
||||
.map(|path| {
|
||||
let pattern = path
|
||||
.to_str()
|
||||
.context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
|
||||
let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
|
||||
Pattern::new(&pattern)
|
||||
.with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {}", pattern))
|
||||
})
|
||||
@ -61,9 +59,7 @@ pub fn zo_fzf_opts() -> Option<OsString> {
|
||||
pub fn zo_maxage() -> Result<Rank> {
|
||||
match env::var_os("_ZO_MAXAGE") {
|
||||
Some(maxage_osstr) => {
|
||||
let maxage_str = maxage_osstr
|
||||
.to_str()
|
||||
.context("invalid unicode in _ZO_MAXAGE")?;
|
||||
let maxage_str = maxage_osstr.to_str().context("invalid unicode in _ZO_MAXAGE")?;
|
||||
let maxage = maxage_str.parse::<u64>().with_context(|| {
|
||||
format!("unable to parse _ZO_MAXAGE as integer: {}", maxage_str)
|
||||
})?;
|
||||
|
@ -20,9 +20,7 @@ impl DirList<'_> {
|
||||
// Assume a maximum size for the database. This prevents bincode from
|
||||
// throwing strange errors when it encounters invalid data.
|
||||
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
|
||||
let deserializer = &mut bincode::options()
|
||||
.with_fixint_encoding()
|
||||
.with_limit(MAX_SIZE);
|
||||
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
|
||||
|
||||
// Split bytes into sections.
|
||||
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
|
||||
@ -36,11 +34,9 @@ impl DirList<'_> {
|
||||
let version = deserializer.deserialize(bytes_version)?;
|
||||
match version {
|
||||
Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?),
|
||||
version => bail!(
|
||||
"unsupported version (got {}, supports {})",
|
||||
version,
|
||||
Self::VERSION,
|
||||
),
|
||||
version => {
|
||||
bail!("unsupported version (got {}, supports {})", version, Self::VERSION,)
|
||||
}
|
||||
}
|
||||
})()
|
||||
.context("could not deserialize database")
|
||||
@ -159,11 +155,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn zero_copy() {
|
||||
let dirs = DirList(vec![Dir {
|
||||
path: "/".into(),
|
||||
rank: 0.0,
|
||||
last_accessed: 0,
|
||||
}]);
|
||||
let dirs = DirList(vec![Dir { path: "/".into(), rank: 0.0, last_accessed: 0 }]);
|
||||
|
||||
let bytes = dirs.to_bytes().unwrap();
|
||||
let dirs = DirList::from_bytes(&bytes).unwrap();
|
||||
|
@ -1,26 +1,24 @@
|
||||
mod dir;
|
||||
mod query;
|
||||
mod stream;
|
||||
|
||||
pub use dir::{Dir, DirList, Epoch, Rank};
|
||||
pub use query::Matcher;
|
||||
pub use stream::Stream;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use ordered_float::OrderedFloat;
|
||||
use tempfile::{NamedTempFile, PersistError};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Reverse;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub struct Database<'a> {
|
||||
pub dirs: DirList<'a>,
|
||||
pub struct Database<'file> {
|
||||
pub dirs: DirList<'file>,
|
||||
pub modified: bool,
|
||||
data_dir: &'a Path,
|
||||
data_dir: &'file PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> Database<'a> {
|
||||
impl<'file> Database<'file> {
|
||||
pub fn save(&mut self) -> Result<()> {
|
||||
if !self.modified {
|
||||
return Ok(());
|
||||
@ -28,10 +26,7 @@ impl<'a> Database<'a> {
|
||||
|
||||
let buffer = self.dirs.to_bytes()?;
|
||||
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
|
||||
format!(
|
||||
"could not create temporary database in: {}",
|
||||
self.data_dir.display()
|
||||
)
|
||||
format!("could not create temporary database in: {}", self.data_dir.display())
|
||||
})?;
|
||||
|
||||
// Preallocate enough space on the file, preventing copying later on.
|
||||
@ -39,10 +34,7 @@ impl<'a> Database<'a> {
|
||||
// ignore it and proceed.
|
||||
let _ = file.as_file().set_len(buffer.len() as _);
|
||||
file.write_all(&buffer).with_context(|| {
|
||||
format!(
|
||||
"could not write to temporary database: {}",
|
||||
file.path().display()
|
||||
)
|
||||
format!("could not write to temporary database: {}", file.path().display())
|
||||
})?;
|
||||
|
||||
let path = db_path(&self.data_dir);
|
||||
@ -58,11 +50,9 @@ impl<'a> Database<'a> {
|
||||
let path = path.as_ref();
|
||||
|
||||
match self.dirs.iter_mut().find(|dir| dir.path == path) {
|
||||
None => self.dirs.push(Dir {
|
||||
path: Cow::Owned(path.into()),
|
||||
last_accessed: now,
|
||||
rank: 1.0,
|
||||
}),
|
||||
None => {
|
||||
self.dirs.push(Dir { path: Cow::Owned(path.into()), last_accessed: now, rank: 1.0 })
|
||||
}
|
||||
Some(dir) => {
|
||||
dir.last_accessed = now;
|
||||
dir.rank += 1.0;
|
||||
@ -72,12 +62,9 @@ impl<'a> Database<'a> {
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator<Item = &'i Dir> {
|
||||
self.dirs
|
||||
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
|
||||
self.dirs
|
||||
.iter()
|
||||
.filter(move |dir| m.matches(dir.path.as_ref()))
|
||||
// Streaming iterator for directories.
|
||||
pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> {
|
||||
Stream::new(self, now)
|
||||
}
|
||||
|
||||
/// Removes the directory with `path` from the store.
|
||||
@ -159,16 +146,13 @@ fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistEr
|
||||
}
|
||||
|
||||
pub struct DatabaseFile {
|
||||
data_dir: PathBuf,
|
||||
buffer: Vec<u8>,
|
||||
data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DatabaseFile {
|
||||
pub fn new<P: Into<PathBuf>>(data_dir: P) -> DatabaseFile {
|
||||
DatabaseFile {
|
||||
data_dir: data_dir.into(),
|
||||
buffer: Vec::new(),
|
||||
}
|
||||
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
|
||||
DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() }
|
||||
}
|
||||
|
||||
pub fn open(&mut self) -> Result<Database> {
|
||||
@ -182,27 +166,16 @@ impl DatabaseFile {
|
||||
let dirs = DirList::from_bytes(&self.buffer).with_context(|| {
|
||||
format!("could not deserialize database: {}", path.display())
|
||||
})?;
|
||||
Ok(Database {
|
||||
dirs,
|
||||
modified: false,
|
||||
data_dir: &self.data_dir,
|
||||
})
|
||||
Ok(Database { dirs, modified: false, data_dir: &self.data_dir })
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
// Create data directory, but don't create any file yet.
|
||||
// The file will be created later by [`Database::save`]
|
||||
// if any data is modified.
|
||||
fs::create_dir_all(&self.data_dir).with_context(|| {
|
||||
format!(
|
||||
"unable to create data directory: {}",
|
||||
self.data_dir.display()
|
||||
)
|
||||
format!("unable to create data directory: {}", self.data_dir.display())
|
||||
})?;
|
||||
Ok(Database {
|
||||
dirs: DirList::new(),
|
||||
modified: false,
|
||||
data_dir: &self.data_dir,
|
||||
})
|
||||
Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir })
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
|
||||
@ -222,11 +195,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn add() {
|
||||
let path = if cfg!(windows) {
|
||||
r"C:\foo\bar"
|
||||
} else {
|
||||
"/foo/bar"
|
||||
};
|
||||
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
|
||||
let now = 946684800;
|
||||
|
||||
let data_dir = tempfile::tempdir().unwrap();
|
||||
@ -249,11 +218,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn remove() {
|
||||
let path = if cfg!(windows) {
|
||||
r"C:\foo\bar"
|
||||
} else {
|
||||
"/foo/bar"
|
||||
};
|
||||
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
|
||||
let now = 946684800;
|
||||
|
||||
let data_dir = tempfile::tempdir().unwrap();
|
||||
|
111
src/db/query.rs
111
src/db/query.rs
@ -1,111 +0,0 @@
|
||||
use crate::util;
|
||||
|
||||
use std::fs;
|
||||
use std::path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Matcher {
|
||||
keywords: Vec<String>,
|
||||
check_exists: bool,
|
||||
resolve_symlinks: bool,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
pub fn new() -> Matcher {
|
||||
Matcher::default()
|
||||
}
|
||||
|
||||
pub fn with_exists(mut self, resolve_symlinks: bool) -> Matcher {
|
||||
self.check_exists = true;
|
||||
self.resolve_symlinks = resolve_symlinks;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Matcher {
|
||||
self.keywords = keywords.iter().map(util::to_lowercase).collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn matches<S: AsRef<str>>(&self, path: S) -> bool {
|
||||
self.matches_keywords(&path) && self.matches_exists(path)
|
||||
}
|
||||
|
||||
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool {
|
||||
if !self.check_exists {
|
||||
return true;
|
||||
}
|
||||
|
||||
let resolver = if self.resolve_symlinks {
|
||||
fs::symlink_metadata
|
||||
} else {
|
||||
fs::metadata
|
||||
};
|
||||
|
||||
resolver(path.as_ref())
|
||||
.map(|m| m.is_dir())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool {
|
||||
let (keywords_last, keywords) = match self.keywords.split_last() {
|
||||
Some(split) => split,
|
||||
None => return true,
|
||||
};
|
||||
|
||||
let path = util::to_lowercase(path);
|
||||
let mut path = path.as_str();
|
||||
match path.rfind(keywords_last) {
|
||||
Some(idx) => {
|
||||
if path[idx + keywords_last.len()..].contains(path::is_separator) {
|
||||
return false;
|
||||
}
|
||||
path = &path[..idx];
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
|
||||
for keyword in keywords.iter().rev() {
|
||||
match path.rfind(keyword) {
|
||||
Some(idx) => path = &path[..idx],
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Matcher;
|
||||
|
||||
#[test]
|
||||
fn query() {
|
||||
const CASES: &[(&[&str], &str, bool)] = &[
|
||||
// Case normalization
|
||||
(&["fOo", "bAr"], "/foo/bar", true),
|
||||
// Last component
|
||||
(&["ba"], "/foo/bar", true),
|
||||
(&["fo"], "/foo/bar", false),
|
||||
// Slash as suffix
|
||||
(&["foo/"], "/foo", false),
|
||||
(&["foo/"], "/foo/bar", true),
|
||||
(&["foo/"], "/foo/bar/baz", false),
|
||||
(&["foo", "/"], "/foo", false),
|
||||
(&["foo", "/"], "/foo/bar", true),
|
||||
(&["foo", "/"], "/foo/bar/baz", true),
|
||||
// Split components
|
||||
(&["/", "fo", "/", "ar"], "/foo/bar", true),
|
||||
(&["oo/ba"], "/foo/bar", true),
|
||||
// Overlap
|
||||
(&["foo", "o", "bar"], "/foo/bar", false),
|
||||
(&["/foo/", "/bar"], "/foo/bar", false),
|
||||
(&["/foo/", "/bar"], "/foo/baz/bar", true),
|
||||
];
|
||||
|
||||
for &(keywords, path, is_match) in CASES {
|
||||
let matcher = Matcher::new().with_keywords(keywords);
|
||||
assert_eq!(is_match, matcher.matches(path))
|
||||
}
|
||||
}
|
||||
}
|
161
src/db/stream.rs
Normal file
161
src/db/stream.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use super::{Database, Dir, Epoch};
|
||||
use crate::util;
|
||||
|
||||
use ordered_float::OrderedFloat;
|
||||
|
||||
use std::fs;
|
||||
use std::iter::Rev;
|
||||
use std::ops::Range;
|
||||
use std::path;
|
||||
|
||||
pub struct Stream<'db, 'file> {
|
||||
db: &'db mut Database<'file>,
|
||||
idxs: Rev<Range<usize>>,
|
||||
|
||||
keywords: Vec<String>,
|
||||
|
||||
check_exists: bool,
|
||||
expire_below: Epoch,
|
||||
resolve_symlinks: bool,
|
||||
|
||||
exclude_path: Option<String>,
|
||||
}
|
||||
|
||||
impl<'db, 'file> Stream<'db, 'file> {
|
||||
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self {
|
||||
// Iterate in descending order of score.
|
||||
db.dirs.sort_unstable_by_key(|dir| OrderedFloat(dir.score(now)));
|
||||
let idxs = (0..db.dirs.len()).rev();
|
||||
|
||||
// If a directory is deleted and hasn't been used for 90 days, delete
|
||||
// it from the database.
|
||||
let expire_below = now.saturating_sub(90 * 24 * 60 * 60);
|
||||
|
||||
Stream {
|
||||
db,
|
||||
idxs,
|
||||
keywords: Vec::new(),
|
||||
check_exists: false,
|
||||
expire_below,
|
||||
resolve_symlinks: false,
|
||||
exclude_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_exclude<S: Into<String>>(mut self, path: S) -> Self {
|
||||
self.exclude_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_exists(mut self, resolve_symlinks: bool) -> Self {
|
||||
self.check_exists = true;
|
||||
self.resolve_symlinks = resolve_symlinks;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Self {
|
||||
self.keywords = keywords.iter().map(util::to_lowercase).collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<&Dir<'file>> {
|
||||
while let Some(idx) = self.idxs.next() {
|
||||
let dir = &self.db.dirs[idx];
|
||||
|
||||
if !self.matches_keywords(&dir.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !self.matches_exists(&dir.path) {
|
||||
if dir.last_accessed < self.expire_below {
|
||||
self.db.dirs.swap_remove(idx);
|
||||
self.db.modified = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if Some(dir.path.as_ref()) == self.exclude_path.as_deref() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dir = &self.db.dirs[idx];
|
||||
return Some(dir);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool {
|
||||
if !self.check_exists {
|
||||
return true;
|
||||
}
|
||||
|
||||
let resolver = if self.resolve_symlinks { fs::symlink_metadata } else { fs::metadata };
|
||||
|
||||
resolver(path.as_ref()).map(|m| m.is_dir()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool {
|
||||
let (keywords_last, keywords) = match self.keywords.split_last() {
|
||||
Some(split) => split,
|
||||
None => return true,
|
||||
};
|
||||
|
||||
let path = util::to_lowercase(path);
|
||||
let mut path = path.as_str();
|
||||
match path.rfind(keywords_last) {
|
||||
Some(idx) => {
|
||||
if path[idx + keywords_last.len()..].contains(path::is_separator) {
|
||||
return false;
|
||||
}
|
||||
path = &path[..idx];
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
|
||||
for keyword in keywords.iter().rev() {
|
||||
match path.rfind(keyword) {
|
||||
Some(idx) => path = &path[..idx],
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Database;
|
||||
|
||||
use rstest::rstest;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[rstest]
|
||||
// Case normalization
|
||||
#[case(&["fOo", "bAr"], "/foo/bar", true)]
|
||||
// Last component
|
||||
#[case(&["ba"], "/foo/bar", true)]
|
||||
#[case(&["fo"], "/foo/bar", false)]
|
||||
// Slash as suffix
|
||||
#[case(&["foo/"], "/foo", false)]
|
||||
#[case(&["foo/"], "/foo/bar", true)]
|
||||
#[case(&["foo/"], "/foo/bar/baz", false)]
|
||||
#[case(&["foo", "/"], "/foo", false)]
|
||||
#[case(&["foo", "/"], "/foo/bar", true)]
|
||||
#[case(&["foo", "/"], "/foo/bar/baz", true)]
|
||||
// Split components
|
||||
#[case(&["/", "fo", "/", "ar"], "/foo/bar", true)]
|
||||
#[case(&["oo/ba"], "/foo/bar", true)]
|
||||
// Overlap
|
||||
#[case(&["foo", "o", "bar"], "/foo/bar", false)]
|
||||
#[case(&["/foo/", "/bar"], "/foo/bar", false)]
|
||||
#[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
|
||||
fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
|
||||
let mut db =
|
||||
Database { dirs: Vec::new().into(), modified: false, data_dir: &PathBuf::new() };
|
||||
let stream = db.stream(0).with_keywords(keywords);
|
||||
assert_eq!(is_match, stream.matches_keywords(path));
|
||||
}
|
||||
}
|
10
src/fzf.rs
10
src/fzf.rs
@ -16,10 +16,7 @@ impl Fzf {
|
||||
if multiple {
|
||||
command.arg("-m");
|
||||
}
|
||||
command
|
||||
.arg("-n2..")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped());
|
||||
command.arg("-n2..").stdin(Stdio::piped()).stdout(Stdio::piped());
|
||||
if let Some(fzf_opts) = config::zo_fzf_opts() {
|
||||
command.env("FZF_DEFAULT_OPTS", fzf_opts);
|
||||
}
|
||||
@ -41,10 +38,7 @@ impl Fzf {
|
||||
}
|
||||
|
||||
pub fn wait_select(self) -> Result<String> {
|
||||
let output = self
|
||||
.child
|
||||
.wait_with_output()
|
||||
.context("wait failed on fzf")?;
|
||||
let output = self.child.wait_with_output().context("wait failed on fzf")?;
|
||||
|
||||
match output.status.code() {
|
||||
// normal exit
|
||||
|
644
src/shell.rs
644
src/shell.rs
@ -39,329 +39,369 @@ mod tests {
|
||||
|
||||
use askama::Template;
|
||||
use assert_cmd::Command;
|
||||
use once_cell::sync::OnceCell;
|
||||
use seq_macro::seq;
|
||||
use rstest::rstest;
|
||||
|
||||
macro_rules! with_opts_size {
|
||||
($macro:ident) => {
|
||||
$macro!(24);
|
||||
};
|
||||
#[rstest]
|
||||
fn bash_bash(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Bash(&opts).render().unwrap();
|
||||
|
||||
Command::new("bash")
|
||||
.args(&["--noprofile", "--norc", "-c", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
fn opts() -> &'static [Opts<'static>] {
|
||||
static OPTS: OnceCell<Vec<Opts>> = OnceCell::new();
|
||||
const BOOLS: &[bool] = &[false, true];
|
||||
const HOOKS: &[InitHook] = &[InitHook::None, InitHook::Prompt, InitHook::Pwd];
|
||||
const CMDS: &[Option<&str>] = &[None, Some("z")];
|
||||
#[rstest]
|
||||
fn bash_shellcheck(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Bash(&opts).render().unwrap();
|
||||
|
||||
OPTS.get_or_init(|| {
|
||||
let mut opts = Vec::new();
|
||||
for &echo in BOOLS {
|
||||
for &resolve_symlinks in BOOLS {
|
||||
for &hook in HOOKS {
|
||||
for &cmd in CMDS {
|
||||
opts.push(Opts {
|
||||
cmd,
|
||||
hook,
|
||||
echo,
|
||||
resolve_symlinks,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the value hardcoded into `with_opts_size` is correct.
|
||||
macro_rules! id {
|
||||
($x:literal) => {
|
||||
$x
|
||||
};
|
||||
}
|
||||
assert_eq!(opts.len(), with_opts_size!(id));
|
||||
|
||||
opts
|
||||
})
|
||||
Command::new("shellcheck")
|
||||
.args(&["--enable", "all", "--shell", "bash", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
macro_rules! make_tests {
|
||||
($N:literal) => {
|
||||
seq!(i in 0..$N {
|
||||
#[test]
|
||||
fn bash_bash_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Bash(opts).render().unwrap();
|
||||
Command::new("bash")
|
||||
.args(&["--noprofile", "--norc", "-c", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
#[rstest]
|
||||
fn bash_shfmt(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let mut source = Bash(&opts).render().unwrap();
|
||||
source.push('\n');
|
||||
|
||||
#[test]
|
||||
fn bash_shellcheck_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Bash(opts).render().unwrap();
|
||||
Command::new("shellcheck")
|
||||
.args(&["--enable", "all", "--shell", "bash", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
Command::new("shfmt")
|
||||
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_shfmt_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let mut source = Bash(opts).render().unwrap();
|
||||
source.push('\n');
|
||||
#[rstest]
|
||||
fn elvish_elvish(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let mut source = String::new();
|
||||
|
||||
Command::new("shfmt")
|
||||
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
// Filter out lines using edit:*, since those functions
|
||||
// are only available in the interactive editor.
|
||||
for line in
|
||||
Elvish(&opts).render().unwrap().split('\n').filter(|line| !line.contains("edit:"))
|
||||
{
|
||||
source.push_str(line);
|
||||
source.push('\n');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elvish_elvish_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let mut source = String::new();
|
||||
Command::new("elvish")
|
||||
.args(&["-c", &source, "-norc"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
// Filter out lines using edit:*, since those functions
|
||||
// are only available in the interactive editor.
|
||||
for line in Elvish(opts)
|
||||
.render()
|
||||
.unwrap()
|
||||
.split('\n')
|
||||
.filter(|line| !line.contains("edit:"))
|
||||
{
|
||||
source.push_str(line);
|
||||
source.push('\n');
|
||||
}
|
||||
#[rstest]
|
||||
fn fish_fish(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Fish(&opts).render().unwrap();
|
||||
|
||||
Command::new("elvish")
|
||||
.args(&["-c", &source, "-norc"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let tempdir = tempdir.path().to_str().unwrap();
|
||||
|
||||
#[test]
|
||||
fn fish_fish_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Fish(opts).render().unwrap();
|
||||
Command::new("fish")
|
||||
.env("HOME", tempdir)
|
||||
.args(&["--command", &source, "--private"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let tempdir = tempdir.path().to_str().unwrap();
|
||||
#[rstest]
|
||||
fn fish_fishindent(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let mut source = Fish(&opts).render().unwrap();
|
||||
source.push('\n');
|
||||
|
||||
Command::new("fish")
|
||||
.env("HOME", tempdir)
|
||||
.args(&["--command", &source, "--private"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let tempdir = tempdir.path().to_str().unwrap();
|
||||
|
||||
#[test]
|
||||
fn fish_fishindent_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let mut source = Fish(opts).render().unwrap();
|
||||
source.push('\n');
|
||||
Command::new("fish")
|
||||
.env("HOME", tempdir)
|
||||
.args(&["--command", "fish_indent", "--private"])
|
||||
.write_stdin(source.to_string())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(source)
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let tempdir = tempdir.path().to_str().unwrap();
|
||||
#[rstest]
|
||||
fn nushell_nushell(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Nushell(&opts).render().unwrap();
|
||||
|
||||
Command::new("fish")
|
||||
.env("HOME", tempdir)
|
||||
.args(&["--command", "fish_indent", "--private"])
|
||||
.write_stdin(source.to_string())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(source)
|
||||
.stderr("");
|
||||
}
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let tempdir = tempdir.path().to_str().unwrap();
|
||||
|
||||
#[test]
|
||||
fn nushell_nushell_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Nushell(opts).render().unwrap();
|
||||
let assert = Command::new("nu")
|
||||
.env("HOME", tempdir)
|
||||
.args(&["--commands", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let tempdir = tempdir.path().to_str().unwrap();
|
||||
|
||||
let assert = Command::new("nu")
|
||||
.env("HOME", tempdir)
|
||||
.args(&["--commands", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
|
||||
if opts.hook != InitHook::Pwd {
|
||||
assert.stdout("");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posix_bashposix_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Posix(opts).render().unwrap();
|
||||
let assert = Command::new("bash")
|
||||
.args(&["--posix", "--noprofile", "--norc", "-c", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
|
||||
if opts.hook != InitHook::Pwd {
|
||||
assert.stdout("");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posix_dash_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Posix(opts).render().unwrap();
|
||||
let assert = Command::new("dash")
|
||||
.args(&["-c", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
|
||||
if opts.hook != InitHook::Pwd {
|
||||
assert.stdout("");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posix_shellcheck_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Posix(opts).render().unwrap();
|
||||
Command::new("shellcheck")
|
||||
.args(&["--enable", "all", "--shell", "sh", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posix_shfmt_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let mut source = Posix(opts).render().unwrap();
|
||||
source.push('\n');
|
||||
Command::new("shfmt")
|
||||
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powershell_pwsh_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Powershell(opts).render().unwrap();
|
||||
Command::new("pwsh")
|
||||
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xonsh_black_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let mut source = Xonsh(opts).render().unwrap();
|
||||
source.push('\n');
|
||||
Command::new("black")
|
||||
.args(&["--check", "--diff", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xonsh_mypy_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Xonsh(opts).render().unwrap();
|
||||
Command::new("mypy")
|
||||
.args(&["--command", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xonsh_pylint_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let mut source = Xonsh(opts).render().unwrap();
|
||||
source.push('\n');
|
||||
Command::new("pylint")
|
||||
.args(&["--from-stdin", "zoxide"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xonsh_xonsh_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Xonsh(opts).render().unwrap();
|
||||
|
||||
// We can't pass the source directly to `xonsh -c` due to
|
||||
// a bug: <https://github.com/xonsh/xonsh/issues/3959>
|
||||
Command::new("xonsh")
|
||||
.args(&[
|
||||
"-c",
|
||||
"import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')",
|
||||
"--no-rc"
|
||||
])
|
||||
.write_stdin(source.as_bytes())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zsh_shellcheck_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Zsh(opts).render().unwrap();
|
||||
// ShellCheck doesn't support zsh yet.
|
||||
// https://github.com/koalaman/shellcheck/issues/809
|
||||
Command::new("shellcheck")
|
||||
.args(&["--enable", "all", "--shell", "bash", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zsh_zsh_#i() {
|
||||
let opts = dbg!(&opts()[i]);
|
||||
let source = Zsh(opts).render().unwrap();
|
||||
Command::new("zsh")
|
||||
.args(&["-c", &source, "--no-rcs"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
});
|
||||
if opts.hook != InitHook::Pwd {
|
||||
assert.stdout("");
|
||||
}
|
||||
}
|
||||
|
||||
with_opts_size!(make_tests);
|
||||
#[rstest]
|
||||
fn posix_bashposix(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Posix(&opts).render().unwrap();
|
||||
|
||||
let assert = Command::new("bash")
|
||||
.args(&["--posix", "--noprofile", "--norc", "-c", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
|
||||
if opts.hook != InitHook::Pwd {
|
||||
assert.stdout("");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn posix_dash(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Posix(&opts).render().unwrap();
|
||||
|
||||
let assert = Command::new("dash").args(&["-c", &source]).assert().success().stderr("");
|
||||
if opts.hook != InitHook::Pwd {
|
||||
assert.stdout("");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn posix_shellcheck_(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Posix(&opts).render().unwrap();
|
||||
|
||||
Command::new("shellcheck")
|
||||
.args(&["--enable", "all", "--shell", "sh", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn posix_shfmt(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let mut source = Posix(&opts).render().unwrap();
|
||||
source.push('\n');
|
||||
|
||||
Command::new("shfmt")
|
||||
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn powershell_pwsh(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Powershell(&opts).render().unwrap();
|
||||
|
||||
Command::new("pwsh")
|
||||
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn xonsh_black(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let mut source = Xonsh(&opts).render().unwrap();
|
||||
source.push('\n');
|
||||
|
||||
Command::new("black")
|
||||
.args(&["--check", "--diff", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn xonsh_mypy(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Xonsh(&opts).render().unwrap();
|
||||
|
||||
Command::new("mypy").args(&["--command", &source]).assert().success().stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn xonsh_pylint(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let mut source = Xonsh(&opts).render().unwrap();
|
||||
source.push('\n');
|
||||
|
||||
Command::new("pylint")
|
||||
.args(&["--from-stdin", "zoxide"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn xonsh_xonsh(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Xonsh(&opts).render().unwrap();
|
||||
|
||||
// We can't pass the source directly to `xonsh -c` due to
|
||||
// a bug: <https://github.com/xonsh/xonsh/issues/3959>
|
||||
Command::new("xonsh")
|
||||
.args(&[
|
||||
"-c",
|
||||
"import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')",
|
||||
"--no-rc",
|
||||
])
|
||||
.write_stdin(source.as_bytes())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn zsh_shellcheck(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Zsh(&opts).render().unwrap();
|
||||
|
||||
// ShellCheck doesn't support zsh yet.
|
||||
// https://github.com/koalaman/shellcheck/issues/809
|
||||
Command::new("shellcheck")
|
||||
.args(&["--enable", "all", "--shell", "bash", "-"])
|
||||
.write_stdin(source)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn zsh_zsh(
|
||||
#[values(None, Some("z"))] cmd: Option<&str>,
|
||||
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
|
||||
#[values(false, true)] echo: bool,
|
||||
#[values(false, true)] resolve_symlinks: bool,
|
||||
) {
|
||||
let opts = Opts { cmd, hook, echo, resolve_symlinks };
|
||||
let source = Zsh(&opts).render().unwrap();
|
||||
|
||||
Command::new("zsh")
|
||||
.args(&["-c", &source, "--no-rcs"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,7 @@ pub fn current_time() -> Result<Epoch> {
|
||||
|
||||
pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
|
||||
let path = path.as_ref();
|
||||
path.to_str()
|
||||
.with_context(|| format!("invalid unicode in path: {}", path.display()))
|
||||
path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display()))
|
||||
}
|
||||
|
||||
/// Resolves the absolute version of a path.
|
||||
|
@ -38,13 +38,7 @@ fn completions_fish() {
|
||||
fn completions_powershell() {
|
||||
let source = include_str!("../contrib/completions/_zoxide.ps1");
|
||||
Command::new("pwsh")
|
||||
.args(&[
|
||||
"-NoLogo",
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
source,
|
||||
])
|
||||
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
@ -62,10 +56,5 @@ fn completions_zsh() {
|
||||
compinit -u
|
||||
"#;
|
||||
|
||||
Command::new("zsh")
|
||||
.args(&["-c", source, "--no-rcs"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
Command::new("zsh").args(&["-c", source, "--no-rcs"]).assert().success().stdout("").stderr("");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user