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

Add support for case-sensitive queries #959

Open
wants to merge 1 commit into
base: main
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/cmd/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ https://github.com/ajeetdsouza/zoxide
{all-args}{after-help}

<bold><underline>Environment variables:</underline></bold>
{tab}<bold>_ZO_CASE_SENSITIVITY</bold>{tab}Set case-sensitivity: case-sensitive or case-insensitive (default)
{tab}<bold>_ZO_DATA_DIR</bold> {tab}Path for zoxide data files
{tab}<bold>_ZO_ECHO</bold> {tab}Print the matched directory before navigating to it when set to 1
{tab}<bold>_ZO_EXCLUDE_DIRS</bold> {tab}List of directory globs to be excluded
Expand Down
1 change: 1 addition & 0 deletions src/cmd/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl Query {
fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Result<Stream<'a>> {
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();
Expand Down
27 changes: 27 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
let dir = match env::var_os("_ZO_DATA_DIR") {
Expand Down
39 changes: 33 additions & 6 deletions src/db/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<String> = 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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -123,6 +132,7 @@ impl StreamOptions {
exists: false,
resolve_symlinks: false,
ttl: now.saturating_sub(3 * MONTH),
case_sensitivity: CaseSensitivity::CaseInsensitive,
}
}

Expand All @@ -131,7 +141,12 @@ impl StreamOptions {
I: IntoIterator,
I::Item: AsRef<str>,
{
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
}

Expand Down Expand Up @@ -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));
}
}
Loading