mirror of
https://github.com/rtk-ai/rtk.git
synced 2026-09-19 07:33:17 +08:00
a240d1a1ee
Add support for passing additional ripgrep arguments to rtk grep while preserving RTK's token-optimized output formatting. Changes: - src/main.rs: Add extra_args field to Grep command variant - src/main.rs: Pass extra_args to grep_cmd::run() - src/grep_cmd.rs: Add extra_args parameter to run() signature - src/grep_cmd.rs: Inject extra_args into ripgrep command - Unit test: test_extra_args_accepted verifies parameter exists - Smoke tests: 2 assertions for -i and -A flags Usage note: Extra args must come AFTER pattern and path: ✓ rtk grep "pattern" src/ -i ✗ rtk grep "pattern" -i (clap interprets -i as path) This enables full ripgrep functionality (case-insensitive, context lines, word boundaries, globs) while maintaining RTK's compact output. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
210 lines
6.1 KiB
Rust
210 lines
6.1 KiB
Rust
//! tree command - proxy to native tree with token-optimized output
|
|
//!
|
|
//! This module proxies to the native `tree` command and filters the output
|
|
//! to reduce token usage while preserving structure visibility.
|
|
//!
|
|
//! Token optimization: automatically excludes noise directories via -I pattern
|
|
//! unless -a flag is present (respecting user intent).
|
|
|
|
use crate::tracking;
|
|
use anyhow::{Context, Result};
|
|
use std::process::Command;
|
|
|
|
/// Noise directories commonly excluded from LLM context
|
|
const NOISE_DIRS: &[&str] = &[
|
|
"node_modules",
|
|
".git",
|
|
"target",
|
|
"__pycache__",
|
|
".next",
|
|
"dist",
|
|
"build",
|
|
".cache",
|
|
".turbo",
|
|
".vercel",
|
|
".pytest_cache",
|
|
".mypy_cache",
|
|
".tox",
|
|
".venv",
|
|
"venv",
|
|
"env",
|
|
".env",
|
|
"coverage",
|
|
".nyc_output",
|
|
".DS_Store",
|
|
"Thumbs.db",
|
|
".idea",
|
|
".vscode",
|
|
".vs",
|
|
"*.egg-info",
|
|
".eggs",
|
|
];
|
|
|
|
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
|
let timer = tracking::TimedExecution::start();
|
|
|
|
// Check if tree is installed
|
|
let tree_check = Command::new("which").arg("tree").output();
|
|
if tree_check.is_err() || !tree_check.unwrap().status.success() {
|
|
anyhow::bail!(
|
|
"tree command not found. Install it first:\n\
|
|
- macOS: brew install tree\n\
|
|
- Ubuntu/Debian: sudo apt install tree\n\
|
|
- Fedora/RHEL: sudo dnf install tree\n\
|
|
- Arch: sudo pacman -S tree"
|
|
);
|
|
}
|
|
|
|
let mut cmd = Command::new("tree");
|
|
|
|
// Determine if user wants all files or default behavior
|
|
let show_all = args.iter().any(|a| a == "-a" || a == "--all");
|
|
let has_ignore = args.iter().any(|a| a == "-I" || a.starts_with("--ignore="));
|
|
|
|
// Auto-inject -I pattern unless user wants all or already specified -I
|
|
if !show_all && !has_ignore {
|
|
let ignore_pattern = NOISE_DIRS.join("|");
|
|
cmd.arg("-I").arg(&ignore_pattern);
|
|
}
|
|
|
|
// Pass all user args
|
|
for arg in args {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
let output = cmd.output().context("Failed to run tree")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
eprint!("{}", stderr);
|
|
std::process::exit(output.status.code().unwrap_or(1));
|
|
}
|
|
|
|
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let filtered = filter_tree_output(&raw);
|
|
|
|
if verbose > 0 {
|
|
eprintln!(
|
|
"Lines: {} → {} ({}% reduction)",
|
|
raw.lines().count(),
|
|
filtered.lines().count(),
|
|
if raw.lines().count() > 0 {
|
|
100 - (filtered.lines().count() * 100 / raw.lines().count())
|
|
} else {
|
|
0
|
|
}
|
|
);
|
|
}
|
|
|
|
print!("{}", filtered);
|
|
timer.track("tree", "rtk tree", &raw, &filtered);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn filter_tree_output(raw: &str) -> String {
|
|
let lines: Vec<&str> = raw.lines().collect();
|
|
|
|
if lines.is_empty() {
|
|
return "\n".to_string();
|
|
}
|
|
|
|
let mut filtered_lines = Vec::new();
|
|
|
|
for line in lines {
|
|
// Skip the final summary line (e.g., "5 directories, 23 files")
|
|
if line.contains("director") && line.contains("file") {
|
|
continue;
|
|
}
|
|
|
|
// Skip empty lines at the end
|
|
if line.trim().is_empty() && filtered_lines.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
filtered_lines.push(line);
|
|
}
|
|
|
|
// Remove trailing empty lines
|
|
while filtered_lines.last().map_or(false, |l| l.trim().is_empty()) {
|
|
filtered_lines.pop();
|
|
}
|
|
|
|
filtered_lines.join("\n") + "\n"
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_filter_removes_summary() {
|
|
let input = ".\n├── src\n│ └── main.rs\n└── Cargo.toml\n\n2 directories, 3 files\n";
|
|
let output = filter_tree_output(input);
|
|
assert!(!output.contains("directories"));
|
|
assert!(!output.contains("files"));
|
|
assert!(output.contains("main.rs"));
|
|
assert!(output.contains("Cargo.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_preserves_structure() {
|
|
let input = ".\n├── src\n│ ├── main.rs\n│ └── lib.rs\n└── tests\n └── test.rs\n";
|
|
let output = filter_tree_output(input);
|
|
assert!(output.contains("├──"));
|
|
assert!(output.contains("│"));
|
|
assert!(output.contains("└──"));
|
|
assert!(output.contains("main.rs"));
|
|
assert!(output.contains("test.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_handles_empty() {
|
|
let input = "";
|
|
let output = filter_tree_output(input);
|
|
assert_eq!(output, "\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_removes_trailing_empty_lines() {
|
|
let input = ".\n├── file.txt\n\n\n";
|
|
let output = filter_tree_output(input);
|
|
assert_eq!(output.matches('\n').count(), 2); // Root + file.txt + final newline
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_summary_variations() {
|
|
// Test different summary formats
|
|
let inputs = vec![
|
|
(".\n└── file.txt\n\n0 directories, 1 file\n", "1 file"),
|
|
(".\n└── file.txt\n\n1 directory, 0 files\n", "1 directory"),
|
|
(".\n└── file.txt\n\n10 directories, 25 files\n", "25 files"),
|
|
];
|
|
|
|
for (input, summary_fragment) in inputs {
|
|
let output = filter_tree_output(input);
|
|
assert!(
|
|
!output.contains(summary_fragment),
|
|
"Should remove summary '{}' from output",
|
|
summary_fragment
|
|
);
|
|
assert!(
|
|
output.contains("file.txt"),
|
|
"Should preserve file.txt in output"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_noise_dirs_constant() {
|
|
// Verify NOISE_DIRS contains expected patterns
|
|
assert!(NOISE_DIRS.contains(&"node_modules"));
|
|
assert!(NOISE_DIRS.contains(&".git"));
|
|
assert!(NOISE_DIRS.contains(&"target"));
|
|
assert!(NOISE_DIRS.contains(&"__pycache__"));
|
|
assert!(NOISE_DIRS.contains(&".next"));
|
|
assert!(NOISE_DIRS.contains(&"dist"));
|
|
assert!(NOISE_DIRS.contains(&"build"));
|
|
}
|
|
}
|