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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ jobs:
- name: Check formatting
run: cargo fmt --check

# TODO(REF-12): upgrade to -D warnings once clippy cleanup is complete.
- name: Clippy
run: cargo clippy --all-targets
run: cargo clippy --all-targets -- -D warnings

- name: Run tests
run: cargo test --all

- name: Run performance tests (release build)
if: matrix.os == 'ubuntu-latest'
run: cargo test --release -- --ignored --test performance_test
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ rfx query "(function_item) @fn" --ast --lang rust --glob "src/**/*.rs"

## Supported Languages

**18 languages** with full symbol extraction support (functions, classes, variables, etc.):
**15 languages** with full symbol extraction support (functions, classes, variables, etc.):

- **Systems**: Rust, C, C++, Zig
- **Backend**: Python, Go, Java, C#, PHP, Ruby, Kotlin
- **Frontend**: TypeScript, JavaScript, Vue, Svelte
- **Swift**: Temporarily disabled (tree-sitter version incompatibility)
- **Swift**: Temporarily disabled — `rfx query --lang swift` emits a warning; full-text search still works but symbol queries return no results (tree-sitter-swift 0.7.x grammar incompatibility)

**Symbol extraction**: Functions, classes, methods, variables (global + local), interfaces, traits, enums, attributes/annotations, and more.

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
Reflex is a local-first, full-text code search engine. Use it from the command line, pipe it into scripts, or connect it to AI coding assistants (Claude Code, Cursor, and any MCP-compatible tool) for instant symbol lookup, dependency analysis, and codebase exploration — fully offline, fully deterministic, no cloud required.

[![CI](https://github.com/reflex-search/reflex/actions/workflows/ci.yml/badge.svg)](https://github.com/reflex-search/reflex/actions/workflows/ci.yml)
[![Tests](https://img.shields.io/badge/tests-347%20passing-brightgreen)]()
[![License](https://img.shields.io/badge/license-MIT-blue)]()
[![MCP Quickstart](https://img.shields.io/badge/MCP-quickstart-blue)](docs/ai-agent-integration.md)

Expand Down Expand Up @@ -85,7 +84,9 @@ When connected via MCP, your AI assistant gets these tools:
| `count_occurrences` | Quick match statistics without full content |
| `search_regex` | Regex pattern matching across the codebase |
| `search_ast` | Structure-aware search via Tree-sitter AST queries |
| `find_references` | Symbol definition + all usage sites in a single call; the primary code-navigation tool for AI agents |
| `index_project` | Trigger or refresh the search index |
| `check_index_status` | Check whether the index is fresh, stale, or missing; call before any search session or after git operations |
| `get_dependencies` | All imports for a specific file |
| `get_dependents` | All files that import a given file (reverse lookup) |
| `get_transitive_deps` | Transitive dependency graph up to a configurable depth |
Expand Down Expand Up @@ -154,7 +155,7 @@ rfx index # Build / update the search index
rfx index status # Background indexing status
rfx watch # Auto-reindex on file changes
rfx stats # Index statistics
rfx pulse digest # Codebase change digest
rfx pulse changelog # Codebase change digest
rfx pulse wiki # Per-module documentation
rfx pulse map # Architecture diagram (Mermaid / D2)
rfx serve --port 7878 # Local HTTP API server
Expand Down Expand Up @@ -190,6 +191,8 @@ Full symbol extraction (functions, classes, methods, types, etc.) for 15 languag
**Backend:** Python, Go, Java, C#, PHP, Ruby, Kotlin
**Frontend:** TypeScript, JavaScript, Vue, Svelte

> **Swift** is temporarily disabled (tree-sitter-swift 0.7.x grammar incompatibility). `rfx query --lang swift` emits a warning; full-text search still works.

Full-text search works on **all file types** regardless of parser support.

---
Expand Down
1 change: 0 additions & 1 deletion benches/trigram_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use reflex::trigram::{
};
use reflex::{CacheManager, Indexer, QueryEngine, QueryFilter};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;

// ─── helpers ──────────────────────────────────────────────────────────────────
Expand Down
20 changes: 10 additions & 10 deletions src/background_indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,12 @@ impl BackgroundIndexer {
});

// Write batch to cache (sequential - SQLite limitation)
if !parsed_results.is_empty() {
if let Err(e) = symbol_cache.batch_set(&parsed_results) {
log::error!("Failed to write symbol batch: {}", e);
let mut status = status_mutex.lock().unwrap();
status.2 += parsed_results.len();
}
if !parsed_results.is_empty()
&& let Err(e) = symbol_cache.batch_set(&parsed_results)
{
log::error!("Failed to write symbol batch: {}", e);
let mut status = status_mutex.lock().unwrap();
status.2 += parsed_results.len();
}

// Update status counters
Expand All @@ -451,10 +451,10 @@ impl BackgroundIndexer {
}

// Write status every batch
if processed % 500 < batch_size {
if let Err(e) = self.write_status() {
log::warn!("Failed to write status: {}", e);
}
if processed % 500 < batch_size
&& let Err(e) = self.write_status()
{
log::warn!("Failed to write status: {}", e);
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,10 +548,10 @@ provider = "openrouter" # Options: openai, anthropic, openrouter
}
}

if let Some(perf) = toml_val.get("performance") {
if let Some(threads) = perf.get("parallel_threads").and_then(|v| v.as_integer()) {
cfg.parallel_threads = threads as usize;
}
if let Some(perf) = toml_val.get("performance")
&& let Some(threads) = perf.get("parallel_threads").and_then(|v| v.as_integer())
{
cfg.parallel_threads = threads as usize;
}

log::debug!("Loaded IndexConfig from config.toml: {:?}", cfg);
Expand Down Expand Up @@ -2175,7 +2175,7 @@ mod tests {
assert_eq!(info.branch, "main");
assert_eq!(info.commit_sha, "commit123");
assert_eq!(info.file_count, 10);
assert_eq!(info.is_dirty, false);
assert!(!info.is_dirty);
}

#[test]
Expand All @@ -2189,7 +2189,7 @@ mod tests {
.unwrap();

let info = cache.get_branch_info("feature").unwrap();
assert_eq!(info.is_dirty, true);
assert!(info.is_dirty);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions src/cli/ask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use owo_colors::OwoColorize;
use std::sync::{Arc, Mutex};

/// Handle the `ask` command
#[allow(clippy::too_many_arguments)]
pub(super) fn handle_ask(
question: Option<String>,
auto_execute: bool,
Expand Down
26 changes: 14 additions & 12 deletions src/cli/deps.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::cache::CacheManager;
use crate::output;
use anyhow::Result;
use std::path::PathBuf;

Expand Down Expand Up @@ -283,7 +282,7 @@ pub(super) fn handle_deps(
let dependents = deps_index.get_dependents(file_id)?;
let paths = deps_index.get_file_paths(&dependents)?;

match format.as_ref() {
match format {
"json" => {
// Expose only paths (file_id is an internal detail)
let output: Vec<_> = dependents.iter().filter_map(|id| paths.get(id)).collect();
Expand Down Expand Up @@ -327,7 +326,7 @@ pub(super) fn handle_deps(
// Direct dependencies only
let deps = deps_index.get_dependencies(file_id)?;

match format.as_ref() {
match format {
"json" => {
let output: Vec<_> = deps
.iter()
Expand Down Expand Up @@ -399,7 +398,7 @@ pub(super) fn handle_deps(
let file_ids: Vec<_> = transitive.keys().copied().collect();
let paths = deps_index.get_file_paths(&file_ids)?;

match format.as_ref() {
match format {
"json" => {
let output: Vec<_> = transitive
.iter()
Expand Down Expand Up @@ -427,7 +426,7 @@ pub(super) fn handle_deps(
std::collections::BTreeMap::new();
for (id, d) in &transitive {
if *d > 0 {
by_depth.entry(*d).or_insert_with(Vec::new).push(*id);
by_depth.entry(*d).or_default().push(*id);
}
}

Expand Down Expand Up @@ -469,6 +468,7 @@ pub(super) fn handle_deps(
}

/// Handle --circular flag (detect cycles)
#[allow(clippy::too_many_arguments)]
fn handle_deps_circular(
deps_index: &crate::dependency::DependencyIndex,
format: &str,
Expand Down Expand Up @@ -594,10 +594,10 @@ fn handle_deps_circular(
}
}
// Show cycle completion
if let Some(first_id) = cycle.first() {
if let Some(path) = paths.get(first_id) {
println!(" → {} (cycle completes)", path);
}
if let Some(first_id) = cycle.first()
&& let Some(path) = paths.get(first_id)
{
println!(" → {} (cycle completes)", path);
}
}
if total_count > count {
Expand Down Expand Up @@ -641,6 +641,7 @@ fn handle_deps_circular(
}

/// Handle --hotspots flag (most-imported files)
#[allow(clippy::too_many_arguments)]
fn handle_deps_hotspots(
deps_index: &crate::dependency::DependencyIndex,
format: &str,
Expand All @@ -660,11 +661,11 @@ fn handle_deps_hotspots(
match sort_order {
"asc" => {
// Ascending: least imports first
all_hotspots.sort_by(|a, b| a.1.cmp(&b.1));
all_hotspots.sort_by_key(|a| a.1);
}
"desc" => {
// Descending: most imports first (default)
all_hotspots.sort_by(|a, b| b.1.cmp(&a.1));
all_hotspots.sort_by_key(|a| std::cmp::Reverse(a.1));
}
_ => {
anyhow::bail!("Invalid sort order '{}'. Supported: asc, desc", sort_order);
Expand Down Expand Up @@ -927,6 +928,7 @@ fn handle_deps_unused(
}

/// Handle --islands flag (disconnected components)
#[allow(clippy::too_many_arguments)]
fn handle_deps_islands(
deps_index: &crate::dependency::DependencyIndex,
format: &str,
Expand All @@ -944,7 +946,7 @@ fn handle_deps_islands(

// Get total file count from the cache for percentage calculation
let cache = deps_index.get_cache();
let total_files = cache.stats()?.total_files as usize;
let total_files = cache.stats()?.total_files;

// Calculate max_island_size default: min of 500 or 50% of total files
let max_size = max_island_size.unwrap_or_else(|| {
Expand Down
20 changes: 10 additions & 10 deletions src/cli/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,18 +246,17 @@ pub(super) fn handle_list_files(
.into_iter()
.filter(|f| {
// Language filter: compare lowercase language name
if let Some(ref wanted_lang) = lang_name_filter {
if !f.language.to_lowercase().starts_with(wanted_lang.as_str())
&& f.language.to_lowercase() != *wanted_lang
{
return false;
}
if let Some(ref wanted_lang) = lang_name_filter
&& !f.language.to_lowercase().starts_with(wanted_lang.as_str())
&& f.language.to_lowercase() != *wanted_lang
{
return false;
}
// Glob filter
if let Some(ref gs) = glob_set {
if !gs.is_match(&f.path) {
return false;
}
if let Some(ref gs) = glob_set
&& !gs.is_match(&f.path)
{
return false;
}
true
})
Expand Down Expand Up @@ -292,6 +291,7 @@ pub(super) fn handle_mcp() -> Result<()> {
}

/// Handle the `context` command
#[allow(clippy::too_many_arguments)]
pub(super) fn handle_context(
structure: bool,
path: Option<String>,
Expand Down
27 changes: 13 additions & 14 deletions src/cli/pulse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ pub(super) fn handle_pulse_map(
Ok(())
}

#[allow(clippy::too_many_arguments)]
pub(super) fn handle_pulse_generate(
output: PathBuf,
base_url: String,
Expand Down Expand Up @@ -376,20 +377,18 @@ pub(super) fn handle_pulse_onboard(no_llm: bool, json: bool) -> Result<()> {
)?;
let mut data = crate::pulse::onboard::generate_onboard_structural(&cache, modules.len())?;

if !no_llm {
if let Ok(provider) = crate::pulse::narrate::create_pulse_provider() {
let llm_cache = crate::pulse::llm_cache::LlmCache::new(cache.path());
let ctx = crate::pulse::onboard::build_onboard_context(&data);
let narration = crate::pulse::narrate::narrate_section(
&*provider,
crate::pulse::narrate::onboard_system_prompt(),
&ctx,
&llm_cache,
"standalone",
"onboard-guide",
);
data.narration = narration;
}
if !no_llm && let Ok(provider) = crate::pulse::narrate::create_pulse_provider() {
let llm_cache = crate::pulse::llm_cache::LlmCache::new(cache.path());
let ctx = crate::pulse::onboard::build_onboard_context(&data);
let narration = crate::pulse::narrate::narrate_section(
&*provider,
crate::pulse::narrate::onboard_system_prompt(),
&ctx,
&llm_cache,
"standalone",
"onboard-guide",
);
data.narration = narration;
}

if json {
Expand Down
31 changes: 23 additions & 8 deletions src/cli/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use crate::cache::CacheManager;
use crate::models::Language;
use crate::query::{QueryEngine, QueryFilter};
use anyhow::Result;
use indicatif::{ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
use std::time::Instant;

Expand All @@ -28,6 +27,7 @@ pub fn truncate_preview(preview: &str, max_length: usize) -> String {
}

/// Handle the `query` subcommand
#[allow(clippy::too_many_arguments)]
pub(super) fn handle_query(
pattern: String,
symbols_flag: bool,
Expand Down Expand Up @@ -78,6 +78,25 @@ pub(super) fn handle_query(
None
};

// Warn when Swift is requested — symbol queries will return no results
if language == Some(Language::Swift) {
eprintln!(
"{}: Swift symbol extraction is temporarily disabled (tree-sitter-swift 0.7.x grammar incompatibility). \
Full-text search will still work, but --symbols queries will return no results.",
"Warning".yellow().bold()
);
}

// Warn when --dependencies is used with a non-Rust language filter (REF-171)
if include_dependencies && matches!(language, Some(l) if l != Language::Rust) {
eprintln!(
"{}: --dependencies is currently only supported for Rust files. \
No dependency data will be included for {} files.",
"Warning".yellow().bold(),
lang.as_deref().unwrap_or("non-Rust")
);
}

// Parse and validate symbol kind — error on unrecognised values (REF-60)
let kind = if let Some(s) = kind_str.as_deref() {
let capitalized = {
Expand Down Expand Up @@ -123,12 +142,8 @@ pub(super) fn handle_query(
// 3. If --paths is set and user didn't specify --limit: no limit (None)
// 4. If user specified --limit: use that value
// 5. Otherwise: use default limit of 100
let final_limit = if count_only {
None // --count always shows total count, no pagination
} else if all {
None // --all means no limit
} else if paths_only && limit.is_none() {
None // --paths without explicit --limit means no limit
let final_limit = if count_only || all || (paths_only && limit.is_none()) {
None // --count, --all, and --paths (without explicit --limit) all remove the result limit
} else if let Some(user_limit) = limit {
Some(user_limit) // Use user-specified limit
} else {
Expand Down Expand Up @@ -537,7 +552,7 @@ pub(super) fn handle_query(
(&content_reader_opt, file_id_for_context)
{
reader
.get_context_by_line(fid as u32, r.span.start_line, 3)
.get_context_by_line(fid, r.span.start_line, 3)
.unwrap_or_else(|_| (vec![], vec![]))
} else {
(vec![], vec![])
Expand Down
Loading
Loading