From 3571c32561921a336edbdb7ce08e2128fba341f8 Mon Sep 17 00:00:00 2001 From: Ryan Winograd Date: Tue, 3 Dec 2024 16:32:43 -0600 Subject: [PATCH] Add support for case-sensitive queries Add _ZO_CASE_SENSITIVITY env var to control whether queries should be performed in a case-sensitive manner, keeping the current behavior (case-insensitive) as the default. Setting _ZO_CASE_SENSITIVITY=case-sensitive changes the query behavior to be case-sensitive. --- CHANGELOG.md | 7 +++++++ README.md | 3 +++ src/cmd/cmd.rs | 1 + src/cmd/query.rs | 1 + src/config.rs | 27 +++++++++++++++++++++++++++ src/db/stream.rs | 39 +++++++++++++++++++++++++++++++++------ 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1bd8c3c..b7746555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- `$_ZO_CASE_SENSITIVITY` to support case-sensitive querying in addition to the + default case-insensitive querying. + ## [0.9.6] - 2024-09-19 ### Fixed diff --git a/README.md b/README.md index 882222dc..c9625d3e 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,9 @@ When calling `zoxide init`, the following flags are available: Environment variables[^2] can be used for configuration. They must be set before `zoxide init` is called. +- `_ZO_CASE_SENSITIVITY` + - Defaults to case-insensitive searches. Set to `case-sensitive` for + case-sensitive searching. - `_ZO_DATA_DIR` - Specifies the directory in which the database is stored. - The default value varies across OSes: diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 3b05a60b..93f38780 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -17,6 +17,7 @@ https://github.com/ajeetdsouza/zoxide {all-args}{after-help} Environment variables: +{tab}_ZO_CASE_SENSITIVITY{tab}Set case-sensitivity: case-sensitive or case-insensitive (default) {tab}_ZO_DATA_DIR {tab}Path for zoxide data files {tab}_ZO_ECHO {tab}Print the matched directory before navigating to it when set to 1 {tab}_ZO_EXCLUDE_DIRS {tab}List of directory globs to be excluded diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 362d80a3..91545c21 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -79,6 +79,7 @@ impl Query { fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Result> { let mut options = StreamOptions::new(now) .with_keywords(self.keywords.iter().map(|s| s.as_str())) + .with_case_sensitivity(config::case_sensitivity()) .with_exclude(config::exclude_dirs()?); if !self.all { let resolve_symlinks = config::resolve_symlinks(); diff --git a/src/config.rs b/src/config.rs index 4b46a78c..c9b9cef9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,33 @@ use anyhow::{ensure, Context, Result}; use glob::Pattern; use crate::db::Rank; +use crate::util; + +pub enum CaseSensitivity { + CaseInsensitive, + CaseSensitive, +} + +impl CaseSensitivity { + pub fn convert_case(&self, s: &str) -> String { + match self { + CaseSensitivity::CaseInsensitive => util::to_lowercase(s), + CaseSensitivity::CaseSensitive => s.into(), + } + } +} + +pub fn case_sensitivity() -> CaseSensitivity { + env::var_os("_ZO_CASE_SENSITIVITY") + .map_or(CaseSensitivity::CaseInsensitive, map_case_sensitivity) +} + +fn map_case_sensitivity(s: OsString) -> CaseSensitivity { + match s.to_str() { + Some("case-sensitive") => CaseSensitivity::CaseSensitive, + _ => CaseSensitivity::CaseInsensitive, + } +} pub fn data_dir() -> Result { let dir = match env::var_os("_ZO_DATA_DIR") { diff --git a/src/db/stream.rs b/src/db/stream.rs index 4af7d7a9..e91f6833 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -4,8 +4,9 @@ use std::{fs, path}; use glob::Pattern; +use crate::config::CaseSensitivity; use crate::db::{Database, Dir, Epoch}; -use crate::util::{self, MONTH}; +use crate::util::MONTH; pub struct Stream<'a> { db: &'a mut Database, @@ -48,13 +49,18 @@ impl<'a> Stream<'a> { } fn filter_by_keywords(&self, path: &str) -> bool { - let (keywords_last, keywords) = match self.options.keywords.split_last() { + let keywords: Vec = self + .options + .keywords + .iter() + .map(|s| self.options.case_sensitivity.convert_case(s)) + .collect(); + + let (keywords_last, keywords) = match keywords.split_last() { Some(split) => split, None => return true, }; - - let path = util::to_lowercase(path); - let mut path = path.as_str(); + let mut path = &self.options.case_sensitivity.convert_case(path)[..]; match path.rfind(keywords_last) { Some(idx) => { if path[idx + keywords_last.len()..].contains(path::is_separator) { @@ -112,6 +118,9 @@ pub struct StreamOptions { /// Directories that do not exist and haven't been accessed since TTL will /// be lazily removed. ttl: Epoch, + + /// Whether searching should be perform case sensitively. + case_sensitivity: CaseSensitivity, } impl StreamOptions { @@ -123,6 +132,7 @@ impl StreamOptions { exists: false, resolve_symlinks: false, ttl: now.saturating_sub(3 * MONTH), + case_sensitivity: CaseSensitivity::CaseInsensitive, } } @@ -131,7 +141,12 @@ impl StreamOptions { I: IntoIterator, I::Item: AsRef, { - self.keywords = keywords.into_iter().map(util::to_lowercase).collect(); + self.keywords = keywords.into_iter().map(|s| s.as_ref().into()).collect(); + self + } + + pub fn with_case_sensitivity(mut self, case_sensitivity: CaseSensitivity) -> Self { + self.case_sensitivity = case_sensitivity; self } @@ -185,4 +200,16 @@ mod tests { let stream = Stream::new(db, options); assert_eq!(is_match, stream.filter_by_keywords(path)); } + + #[rstest] + // Case normalization + #[case(&["fOo", "bAr"], "/foo/bar", false)] + fn query_case_sensitive(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { + let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let options = StreamOptions::new(0) + .with_keywords(keywords.iter()) + .with_case_sensitivity(CaseSensitivity::CaseSensitive); + let stream = Stream::new(db, options); + assert_eq!(is_match, stream.filter_by_keywords(path)); + } }