use crate::utils::file_parser::parse_file_references;
use crate::utils::file_renderer::render_files_as_xml;
pub fn parse_file_contexts(summary_content: &str) -> Vec<(String, usize, usize)> {
let file_refs = parse_file_references(summary_content);
let mut contexts = Vec::new();
for (filepath, ranges) in file_refs {
for range in ranges {
contexts.push((filepath.clone(), range.start, range.end));
}
}
contexts
}
pub fn generate_file_context_content(file_contexts: &[(String, usize, usize)]) -> String {
if file_contexts.is_empty() {
return "No specific file context requested.".to_string();
}
use crate::utils::file_parser::{read_file_lines, LineRange};
use std::collections::HashMap;
let mut file_contents = HashMap::new();
for (filepath, start_line, end_line) in file_contexts {
if let Some(range) = LineRange::new(*start_line, *end_line) {
let content = read_file_lines(filepath, &range);
file_contents
.entry(filepath.clone())
.or_insert_with(Vec::new)
.push(content);
}
}
render_files_as_xml(&file_contents)
}
pub fn extract_file_refs_from_args(
tool_name: &str,
args: &serde_json::Value,
refs: &mut Vec<String>,
) {
let file_read_tools = ["view", "text_editor", "batch_edit", "extract_lines"];
if !file_read_tools.contains(&tool_name) {
return;
}
let args_obj = match args.as_object() {
Some(obj) => obj,
None => return,
};
if let Some(path) = args_obj.get("path").and_then(|p| p.as_str()) {
let lines = args_obj.get("lines");
if let Some(lines) = lines {
if let Some(arr) = lines.as_array() {
if arr.len() >= 2 {
if let (Some(start), Some(end)) = (arr[0].as_u64(), arr[1].as_u64()) {
refs.push(format!("{}:{}:{}", path, start, end));
return;
}
}
}
}
refs.push(path.to_string());
}
if tool_name == "view" {
if let Some(paths) = args_obj.get("paths").and_then(|p| p.as_array()) {
for p in paths {
if let Some(path) = p.as_str() {
refs.push(path.to_string());
}
}
}
}
if tool_name == "extract_lines" {
if let Some(from_path) = args_obj.get("from_path").and_then(|p| p.as_str()) {
let from_range = args_obj.get("from_range");
if let Some(arr) = from_range.and_then(|r| r.as_array()) {
if arr.len() >= 2 {
if let (Some(start), Some(end)) = (arr[0].as_u64(), arr[1].as_u64()) {
refs.push(format!("{}:{}:{}", from_path, start, end));
return;
}
}
}
refs.push(from_path.to_string());
}
}
}
pub fn merge_file_refs(refs: &[String]) -> Vec<String> {
use std::collections::{BTreeMap, BTreeSet};
let mut by_file: BTreeMap<String, Vec<(u64, u64)>> = BTreeMap::new();
let mut whole_files: BTreeSet<String> = BTreeSet::new();
for ref_str in refs {
let parts: Vec<&str> = ref_str.split(':').collect();
match parts.len() {
1 => {
whole_files.insert(parts[0].to_string());
}
3 => {
if let (Ok(start), Ok(end)) = (parts[1].parse::<u64>(), parts[2].parse::<u64>()) {
by_file
.entry(parts[0].to_string())
.or_default()
.push((start, end));
}
}
_ => {}
}
}
let mut merged: Vec<String> = Vec::new();
for path in whole_files {
by_file.remove(&path);
merged.push(path);
}
for (path, mut ranges) in by_file {
if ranges.is_empty() {
continue;
}
ranges.sort_by_key(|r| r.0);
let mut merged_ranges: Vec<(u64, u64)> = Vec::new();
for (start, end) in ranges {
if let Some(last) = merged_ranges.last_mut() {
if start <= last.1 + 10 {
last.1 = last.1.max(end);
continue;
}
}
merged_ranges.push((start, end));
}
for (start, end) in merged_ranges {
merged.push(format!("{}:{}:{}", path, start, end));
}
}
merged
}
#[cfg(test)]
mod tests {
use super::parse_file_contexts;
#[test]
fn test_parse_file_contexts_code_block() {
let summary = r#"
## REQUIRED FILE CONTEXTS
List ALL files needed as context to continue work. Use EXACT format:
```
src/main.rs:1:50
src/lib.rs:100:150
config/settings.toml:10:20
```
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 3);
assert!(contexts.contains(&("src/main.rs".to_string(), 1, 50)));
assert!(contexts.contains(&("src/lib.rs".to_string(), 100, 150)));
assert!(contexts.contains(&("config/settings.toml".to_string(), 10, 20)));
}
#[test]
fn test_parse_file_contexts_section() {
let summary = r#"
## REQUIRED FILE CONTEXTS
The following files need context:
- src/session/mod.rs:200:300
- tests/integration.rs:1:100
## NEXT STEPS
Continue with implementation...
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&("src/session/mod.rs".to_string(), 200, 300)));
assert!(contexts.contains(&("tests/integration.rs".to_string(), 1, 100)));
}
#[test]
fn test_parse_file_contexts_fallback() {
let summary = r#"
We need to look at src/core.rs:50:100 and also check lib/utils.rs:1:25 for the implementation.
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&("src/core.rs".to_string(), 50, 100)));
assert!(contexts.contains(&("lib/utils.rs".to_string(), 1, 25)));
}
#[test]
fn test_parse_file_contexts_mandatory_format() {
let summary = r#"
## REQUIRED FILE CONTEXTS
CRITICAL: List ALL files needed as context using EXACT format below.
**MANDATORY FORMAT - Use code block with exact pattern:**
```
src/session/chat/session_continuation.rs:100:200
src/config/mod.rs:50:100
tests/integration_test.rs:1:50
```
**PARSING REQUIREMENTS:**
- Each line must be exactly: filepath:number:number
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 3);
assert!(contexts.contains(&(
"src/session/chat/session_continuation.rs".to_string(),
100,
200
)));
assert!(contexts.contains(&("src/config/mod.rs".to_string(), 50, 100)));
assert!(contexts.contains(&("tests/integration_test.rs".to_string(), 1, 50)));
}
#[test]
fn test_parse_file_contexts_invalid_ranges() {
let summary = r#"
```
src/main.rs:0:50
src/lib.rs:100:50
src/test.rs:1:20000
```
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 0);
}
#[test]
fn test_parse_file_contexts_with_context_tags() {
let summary = r#"
## REQUIRED FILE CONTEXTS
<context>
src/session/chat/continuation.rs:100:200
src/config/mod.rs:50:100
tests/integration_test.rs:1:50
</context>
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 3);
assert!(contexts.contains(&("src/session/chat/continuation.rs".to_string(), 100, 200)));
assert!(contexts.contains(&("src/config/mod.rs".to_string(), 50, 100)));
assert!(contexts.contains(&("tests/integration_test.rs".to_string(), 1, 50)));
}
#[test]
fn test_parse_file_contexts_context_tags_priority() {
let summary = r#"
<context>
src/main.rs:1:10
</context>
```
src/lib.rs:20:30
```
"#;
let contexts = parse_file_contexts(summary);
assert_eq!(contexts.len(), 1);
assert!(contexts.contains(&("src/main.rs".to_string(), 1, 10)));
}
}