use glob::glob;
use ignore::WalkBuilder;
use probe_code::language::is_test_file;
use probe_code::path_resolver::resolve_path;
use regex::Regex;
use std::collections::HashSet;
use std::path::PathBuf;
pub type FilePathInfo = (
PathBuf,
Option<usize>,
Option<usize>,
Option<String>,
Option<HashSet<usize>>,
);
pub fn is_git_diff_format(content: &str) -> bool {
content.trim_start().starts_with("diff --git")
}
pub fn extract_file_paths_from_git_diff(text: &str, allow_tests: bool) -> Vec<FilePathInfo> {
let mut results = Vec::new();
let mut processed_files = HashSet::new();
let mut current_file: Option<PathBuf> = None;
let mut current_file_lines = HashSet::new();
let debug_mode = std::env::var("DEBUG").unwrap_or_default() == "1";
let lines: Vec<&str> = text.lines().collect();
let diff_header_regex = Regex::new(r"^diff --git a/(.*) b/(.*)$").unwrap();
let hunk_header_regex = Regex::new(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@").unwrap();
let finalize_file = |results: &mut Vec<FilePathInfo>,
processed_files: &mut HashSet<String>,
file_path: &PathBuf,
changed_lines: &HashSet<usize>,
allow_tests: bool,
debug_mode: bool| {
if !changed_lines.is_empty()
&& !processed_files.contains(&file_path.to_string_lossy().to_string())
{
let is_test = is_test_file(file_path);
if !is_ignored_by_gitignore(file_path) && (allow_tests || !is_test) {
if debug_mode {
println!(
"[DEBUG] Adding file with {} changed lines: {:?}",
changed_lines.len(),
file_path
);
}
let start_line = changed_lines.iter().min().cloned();
let end_line = changed_lines.iter().max().cloned();
results.push((
file_path.clone(),
start_line,
end_line,
None,
Some(changed_lines.clone()),
));
processed_files.insert(file_path.to_string_lossy().to_string());
} else if debug_mode {
if is_ignored_by_gitignore(file_path) {
println!("[DEBUG] Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("[DEBUG] Skipping test file: {file_path:?}");
}
}
}
};
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if let Some(cap) = diff_header_regex.captures(line) {
if let Some(file_path) = ¤t_file {
finalize_file(
&mut results,
&mut processed_files,
file_path,
¤t_file_lines,
allow_tests,
debug_mode,
);
}
let file_path = cap.get(2).unwrap().as_str();
current_file = Some(PathBuf::from(file_path));
current_file_lines = HashSet::new();
if debug_mode {
println!("[DEBUG] Found file in git diff: {file_path:?}");
}
i += 1;
continue;
}
else if let Some(cap) = hunk_header_regex.captures(line) {
if let Some(file_path) = ¤t_file {
let new_start: usize = cap.get(3).unwrap().as_str().parse().unwrap_or(1);
let _new_len: usize = cap
.get(4)
.map(|m| m.as_str().parse().unwrap_or(1))
.unwrap_or(1);
if debug_mode {
println!(
"[DEBUG] Found hunk for file {file_path:?}: parsing for actual changed lines"
);
}
i += 1;
let mut current_line = new_start;
while i < lines.len() {
let hunk_line = lines[i];
if hunk_line.starts_with("@@") || hunk_line.starts_with("diff --git") {
break;
}
if hunk_line.starts_with('+') && !hunk_line.starts_with("+++") {
if debug_mode {
println!("[DEBUG] Found changed line at {current_line}: {hunk_line}");
}
current_file_lines.insert(current_line);
}
if !hunk_line.starts_with('-') {
current_line += 1;
}
i += 1;
}
continue;
}
}
i += 1;
}
if let Some(file_path) = ¤t_file {
finalize_file(
&mut results,
&mut processed_files,
file_path,
¤t_file_lines,
allow_tests,
debug_mode,
);
}
results
}
pub fn extract_file_paths_from_text(text: &str, allow_tests: bool) -> Vec<FilePathInfo> {
let mut results = Vec::new();
let mut processed_paths = HashSet::new();
let debug_mode = std::env::var("DEBUG").unwrap_or_default() == "1";
let mut preprocessed_text = String::with_capacity(text.len());
let mut in_quote = false;
let mut quote_char = ' ';
let mut prev_char = ' ';
for (i, c) in text.chars().enumerate() {
let next_char = text.chars().nth(i + 1).unwrap_or(' ');
let is_apostrophe_in_word =
c == '\'' && prev_char.is_alphanumeric() && next_char.is_alphanumeric();
if !in_quote && (c == '`' || c == '"' || (c == '\'' && !is_apostrophe_in_word)) {
in_quote = true;
quote_char = c;
preprocessed_text.push(' '); } else if in_quote && c == quote_char {
in_quote = false;
preprocessed_text.push(' '); } else {
preprocessed_text.push(c);
}
prev_char = c;
}
let text = &preprocessed_text;
let file_symbol_regex =
Regex::new(r"(?:^|[\s\r\n])([a-zA-Z0-9_\-./\*\{\}]+\.[a-zA-Z0-9]+)#([a-zA-Z0-9_]+)")
.unwrap();
for cap in file_symbol_regex.captures_iter(text) {
let file_path = cap.get(1).unwrap().as_str();
let symbol = cap.get(2).unwrap().as_str();
if file_path.contains('*') || file_path.contains('{') {
if let Ok(paths) = glob(file_path) {
for entry in paths.flatten() {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
let path_str = entry.to_string_lossy().to_string();
processed_paths.insert(path_str.clone());
results.push((entry, None, None, Some(symbol.to_string()), None));
} else if debug_mode {
if is_ignored_by_gitignore(&entry) {
println!("DEBUG: Skipping ignored file: {entry:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {entry:?}");
}
}
}
}
} else {
match resolve_path(file_path) {
Ok(resolved_path) => {
let is_test = is_test_file(&resolved_path);
if !is_ignored_by_gitignore(&resolved_path) && (allow_tests || !is_test) {
processed_paths.insert(file_path.to_string());
results.push((resolved_path, None, None, Some(symbol.to_string()), None));
} else if debug_mode {
if is_ignored_by_gitignore(&resolved_path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
Err(err) => {
if debug_mode {
println!("DEBUG: Failed to resolve path '{file_path}': {err}");
}
let path = PathBuf::from(file_path);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
processed_paths.insert(file_path.to_string());
results.push((path, None, None, Some(symbol.to_string()), None));
} else if debug_mode {
if is_ignored_by_gitignore(&path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
}
}
}
let file_range_regex =
Regex::new(r"(?:^|[\s\r\n])([a-zA-Z0-9_\-./\*\{\}]+\.[a-zA-Z0-9]+):(\d+)-(\d+)").unwrap();
for cap in file_range_regex.captures_iter(text) {
let file_path = cap.get(1).unwrap().as_str();
if processed_paths.contains(file_path) {
continue;
}
let start_line = cap.get(2).and_then(|m| m.as_str().parse::<usize>().ok());
let end_line = cap.get(3).and_then(|m| m.as_str().parse::<usize>().ok());
if let (Some(start), Some(end)) = (start_line, end_line) {
if file_path.contains('*') || file_path.contains('{') {
if let Ok(paths) = glob(file_path) {
for entry in paths.flatten() {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
processed_paths.insert(entry.to_string_lossy().to_string());
results.push((entry, Some(start), Some(end), None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&entry) {
println!("DEBUG: Skipping ignored file: {entry:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {entry:?}");
}
}
}
}
} else {
match resolve_path(file_path) {
Ok(resolved_path) => {
let is_test = is_test_file(&resolved_path);
if !is_ignored_by_gitignore(&resolved_path) && (allow_tests || !is_test) {
processed_paths.insert(file_path.to_string());
results.push((resolved_path, Some(start), Some(end), None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&resolved_path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
Err(err) => {
if debug_mode {
println!("DEBUG: Failed to resolve path '{file_path}': {err}");
}
let path = PathBuf::from(file_path);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
processed_paths.insert(file_path.to_string());
results.push((path, Some(start), Some(end), None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
}
}
}
}
let file_line_regex =
Regex::new(r"(?:^|[\s\r\n])([a-zA-Z0-9_\-./\*\{\}]+\.[a-zA-Z0-9]+):(\d+)(?::\d+)?")
.unwrap();
for cap in file_line_regex.captures_iter(text) {
let file_path = cap.get(1).unwrap().as_str();
if processed_paths.contains(file_path) {
continue;
}
let line_num = cap.get(2).and_then(|m| m.as_str().parse::<usize>().ok());
if file_path.contains('*') || file_path.contains('{') {
if let Ok(paths) = glob(file_path) {
for entry in paths.flatten() {
let path_str = entry.to_string_lossy().to_string();
if !processed_paths.contains(&path_str) {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
processed_paths.insert(path_str);
results.push((entry, line_num, None, None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&entry) {
println!("DEBUG: Skipping ignored file: {entry:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {entry:?}");
}
}
}
}
}
} else {
match resolve_path(file_path) {
Ok(path) => {
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
processed_paths.insert(file_path.to_string());
results.push((path, line_num, None, None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
Err(err) => {
if debug_mode {
println!("DEBUG: Failed to resolve path '{file_path}': {err}");
}
let path = PathBuf::from(file_path);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
processed_paths.insert(file_path.to_string());
results.push((path, line_num, None, None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
}
}
}
let simple_file_regex =
Regex::new(r"(?:^|[\s\r\n])([a-zA-Z0-9_\-./\*\{\}]+\.[a-zA-Z0-9]+)").unwrap();
for cap in simple_file_regex.captures_iter(text) {
let file_path = cap.get(1).unwrap().as_str();
if !processed_paths.contains(file_path) {
if file_path.contains('*') || file_path.contains('{') {
if let Ok(paths) = glob(file_path) {
for entry in paths.flatten() {
let path_str = entry.to_string_lossy().to_string();
if !processed_paths.contains(&path_str) {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
processed_paths.insert(path_str);
results.push((entry, None, None, None, None));
} else if debug_mode {
if is_ignored_by_gitignore(&entry) {
println!("DEBUG: Skipping ignored file: {entry:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {entry:?}");
}
}
}
}
}
} else {
match resolve_path(file_path) {
Ok(path) => {
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
results.push((path, None, None, None, None));
processed_paths.insert(file_path.to_string());
} else if debug_mode {
if is_ignored_by_gitignore(&path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
Err(err) => {
if debug_mode {
println!("DEBUG: Failed to resolve path '{file_path}': {err}");
}
let path = PathBuf::from(file_path);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
results.push((path, None, None, None, None));
processed_paths.insert(file_path.to_string());
} else if debug_mode {
if is_ignored_by_gitignore(&path) {
println!("DEBUG: Skipping ignored file: {file_path:?}");
} else if !allow_tests && is_test {
println!("DEBUG: Skipping test file: {file_path:?}");
}
}
}
}
}
}
}
results
}
pub fn parse_file_with_line(input: &str, allow_tests: bool) -> Vec<FilePathInfo> {
let mut results = Vec::new();
let first_char = input.chars().next().unwrap_or(' ');
let last_char = input.chars().last().unwrap_or(' ');
let cleaned_input = if (first_char == '`' || first_char == '\'' || first_char == '"')
&& first_char == last_char
{
&input[1..input.len() - 1]
} else {
input.trim_matches(|c| c == '`' || c == '"')
};
if let Some((file_part, symbol)) = cleaned_input.split_once('#') {
match resolve_path(file_part) {
Ok(path) => {
let is_test = is_test_file(&path);
if allow_tests || !is_test {
results.push((path, None, None, Some(symbol.to_string()), None));
}
}
Err(err) => {
if std::env::var("DEBUG").unwrap_or_default() == "1" {
println!("DEBUG: Failed to resolve path '{file_part}': {err}");
}
let path = PathBuf::from(file_part);
let is_test = is_test_file(&path);
if allow_tests || !is_test {
results.push((path, None, None, Some(symbol.to_string()), None));
}
}
}
return results;
} else if let Some((file_part, rest)) = cleaned_input.split_once(':') {
let line_spec = rest.split(':').next().unwrap_or("");
if let Some((start_str, end_str)) = line_spec.split_once('-') {
let start_num = start_str.parse::<usize>().ok();
let end_num = end_str.parse::<usize>().ok();
if let (Some(start), Some(end)) = (start_num, end_num) {
if file_part.contains('*') || file_part.contains('{') {
let base_dir = std::path::Path::new(".");
let mut builder = WalkBuilder::new(base_dir);
builder.git_ignore(true);
builder.git_global(true);
builder.git_exclude(true);
if let Ok(paths) = glob(file_part) {
for entry in paths.flatten() {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
results.push((entry, Some(start), Some(end), None, None));
}
}
}
} else {
match resolve_path(file_part) {
Ok(path) => {
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
results.push((path, Some(start), Some(end), None, None));
}
}
Err(err) => {
if std::env::var("DEBUG").unwrap_or_default() == "1" {
println!("DEBUG: Failed to resolve path '{file_part}': {err}");
}
let path = PathBuf::from(file_part);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
results.push((path, Some(start), Some(end), None, None));
}
}
}
}
}
} else {
let line_num = line_spec.parse::<usize>().ok();
if let Some(num) = line_num {
if file_part.contains('*') || file_part.contains('{') {
if let Ok(paths) = glob(file_part) {
for entry in paths.flatten() {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
let mut lines_set = HashSet::new();
lines_set.insert(num);
results.push((entry, Some(num), None, None, Some(lines_set)));
}
}
}
} else {
match resolve_path(file_part) {
Ok(path) => {
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
let mut lines_set = HashSet::new();
lines_set.insert(num);
results.push((path, Some(num), None, None, Some(lines_set)));
}
}
Err(err) => {
if std::env::var("DEBUG").unwrap_or_default() == "1" {
println!("DEBUG: Failed to resolve path '{file_part}': {err}");
}
let path = PathBuf::from(file_part);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
let mut lines_set = HashSet::new();
lines_set.insert(num);
results.push((path, Some(num), None, None, Some(lines_set)));
}
}
}
}
}
}
} else {
if cleaned_input.contains('*') || cleaned_input.contains('{') {
if let Ok(paths) = glob(cleaned_input) {
for entry in paths.flatten() {
let is_test = is_test_file(&entry);
let should_include =
!is_ignored_by_gitignore(&entry) && (allow_tests || !is_test);
if should_include {
results.push((entry, None, None, None, None));
}
}
}
} else {
match resolve_path(cleaned_input) {
Ok(path) => {
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
results.push((path, None, None, None, None));
}
}
Err(err) => {
if std::env::var("DEBUG").unwrap_or_default() == "1" {
println!("DEBUG: Failed to resolve path '{cleaned_input}': {err}");
}
let path = PathBuf::from(cleaned_input);
let is_test = is_test_file(&path);
if !is_ignored_by_gitignore(&path) && (allow_tests || !is_test) {
results.push((path, None, None, None, None));
}
}
}
}
}
results
}
thread_local! {
static CUSTOM_IGNORES: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
}
pub fn set_custom_ignores(patterns: &[String]) {
CUSTOM_IGNORES.with(|cell| {
let mut ignores = cell.borrow_mut();
ignores.clear();
ignores.extend(patterns.iter().cloned());
});
}
fn is_ignored_by_gitignore(path: &PathBuf) -> bool {
let debug_mode = std::env::var("DEBUG").unwrap_or_default() == "1";
let path_str = path.to_string_lossy().to_lowercase();
let common_ignore_patterns = [
"node_modules",
"vendor",
"target",
"dist",
"build",
".git",
".svn",
".hg",
".idea",
".vscode",
"__pycache__",
];
let mut custom_patterns = Vec::new();
CUSTOM_IGNORES.with(|cell| {
let ignores = cell.borrow();
custom_patterns.extend(ignores.iter().cloned());
});
for pattern in &common_ignore_patterns {
if path_str.contains(pattern) {
if debug_mode {
println!("DEBUG: File {path:?} is ignored (contains pattern '{pattern}')");
}
return true;
}
}
for pattern in &custom_patterns {
if path_str.contains(pattern) {
if debug_mode {
println!("DEBUG: File {path:?} is ignored (contains custom pattern '{pattern}')");
}
return true;
}
}
false
}