use sqry_core::graph::unified::persistence::GraphStorage;
use std::path::{Path, PathBuf};
const MAX_ANCESTOR_DEPTH: usize = 64;
pub const INDEX_FILE_NAME: &str = ".sqry-index";
const PATH_ESCAPE_CHARS: &[char] = &['*', '?', '[', ']', '{', '}', '\\'];
#[derive(Debug, Clone)]
pub struct IndexLocation {
pub index_root: PathBuf,
pub query_scope: PathBuf,
pub is_ancestor: bool,
pub is_file_query: bool,
pub requires_scope_filter: bool,
}
impl IndexLocation {
#[must_use]
pub fn relative_scope(&self) -> Option<PathBuf> {
if self.requires_scope_filter {
self.query_scope
.strip_prefix(&self.index_root)
.ok()
.map(Path::to_path_buf)
} else {
None
}
}
}
#[must_use]
pub fn find_nearest_index(start: &Path) -> Option<IndexLocation> {
let query_scope = start.to_path_buf();
let canonical_start = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
let (mut ancestor_dir, is_file_query) = if canonical_start.is_file() {
let parent = canonical_start
.parent()
.map_or_else(|| canonical_start.clone(), Path::to_path_buf);
(parent, true)
} else {
(canonical_start, false)
};
if ancestor_dir.is_relative()
&& let Ok(cwd) = std::env::current_dir()
{
ancestor_dir = cwd.join(&ancestor_dir);
}
for ancestor_depth in 0..MAX_ANCESTOR_DEPTH {
let storage = GraphStorage::new(&ancestor_dir);
if storage.exists() {
let is_ancestor = ancestor_depth > 0;
return Some(IndexLocation {
index_root: ancestor_dir,
query_scope: query_scope.canonicalize().unwrap_or(query_scope),
is_ancestor,
is_file_query,
requires_scope_filter: is_ancestor || is_file_query,
});
}
let legacy_index_path = ancestor_dir.join(INDEX_FILE_NAME);
if legacy_index_path.exists() && legacy_index_path.is_file() {
let is_ancestor = ancestor_depth > 0;
return Some(IndexLocation {
index_root: ancestor_dir,
query_scope: query_scope.canonicalize().unwrap_or(query_scope),
is_ancestor,
is_file_query,
requires_scope_filter: is_ancestor || is_file_query,
});
}
if !ancestor_dir.pop() {
break;
}
}
None
}
fn escape_path_for_query(path: &Path) -> String {
let path_str = path.to_string_lossy();
let mut escaped = String::with_capacity(path_str.len() + 20);
for ch in path_str.chars() {
if ch == '\\' && cfg!(windows) {
escaped.push('/');
continue;
}
if ch == '\\' {
escaped.push_str("\\\\\\\\");
} else if PATH_ESCAPE_CHARS.contains(&ch) {
escaped.push_str("\\\\");
escaped.push(ch);
} else {
escaped.push(ch);
}
}
escaped
}
fn path_needs_quoting(path: &Path) -> bool {
let path_str = path.to_string_lossy();
path_str
.chars()
.any(|c| c == ' ' || c == '"' || PATH_ESCAPE_CHARS.contains(&c))
}
#[must_use]
pub fn augment_query_with_scope(query: &str, relative_scope: &Path, is_file_query: bool) -> String {
if relative_scope.as_os_str().is_empty() {
return query.to_string();
}
let scope_pattern = if path_needs_quoting(relative_scope) {
let escaped_path = escape_path_for_query(relative_scope);
let quoted = escaped_path.replace('"', "\\\"");
if is_file_query {
format!("\"{quoted}\"")
} else {
format!("\"{quoted}/**\"")
}
} else {
let path_str = relative_scope.to_string_lossy();
if is_file_query {
path_str.into_owned()
} else {
format!("{path_str}/**")
}
};
let path_filter = format!("path:{scope_pattern}");
if query.trim().is_empty() {
path_filter
} else {
format!("({query}) AND {path_filter}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_index(path: &Path) {
let index_path = path.join(INDEX_FILE_NAME);
fs::write(&index_path, "test-index-marker").unwrap();
}
#[test]
fn find_nearest_index_at_current_dir() {
let tmp = TempDir::new().unwrap();
create_test_index(tmp.path());
let result = find_nearest_index(tmp.path());
assert!(result.is_some());
let loc = result.unwrap();
assert_eq!(loc.index_root, tmp.path().canonicalize().unwrap());
assert!(!loc.is_ancestor);
assert!(!loc.is_file_query);
assert!(!loc.requires_scope_filter);
}
#[test]
fn find_nearest_index_in_parent() {
let tmp = TempDir::new().unwrap();
create_test_index(tmp.path());
let subdir = tmp.path().join("src");
fs::create_dir(&subdir).unwrap();
let result = find_nearest_index(&subdir);
assert!(result.is_some());
let loc = result.unwrap();
assert_eq!(loc.index_root, tmp.path().canonicalize().unwrap());
assert!(loc.is_ancestor);
assert!(!loc.is_file_query);
assert!(loc.requires_scope_filter);
}
#[test]
fn find_nearest_index_in_grandparent() {
let tmp = TempDir::new().unwrap();
create_test_index(tmp.path());
let deep = tmp.path().join("src").join("utils");
fs::create_dir_all(&deep).unwrap();
let result = find_nearest_index(&deep);
assert!(result.is_some());
let loc = result.unwrap();
assert_eq!(loc.index_root, tmp.path().canonicalize().unwrap());
assert!(loc.is_ancestor);
assert!(loc.requires_scope_filter);
}
#[test]
fn find_nearest_index_none_found() {
let tmp = TempDir::new().unwrap();
let result = find_nearest_index(tmp.path());
match &result {
None => {} Some(loc) => {
let tmp_canonical = tmp.path().canonicalize().unwrap();
assert!(
!loc.index_root.starts_with(&tmp_canonical),
"found unexpected index inside temp dir: {:?}",
loc.index_root
);
}
}
}
#[test]
fn find_nearest_index_nested_repos() {
let tmp = TempDir::new().unwrap();
create_test_index(tmp.path());
let inner = tmp.path().join("packages").join("web");
fs::create_dir_all(&inner).unwrap();
create_test_index(&inner);
let query_path = inner.join("src");
fs::create_dir(&query_path).unwrap();
let result = find_nearest_index(&query_path);
assert!(result.is_some());
let loc = result.unwrap();
assert_eq!(loc.index_root, inner.canonicalize().unwrap());
assert!(loc.is_ancestor);
}
#[test]
fn find_nearest_index_file_input() {
let tmp = TempDir::new().unwrap();
create_test_index(tmp.path());
let subdir = tmp.path().join("src");
fs::create_dir(&subdir).unwrap();
let file = subdir.join("main.rs");
fs::write(&file, "fn main() {}").unwrap();
let result = find_nearest_index(&file);
assert!(result.is_some());
let loc = result.unwrap();
assert!(loc.is_file_query);
assert!(loc.is_ancestor); assert!(loc.requires_scope_filter);
}
#[test]
fn find_nearest_index_file_in_index_dir() {
let tmp = TempDir::new().unwrap();
create_test_index(tmp.path());
let file = tmp.path().join("main.rs");
fs::write(&file, "fn main() {}").unwrap();
let result = find_nearest_index(&file);
assert!(result.is_some());
let loc = result.unwrap();
assert!(!loc.is_ancestor); assert!(loc.is_file_query);
assert!(loc.requires_scope_filter); }
#[test]
fn relative_scope_calculation() {
let loc = IndexLocation {
index_root: PathBuf::from("/project"),
query_scope: PathBuf::from("/project/src/utils"),
is_ancestor: true,
is_file_query: false,
requires_scope_filter: true,
};
let scope = loc.relative_scope();
assert_eq!(scope, Some(PathBuf::from("src/utils")));
}
#[test]
fn relative_scope_same_dir() {
let loc = IndexLocation {
index_root: PathBuf::from("/project"),
query_scope: PathBuf::from("/project"),
is_ancestor: false,
is_file_query: false,
requires_scope_filter: false,
};
let scope = loc.relative_scope();
assert!(scope.is_none());
}
#[test]
fn relative_scope_file_in_root() {
let loc = IndexLocation {
index_root: PathBuf::from("/project"),
query_scope: PathBuf::from("/project/main.rs"),
is_ancestor: false,
is_file_query: true,
requires_scope_filter: true,
};
let scope = loc.relative_scope();
assert_eq!(scope, Some(PathBuf::from("main.rs")));
}
#[test]
fn augment_query_with_scope_basic() {
let result = augment_query_with_scope("kind:function", Path::new("src"), false);
assert_eq!(result, "(kind:function) AND path:src/**");
}
#[test]
fn augment_query_with_scope_empty_query() {
let result = augment_query_with_scope("", Path::new("src"), false);
assert_eq!(result, "path:src/**");
}
#[test]
fn augment_query_with_scope_empty_path() {
let result = augment_query_with_scope("kind:fn", Path::new(""), false);
assert_eq!(result, "kind:fn");
}
#[test]
fn augment_query_with_scope_file_query() {
let result = augment_query_with_scope("kind:function", Path::new("src/main.rs"), true);
assert_eq!(result, "(kind:function) AND path:src/main.rs");
}
#[test]
fn augment_query_with_scope_directory_query() {
let result = augment_query_with_scope("kind:function", Path::new("src"), false);
assert_eq!(result, "(kind:function) AND path:src/**");
}
#[test]
fn augment_query_file_with_spaces() {
let result =
augment_query_with_scope("kind:function", Path::new("my project/main.rs"), true);
assert_eq!(result, "(kind:function) AND path:\"my project/main.rs\"");
}
#[test]
fn augment_query_with_scope_path_with_spaces() {
let result = augment_query_with_scope("kind:function", Path::new("my project/src"), false);
assert_eq!(result, "(kind:function) AND path:\"my project/src/**\"");
}
#[test]
fn augment_query_with_scope_path_with_glob_chars() {
let result = augment_query_with_scope("kind:function", Path::new("src/[test]"), false);
assert_eq!(result, "(kind:function) AND path:\"src/\\\\[test\\\\]/**\"");
}
#[test]
fn augment_query_preserves_precedence() {
let result = augment_query_with_scope("kind:fn OR kind:method", Path::new("src"), false);
assert_eq!(result, "(kind:fn OR kind:method) AND path:src/**");
}
#[test]
fn augment_query_with_existing_path_predicate() {
let result =
augment_query_with_scope("kind:fn AND path:*.rs", Path::new("src/utils"), false);
assert_eq!(result, "(kind:fn AND path:*.rs) AND path:src/utils/**");
}
#[test]
#[cfg(unix)]
fn escape_path_with_backslash_on_unix() {
let result = escape_path_for_query(Path::new("src/file\\name"));
assert_eq!(result, "src/file\\\\\\\\name");
}
#[test]
fn augmented_queries_are_parseable() {
use sqry_core::query::Lexer;
let test_cases = [
("kind:fn", Path::new("src"), false),
("kind:fn", Path::new("my project/src"), false),
("kind:fn", Path::new("src/[test]"), false),
("kind:fn", Path::new("src/test*"), false),
("kind:fn", Path::new("src/test?"), false),
("kind:fn", Path::new("src/{a,b}"), false),
("kind:fn", Path::new("src/main.rs"), true),
("kind:fn", Path::new("src/[test]/main.rs"), true),
("kind:fn OR kind:method", Path::new("src/[utils]"), false),
];
for (query, path, is_file) in test_cases {
let augmented = augment_query_with_scope(query, path, is_file);
let mut lexer = Lexer::new(&augmented);
let result = lexer.tokenize();
assert!(
result.is_ok(),
"Failed to parse augmented query for path {:?}: {:?}\nQuery: {}",
path,
result.err(),
augmented
);
}
}
}