Skip to content
Merged
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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ Short orientation for tools and contributors. Product direction and phased roadm

## CI-equivalent checks

Same as `.github/workflows/ci.yml` (Ubuntu, macOS, Windows; stable Rust):
Same as `.github/workflows/ci.yml` (Ubuntu, macOS; stable Rust):

```bash
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
```

**Mandatory precommit procedure**: Run all three CI-equivalent commands above **before pushing**. Fix any failures locally, then push.

Bench / `sift-profile`: package + features in `crates/core/benches/README.md` and `crates/core/README.md`. Fuzz is manual: `./scripts/fuzz.sh`.

## Conventions
Expand Down
109 changes: 109 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,22 @@ struct Cli {
#[command(flatten)]
unrestricted: UnrestrictedDecl,
#[command(flatten)]
context_decl: ContextDecl,
#[command(flatten)]
paths: PathArgs,
}

/// Declares `-A`/`-B`/`-C` for clap; effective values use [`resolve_context_from_args`].
#[derive(Args)]
struct ContextDecl {
#[arg(short = 'A', long = "after-context", value_name = "NUM", action = ArgAction::Append)]
_after: Vec<usize>,
#[arg(short = 'B', long = "before-context", value_name = "NUM", action = ArgAction::Append)]
_before: Vec<usize>,
#[arg(short = 'C', long = "context", value_name = "NUM", action = ArgAction::Append)]
_context: Vec<usize>,
}

/// Clap declarations only; effective values come from [`resolve_visibility_and_ignore`].
#[derive(Args)]
struct IgnoreNoDecl {
Expand Down Expand Up @@ -221,6 +234,94 @@ fn resolve_visibility_and_ignore(args: &[String]) -> (bool, IgnoreSources, bool)
(hidden, sources, require_git)
}

fn parse_usize_token(s: &str) -> Option<usize> {
s.parse().ok()
}

/// `-A` / `-B` / `-C` and long forms; argv order with later flags overriding (ripgrep-style).
fn resolve_context_from_args(args: &[String]) -> (usize, usize) {
let mut before = 0usize;
let mut after = 0usize;
let mut i = 0usize;
while i < args.len() {
let arg = args[i].as_str();
if let Some(rest) = arg.strip_prefix("--after-context=") {
if let Some(n) = parse_usize_token(rest) {
after = n;
}
i += 1;
continue;
}
if let Some(rest) = arg.strip_prefix("--before-context=") {
if let Some(n) = parse_usize_token(rest) {
before = n;
}
i += 1;
continue;
}
if let Some(rest) = arg.strip_prefix("--context=") {
if let Some(n) = parse_usize_token(rest) {
before = n;
after = n;
}
i += 1;
continue;
}
match arg {
"-A" | "--after-context" => {
if let Some(n) = args.get(i + 1).and_then(|s| parse_usize_token(s)) {
after = n;
i += 2;
continue;
}
}
"-B" | "--before-context" => {
if let Some(n) = args.get(i + 1).and_then(|s| parse_usize_token(s)) {
before = n;
i += 2;
continue;
}
}
"-C" | "--context" => {
if let Some(n) = args.get(i + 1).and_then(|s| parse_usize_token(s)) {
before = n;
after = n;
i += 2;
continue;
}
}
_ => {}
}
if let Some(body) = arg.strip_prefix("-A")
&& !body.is_empty()
&& let Some(n) = parse_usize_token(body)
{
after = n;
i += 1;
continue;
}
if let Some(body) = arg.strip_prefix("-B")
&& !body.is_empty()
&& let Some(n) = parse_usize_token(body)
{
before = n;
i += 1;
continue;
}
if let Some(body) = arg.strip_prefix("-C")
&& !body.is_empty()
&& let Some(n) = parse_usize_token(body)
{
before = n;
after = n;
i += 1;
continue;
}
i += 1;
}
(before, after)
}

fn resolve_heading_from_args(args: &[String]) -> bool {
let mut last_idx = 0usize;
let mut result = false;
Expand Down Expand Up @@ -607,6 +708,7 @@ impl SearchFlags {
flags,
case_mode: self.case_mode,
max_results: None,
..SearchOptions::default()
}
}
}
Expand Down Expand Up @@ -871,6 +973,7 @@ fn run_search(cli: &Cli) -> anyhow::Result<bool> {
let invert_match = resolve_invert_match_from_args(&args);
let glob_case_insensitive = resolve_glob_case_insensitive_from_args(&args);
let (hidden, ignore_sources, require_git) = resolve_visibility_and_ignore(&args);
let (before_context, after_context) = resolve_context_from_args(&args);
let heading = resolve_heading_from_args(&args);
let with_filename = resolve_with_filename_from_args(&args);

Expand All @@ -890,6 +993,12 @@ fn run_search(cli: &Cli) -> anyhow::Result<bool> {
if only_matching {
opts.flags |= SearchMatchFlags::ONLY_MATCHING;
}
opts.before_context = before_context;
opts.after_context = after_context;
if only_matching {
opts.before_context = 0;
opts.after_context = 0;
}

let effective_mode = if only_matching {
SearchMode::OnlyMatching
Expand Down
132 changes: 132 additions & 0 deletions crates/cli/tests/integration_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! Context lines (`-A` / `-B` / `-C`).

mod common;

use std::fs;

use common::{BuildIndexOptions, assert_success, command, fresh_dir, normalized_stdout};

#[test]
fn context_c_shows_surrounding_lines() {
let root = fresh_dir("integration-context-c");
fs::write(root.join("t.txt"), "alpha\nbeta match\ngamma\n").unwrap();
let sift_dir = root.join(".sift");
BuildIndexOptions::default().run(Some(&root), &sift_dir, std::path::Path::new("."));

let mut cmd = command(Some(&root));
cmd.arg("--sift-dir").arg(&sift_dir);
cmd.args(["-C", "1", "match", "t.txt"]);
let output = cmd.output().unwrap();
assert_success(&output);

let expected = "t.txt-1-alpha\nt.txt:2:beta match\nt.txt-3-gamma\n";
assert_eq!(normalized_stdout(&output), expected);
}

#[test]
fn context_a_shows_lines_after_match() {
let root = fresh_dir("integration-context-a");
fs::write(root.join("t.txt"), "alpha\nbeta match\ngamma\ndelta\n").unwrap();
let sift_dir = root.join(".sift");
BuildIndexOptions::default().run(Some(&root), &sift_dir, std::path::Path::new("."));

let mut cmd = command(Some(&root));
cmd.arg("--sift-dir").arg(&sift_dir);
cmd.args(["-A", "2", "match", "t.txt"]);
let output = cmd.output().unwrap();
assert_success(&output);

let expected = "t.txt:2:beta match\nt.txt-3-gamma\nt.txt-4-delta\n";
assert_eq!(normalized_stdout(&output), expected);
}

#[test]
fn context_b_shows_lines_before_match() {
let root = fresh_dir("integration-context-b");
fs::write(root.join("t.txt"), "alpha\nbeta match\ngamma\n").unwrap();
let sift_dir = root.join(".sift");
BuildIndexOptions::default().run(Some(&root), &sift_dir, std::path::Path::new("."));

let mut cmd = command(Some(&root));
cmd.arg("--sift-dir").arg(&sift_dir);
cmd.args(["-B", "2", "match", "t.txt"]);
let output = cmd.output().unwrap();
assert_success(&output);

let stdout = normalized_stdout(&output);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines.len(),
2,
"expected 2 lines (1 before + match), got: {lines:?}"
);
assert!(
lines[0].contains("alpha"),
"expected 'alpha' in line 1, got: {}",
lines[0]
);
assert!(
lines[1].contains("match"),
"expected 'match' in line 2, got: {}",
lines[1]
);
}

#[test]
fn context_break_separates_match_groups() {
let root = fresh_dir("integration-context-break");
fs::write(
root.join("t.txt"),
"line1 match\nline2 not\nline3 not\nline4 not\nline5 match\nline6 not\nline7 not\nline8 match\n",
)
.unwrap();
let sift_dir = root.join(".sift");
BuildIndexOptions::default().run(Some(&root), &sift_dir, std::path::Path::new("."));

let mut cmd = command(Some(&root));
cmd.arg("--sift-dir").arg(&sift_dir);
cmd.args(["-B", "1", "-A", "1", "match", "t.txt"]);
let output = cmd.output().unwrap();
assert_success(&output);

let stdout = normalized_stdout(&output);
let lines: Vec<&str> = stdout.lines().collect();
assert!(
lines.len() >= 7,
"expected at least 7 lines, got {}: {lines:?}",
lines.len()
);
}

#[test]
fn context_c_with_filename_uses_hyphen_separator() {
let root = fresh_dir("integration-context-filename");
fs::write(root.join("t.txt"), "alpha\nbeta match\ngamma\n").unwrap();
let sift_dir = root.join(".sift");
BuildIndexOptions::default().run(Some(&root), &sift_dir, std::path::Path::new("."));

let mut cmd = command(Some(&root));
cmd.arg("--sift-dir").arg(&sift_dir);
cmd.args(["-n", "-C", "1", "match", "t.txt"]);
let output = cmd.output().unwrap();
assert_success(&output);

let stdout = normalized_stdout(&output);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 3);
assert!(
lines[0].starts_with("t.txt-1-"),
"expected 't.txt-1-' prefix for context line, got: {}",
lines[0]
);
assert!(
lines[1].starts_with("t.txt:2:"),
"expected 't.txt:2:' prefix for match line, got: {}",
lines[1]
);
assert!(
lines[2].starts_with("t.txt-3-"),
"expected 't.txt-3-' prefix for context line, got: {}",
lines[2]
);
}
8 changes: 8 additions & 0 deletions crates/core/benches/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ fn bench_word_literal(c: &mut Criterion) {
flags: SearchMatchFlags::WORD_REGEXP,
case_mode: CaseMode::Sensitive,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -336,6 +337,7 @@ fn bench_line_literal(c: &mut Criterion) {
flags: SearchMatchFlags::LINE_REGEXP,
case_mode: CaseMode::Sensitive,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -366,6 +368,7 @@ fn bench_fixed_string(c: &mut Criterion) {
flags: SearchMatchFlags::FIXED_STRINGS,
case_mode: CaseMode::Sensitive,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -396,6 +399,7 @@ fn bench_casei_literal(c: &mut Criterion) {
flags: SearchMatchFlags::default(),
case_mode: CaseMode::Insensitive,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -426,6 +430,7 @@ fn bench_smart_case_lower(c: &mut Criterion) {
flags: SearchMatchFlags::default(),
case_mode: CaseMode::Smart,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -456,6 +461,7 @@ fn bench_smart_case_upper(c: &mut Criterion) {
flags: SearchMatchFlags::default(),
case_mode: CaseMode::Smart,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -574,6 +580,7 @@ fn bench_alternation_casei(c: &mut Criterion) {
flags: SearchMatchFlags::default(),
case_mode: CaseMode::Insensitive,
max_results: None,
..SearchOptions::default()
},
)
.unwrap();
Expand Down Expand Up @@ -999,6 +1006,7 @@ fn bench_max_count_1(c: &mut Criterion) {
flags: SearchMatchFlags::default(),
case_mode: CaseMode::Sensitive,
max_results: Some(1),
..SearchOptions::default()
},
)
.unwrap();
Expand Down
Loading
Loading