From 75d37fb73b6030ae63dba2e7910bf3ad9eb607f9 Mon Sep 17 00:00:00 2001 From: Yash Pratap Kulshrestha <147393761+yash3605@users.noreply.github.com> Date: Sat, 23 May 2026 11:44:42 +0530 Subject: [PATCH 1/4] Feat: Add --hierarchy flag for showing parent context lines --- Cargo.lock | 94 ++++++++++++------------ crates/core/flags/defs.rs | 57 +++++++++++++++ crates/core/flags/hiargs.rs | 13 +++- crates/core/flags/lowargs.rs | 1 + crates/core/search.rs | 137 +++++++++++++++++++++++++++++++++-- fuzz/Cargo.lock | 2 +- 6 files changed, 249 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 560207ace9..33e0bf7abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "getrandom" @@ -258,9 +258,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -274,33 +274,33 @@ dependencies = [ [[package]] name = "lexopt" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" +checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -329,24 +329,24 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -359,9 +359,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -371,9 +371,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ripgrep" @@ -405,12 +405,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -451,15 +445,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -470,9 +464,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.107" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -516,9 +510,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "walkdir" @@ -568,3 +562,9 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/core/flags/defs.rs b/crates/core/flags/defs.rs index b2fc68f49f..a522bfa900 100644 --- a/crates/core/flags/defs.rs +++ b/crates/core/flags/defs.rs @@ -77,6 +77,7 @@ pub(super) const FLAGS: &[&dyn Flag] = &[ &Heading, &Help, &Hidden, + &Hierarchy, &HostnameBin, &HyperlinkFormat, &IGlob, @@ -2832,6 +2833,62 @@ fn test_hidden() { assert_eq!(true, args.hidden); } +#[derive(Debug)] +struct Hierarchy; + +impl Flag for Hierarchy { + fn is_switch(&self) -> bool { + true + } + fn name_long(&self) -> &'static str { + "hierarchy" + } + fn name_negated(&self) -> Option<&'static str> { + Some("no-hierarchy") + } + fn doc_category(&self) -> Category { + Category::Output + } + fn doc_short(&self) -> &'static str { + r"Adds a Top-Level Context Line." + } + fn doc_long(&self) -> &'static str { + r"When enabled, ripgrep prints an additional top-level context +line before matched results to visually group related output. + +This is useful when searching across deeply nested files, structured +documents, logs, or hierarchical data where extra context improves +readability and navigation" + } + fn update( + &self, + value: FlagValue, + args: &mut crate::flags::lowargs::LowArgs, + ) -> anyhow::Result<()> { + args.hierarchy = Some(value.unwrap_switch()); + Ok(()) + } +} + +#[cfg(test)] +#[test] +fn test_hierarchy() { + let args = parse_low_raw(None::<&str>).unwrap(); + assert_eq!(None, args.hierarchy); + + let args = parse_low_raw(["--hierarchy"]).unwrap(); + assert_eq!(Some(true), args.hierarchy); + + let args = parse_low_raw(["--no-hierarchy"]).unwrap(); + assert_eq!(Some(false), args.hierarchy); + + let args = parse_low_raw(["--hierarchy", "--no-hierarchy"]).unwrap(); + assert_eq!(Some(false), args.hierarchy); + + let args = parse_low_raw(["--no-hierarchy", "--hierarchy"]).unwrap(); + assert_eq!(Some(true), args.hierarchy); +} + /// --hostname-bin #[derive(Debug)] struct HostnameBin; diff --git a/crates/core/flags/hiargs.rs b/crates/core/flags/hiargs.rs index 526c91e6d7..f68f5d6152 100644 --- a/crates/core/flags/hiargs.rs +++ b/crates/core/flags/hiargs.rs @@ -57,6 +57,8 @@ pub(crate) struct HiArgs { globs: ignore::overrides::Override, heading: bool, hidden: bool, + /// Whether to print hierarchical parent lines for each match. + hierarchy: bool, hyperlink_config: grep::printer::HyperlinkConfig, ignore_file_case_insensitive: bool, ignore_file: Vec, @@ -163,6 +165,7 @@ impl HiArgs { Some(false) => false, Some(true) => !low.vimgrep, }; + let hierarchy = low.hierarchy.unwrap_or(false); let path_terminator = if low.null { Some(b'\x00') } else { None }; let quit_after_match = stats.is_none() && low.quiet; let threads = if low.sort.is_some() || paths.is_one_file { @@ -274,6 +277,7 @@ impl HiArgs { follow: low.follow, heading, hidden: low.hidden, + hierarchy, hyperlink_config, ignore_file: low.ignore_file, ignore_file_case_insensitive: low.ignore_file_case_insensitive, @@ -360,6 +364,12 @@ impl HiArgs { builder } + /// Whether to print hierarchical parent lines for each match. + #[allow(dead_code)] + pub(crate) fn hierarchy(&self) -> bool { + self.hierarchy + } + /// Return the matcher that should be used for searching using the engine /// choice made by the user. /// @@ -703,7 +713,8 @@ impl HiArgs { .preprocessor_globs(self.pre_globs.clone()) .search_zip(self.search_zip) .binary_detection_explicit(self.binary.explicit.clone()) - .binary_detection_implicit(self.binary.implicit.clone()); + .binary_detection_implicit(self.binary.implicit.clone()) + .hierarchy(self.hierarchy); Ok(builder.build(matcher, searcher, printer)) } diff --git a/crates/core/flags/lowargs.rs b/crates/core/flags/lowargs.rs index 1941cae45d..a73596b186 100644 --- a/crates/core/flags/lowargs.rs +++ b/crates/core/flags/lowargs.rs @@ -59,6 +59,7 @@ pub(crate) struct LowArgs { pub(crate) globs: Vec, pub(crate) heading: Option, pub(crate) hidden: bool, + pub(crate) hierarchy: Option, pub(crate) hostname_bin: Option, pub(crate) hyperlink_format: HyperlinkFormat, pub(crate) iglobs: Vec, diff --git a/crates/core/search.rs b/crates/core/search.rs index 2fb4cbadc2..fad32fadf8 100644 --- a/crates/core/search.rs +++ b/crates/core/search.rs @@ -7,6 +7,7 @@ read and matched using the regex engine) and the printer. For example, the search worker is where things like preprocessors or decompression happens. */ +use std::collections::HashMap; use std::{io, path::Path}; use {grep::matcher::Matcher, termcolor::WriteColor}; @@ -20,16 +21,76 @@ struct Config { preprocessor: Option, preprocessor_globs: ignore::overrides::Override, search_zip: bool, + hierarchy: bool, binary_implicit: grep::searcher::BinaryDetection, binary_explicit: grep::searcher::BinaryDetection, } +struct HierarchySink { + pub inner: S, + map: HashMap)>>, +} + +impl grep::searcher::Sink for HierarchySink { + type Error = S::Error; + + fn matched( + &mut self, + searcher: &grep::searcher::Searcher, + mat: &grep::searcher::SinkMatch<'_>, + ) -> Result { + if let Some(n) = mat.line_number() { + if let Some(parents) = self.map.get(&n) { + for (line_num, content) in parents { + println!( + "{}: {}", + line_num, + String::from_utf8_lossy(content) + ); + } + } + } + self.inner.matched(searcher, mat) + } + + fn context( + &mut self, + searcher: &grep::searcher::Searcher, + ctx: &grep::searcher::SinkContext<'_>, + ) -> Result { + self.inner.context(searcher, ctx) + } + + fn context_break( + &mut self, + searcher: &grep::searcher::Searcher, + ) -> Result { + self.inner.context_break(searcher) + } + + fn begin( + &mut self, + searcher: &grep::searcher::Searcher, + ) -> Result { + self.inner.begin(searcher) + } + + fn finish( + &mut self, + searcher: &grep::searcher::Searcher, + finish: &grep::searcher::SinkFinish, + ) -> Result<(), Self::Error> { + self.inner.finish(searcher, finish) + } +} + impl Default for Config { fn default() -> Config { Config { preprocessor: None, preprocessor_globs: ignore::overrides::Override::empty(), search_zip: false, + hierarchy: false, binary_implicit: grep::searcher::BinaryDetection::none(), binary_explicit: grep::searcher::BinaryDetection::none(), } @@ -113,6 +174,13 @@ impl SearchWorkerBuilder { self } + /// when this set, a sink is created at the time of file searching to + /// to provide extra hierarchical context. + pub(crate) fn hierarchy(&mut self, yes: bool) -> &mut SearchWorkerBuilder { + self.config.hierarchy = yes; + self + } + /// Enable the decompression and searching of common compressed files. /// /// When enabled, if a particular file path is recognized as a compressed @@ -344,7 +412,9 @@ impl SearchWorker { let (searcher, printer) = (&mut self.searcher, &mut self.printer); match self.matcher { - RustRegex(ref m) => search_path(m, searcher, printer, path), + RustRegex(ref m) => { + search_path(m, searcher, printer, path, self.config.hierarchy) + } #[cfg(feature = "pcre2")] PCRE2(ref m) => search_path(m, searcher, printer, path), } @@ -382,15 +452,38 @@ fn search_path( searcher: &mut grep::searcher::Searcher, printer: &mut Printer, path: &Path, + hierarchy: bool, ) -> io::Result { match *printer { Printer::Standard(ref mut p) => { let mut sink = p.sink_with_path(&matcher, path); - searcher.search_path(&matcher, path, &mut sink)?; - Ok(SearchResult { - has_match: sink.has_match(), - stats: sink.stats().map(|s| s.clone()), - }) + if hierarchy { + match build_hierarchy_map(path) { + Ok(map) => { + let mut hsink = HierarchySink { inner: sink, map }; + searcher.search_path(&matcher, path, &mut hsink)?; + Ok(SearchResult { + has_match: hsink.inner.has_match(), + stats: hsink.inner.stats().map(|s| s.clone()), + }) + } + Err(_) => { + // if we can't build the map, fall back to normal search + let mut sink = p.sink_with_path(&matcher, path); + searcher.search_path(&matcher, path, &mut sink)?; + Ok(SearchResult { + has_match: sink.has_match(), + stats: sink.stats().map(|s| s.clone()), + }) + } + } + } else { + searcher.search_path(&matcher, path, &mut sink)?; + Ok(SearchResult { + has_match: sink.has_match(), + stats: sink.stats().map(|s| s.clone()), + }) + } } Printer::Summary(ref mut p) => { let mut sink = p.sink_with_path(&matcher, path); @@ -447,3 +540,35 @@ fn search_reader( } } } + +fn build_hierarchy_map( + path: &Path, +) -> io::Result)>>> { + use std::io::BufRead; + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + + let mut stack: Vec<(u64, Vec, usize)> = vec![]; + let mut map = HashMap::new(); + let mut line_num = 0u64; + + for line in reader.lines() { + let line = line?; + line_num += 1; + let trimmed = line.trim_start(); + let current_indent = line.len() - trimmed.len(); + while let Some(top) = stack.last() { + if top.2 >= current_indent { + stack.pop(); + } else { + break; + } + } + map.insert( + line_num, + stack.iter().map(|(ln, bytes, _)| (*ln, bytes.clone())).collect(), + ); + stack.push((line_num, line.into_bytes(), current_indent)); + } + Ok(map) +} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4f833f3db6..c0cbde26bc 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -61,7 +61,7 @@ dependencies = [ [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" dependencies = [ "aho-corasick", "arbitrary", From 2b6cda54a715bf27acdcfc7e27758c815af6f081 Mon Sep 17 00:00:00 2001 From: Yash Pratap Kulshrestha <147393761+yash3605@users.noreply.github.com> Date: Sat, 23 May 2026 12:09:59 +0530 Subject: [PATCH 2/4] Fix missing hierarchy arg in PCRE2 search path --- crates/core/search.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/core/search.rs b/crates/core/search.rs index fad32fadf8..212304bbec 100644 --- a/crates/core/search.rs +++ b/crates/core/search.rs @@ -416,7 +416,9 @@ impl SearchWorker { search_path(m, searcher, printer, path, self.config.hierarchy) } #[cfg(feature = "pcre2")] - PCRE2(ref m) => search_path(m, searcher, printer, path), + PCRE2(ref m) => { + search_path(m, searcher, printer, path, self.config.hierarchy) + } } } From eab1b63e3f03bcf52736bad9b202328e630f9f3f Mon Sep 17 00:00:00 2001 From: Yash Pratap Kulshrestha <147393761+yash3605@users.noreply.github.com> Date: Sat, 23 May 2026 12:18:14 +0530 Subject: [PATCH 3/4] Fix doc_long formatting and doc_short style for --hierarchy flag --- crates/core/flags/defs.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/core/flags/defs.rs b/crates/core/flags/defs.rs index a522bfa900..c6cfa198b0 100644 --- a/crates/core/flags/defs.rs +++ b/crates/core/flags/defs.rs @@ -2850,12 +2850,11 @@ impl Flag for Hierarchy { Category::Output } fn doc_short(&self) -> &'static str { - r"Adds a Top-Level Context Line." + r"adds a top-Level context line" } fn doc_long(&self) -> &'static str { r"When enabled, ripgrep prints an additional top-level context line before matched results to visually group related output. - This is useful when searching across deeply nested files, structured documents, logs, or hierarchical data where extra context improves readability and navigation" From 1ba03a924fb947f9bfe4def73b1156a2061a9195 Mon Sep 17 00:00:00 2001 From: Yash Pratap Kulshrestha <147393761+yash3605@users.noreply.github.com> Date: Sat, 23 May 2026 12:28:56 +0530 Subject: [PATCH 4/4] Add --hierarchy and --no-hierarchy to zsh completions --- crates/core/flags/complete/rg.zsh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/core/flags/complete/rg.zsh b/crates/core/flags/complete/rg.zsh index ae9f119401..6f8127d362 100644 --- a/crates/core/flags/complete/rg.zsh +++ b/crates/core/flags/complete/rg.zsh @@ -130,6 +130,9 @@ _rg() { '(pretty-vimgrep)--heading[show matches grouped by file name]' "(pretty-vimgrep)--no-heading[don't show matches grouped by file name]" + '(no-hierarchy)--hierarchy[print hierarchical parent lines for each match]' + '(hierarchy)--no-hierarchy[do not print hierarchical parent lines for each match]' + + '(hidden)' # Hidden-file options {-.,--hidden}'[search hidden files and directories]' $no"--no-hidden[don't search hidden files and directories]"