Skip to content

Commit 99d6827

Browse files
committed
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.
1 parent 6324b4e commit 99d6827

File tree

6 files changed

+72
-6
lines changed

6 files changed

+72
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ All notable changes to this project will be documented in this file.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## Unreleased
11+
12+
### Added
13+
14+
- `$_ZO_CASE_SENSITIVITY` to support case-sensitive querying in addition to the
15+
default case-insensitive querying.
16+
1017
## [0.9.8] - 2025-05-27
1118

1219
### Added

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ When calling `zoxide init`, the following flags are available:
426426
Environment variables[^2] can be used for configuration. They must be set before
427427
`zoxide init` is called.
428428
429+
- `_ZO_CASE_SENSITIVITY`
430+
- Defaults to case-insensitive searches. Set to `case-sensitive` for
431+
case-sensitive searching.
429432
- `_ZO_DATA_DIR`
430433
- Specifies the directory in which the database is stored.
431434
- The default value varies across OSes:

src/cmd/cmd.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ https://github.com/ajeetdsouza/zoxide
2222
{all-args}{after-help}
2323
2424
<bold><underline>Environment variables:</underline></bold>
25+
{tab}<bold>_ZO_CASE_SENSITIVITY</bold>{tab}Set case-sensitivity: case-sensitive or case-insensitive (default)
2526
{tab}<bold>_ZO_DATA_DIR</bold> {tab}Path for zoxide data files
2627
{tab}<bold>_ZO_ECHO</bold> {tab}Print the matched directory before navigating to it when set to 1
2728
{tab}<bold>_ZO_EXCLUDE_DIRS</bold> {tab}List of directory globs to be excluded

src/cmd/query.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ impl Query {
7979
fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Result<Stream<'a>> {
8080
let mut options = StreamOptions::new(now)
8181
.with_keywords(self.keywords.iter().map(|s| s.as_str()))
82+
.with_case_sensitivity(config::case_sensitivity())
8283
.with_exclude(config::exclude_dirs()?);
8384
if !self.all {
8485
let resolve_symlinks = config::resolve_symlinks();

src/config.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ use anyhow::{Context, Result, ensure};
66
use glob::Pattern;
77

88
use crate::db::Rank;
9+
use crate::util;
10+
11+
pub enum CaseSensitivity {
12+
CaseInsensitive,
13+
CaseSensitive,
14+
}
15+
16+
impl CaseSensitivity {
17+
pub fn convert_case(&self, s: &str) -> String {
18+
match self {
19+
CaseSensitivity::CaseInsensitive => util::to_lowercase(s),
20+
CaseSensitivity::CaseSensitive => s.into(),
21+
}
22+
}
23+
}
24+
25+
pub fn case_sensitivity() -> CaseSensitivity {
26+
env::var_os("_ZO_CASE_SENSITIVITY")
27+
.map_or(CaseSensitivity::CaseInsensitive, map_case_sensitivity)
28+
}
29+
30+
fn map_case_sensitivity(s: OsString) -> CaseSensitivity {
31+
match s.to_str() {
32+
Some("case-sensitive") => CaseSensitivity::CaseSensitive,
33+
_ => CaseSensitivity::CaseInsensitive,
34+
}
35+
}
936

1037
pub fn data_dir() -> Result<PathBuf> {
1138
let dir = match env::var_os("_ZO_DATA_DIR") {

src/db/stream.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use std::{fs, path};
44

55
use glob::Pattern;
66

7+
use crate::config::CaseSensitivity;
78
use crate::db::{Database, Dir, Epoch};
8-
use crate::util::{self, MONTH};
9+
use crate::util::MONTH;
910

1011
pub struct Stream<'a> {
1112
db: &'a mut Database,
@@ -48,13 +49,18 @@ impl<'a> Stream<'a> {
4849
}
4950

5051
fn filter_by_keywords(&self, path: &str) -> bool {
51-
let (keywords_last, keywords) = match self.options.keywords.split_last() {
52+
let keywords: Vec<String> = self
53+
.options
54+
.keywords
55+
.iter()
56+
.map(|s| self.options.case_sensitivity.convert_case(s))
57+
.collect();
58+
59+
let (keywords_last, keywords) = match keywords.split_last() {
5260
Some(split) => split,
5361
None => return true,
5462
};
55-
56-
let path = util::to_lowercase(path);
57-
let mut path = path.as_str();
63+
let mut path = &self.options.case_sensitivity.convert_case(path)[..];
5864
match path.rfind(keywords_last) {
5965
Some(idx) => {
6066
if path[idx + keywords_last.len()..].contains(path::is_separator) {
@@ -112,6 +118,9 @@ pub struct StreamOptions {
112118
/// Directories that do not exist and haven't been accessed since TTL will
113119
/// be lazily removed.
114120
ttl: Epoch,
121+
122+
/// Whether searching should be perform case sensitively.
123+
case_sensitivity: CaseSensitivity,
115124
}
116125

117126
impl StreamOptions {
@@ -123,6 +132,7 @@ impl StreamOptions {
123132
exists: false,
124133
resolve_symlinks: false,
125134
ttl: now.saturating_sub(3 * MONTH),
135+
case_sensitivity: CaseSensitivity::CaseInsensitive,
126136
}
127137
}
128138

@@ -131,7 +141,12 @@ impl StreamOptions {
131141
I: IntoIterator,
132142
I::Item: AsRef<str>,
133143
{
134-
self.keywords = keywords.into_iter().map(util::to_lowercase).collect();
144+
self.keywords = keywords.into_iter().map(|s| s.as_ref().into()).collect();
145+
self
146+
}
147+
148+
pub fn with_case_sensitivity(mut self, case_sensitivity: CaseSensitivity) -> Self {
149+
self.case_sensitivity = case_sensitivity;
135150
self
136151
}
137152

@@ -185,4 +200,16 @@ mod tests {
185200
let stream = Stream::new(db, options);
186201
assert_eq!(is_match, stream.filter_by_keywords(path));
187202
}
203+
204+
#[rstest]
205+
// Case normalization
206+
#[case(&["fOo", "bAr"], "/foo/bar", false)]
207+
fn query_case_sensitive(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
208+
let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false);
209+
let options = StreamOptions::new(0)
210+
.with_keywords(keywords.iter())
211+
.with_case_sensitivity(CaseSensitivity::CaseSensitive);
212+
let stream = Stream::new(db, options);
213+
assert_eq!(is_match, stream.filter_by_keywords(path));
214+
}
188215
}

0 commit comments

Comments
 (0)