use super::core::resolve_path;
use super::search;
use crate::utils::line_hash::{compute_line_hashes, is_hash_mode};
use crate::utils::truncation::format_content_with_line_numbers;
use anyhow::{anyhow, bail, Result};
use std::path::Path;
use tokio::fs as tokio_fs;
fn format_file_content_with_numbers(lines: &[&str], line_range: Option<(usize, i64)>) -> String {
format_content_with_line_numbers(lines, 1, line_range)
}
pub async fn view_file_spec(path: &Path, line_range: Option<(usize, i64)>) -> Result<String> {
if !path.exists() {
bail!("File not found");
}
if path.is_dir() {
let mut entries = Vec::new();
let read_dir = tokio_fs::read_dir(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read directory: {}", e))?;
let mut dir_entries = read_dir;
while let Some(entry) = dir_entries
.next_entry()
.await
.map_err(|e| anyhow!("Error reading directory: {}", e))?
{
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = entry
.file_type()
.await
.map_err(|e| anyhow!("Error reading file type: {}", e))?
.is_dir();
entries.push(if is_dir { format!("{}/", name) } else { name });
}
entries.sort();
let content = entries.join("\n");
return Ok(content);
}
if !path.is_file() {
bail!("Path is not a file");
}
let metadata = tokio_fs::metadata(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read file: {}", e))?;
if metadata.len() > 1024 * 1024 * 5 {
bail!("File is too large (>5MB)");
}
let content = tokio_fs::read_to_string(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read file: {}", e))?;
let lines: Vec<&str> = content.lines().collect();
let content_with_numbers = format_file_content_with_numbers(&lines, line_range);
if content_with_numbers.starts_with("Start line")
|| content_with_numbers.starts_with("Start line")
{
bail!("{}", content_with_numbers);
}
Ok(content_with_numbers)
}
pub async fn view_file_multi_ranges(path: &Path, ranges: &[(usize, i64)]) -> Result<String> {
if !path.exists() {
bail!("File not found");
}
if !path.is_file() {
bail!("Path is not a file");
}
let metadata = tokio_fs::metadata(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read file: {}", e))?;
if metadata.len() > 1024 * 1024 * 5 {
bail!("File is too large (>5MB)");
}
let content = tokio_fs::read_to_string(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read file: {}", e))?;
let lines: Vec<&str> = content.lines().collect();
if ranges.is_empty() {
return Ok(format_file_content_with_numbers(&lines, None));
}
let parts: Vec<String> = ranges
.iter()
.map(|r| format_file_content_with_numbers(&lines, Some(*r)))
.collect();
Ok(parts.join("\n--\n"))
}
pub async fn view_file_with_content_search(
path: &Path,
pattern: &str,
context_lines: usize,
regex: bool,
) -> Result<String> {
if !path.exists() {
bail!("File not found");
}
if !path.is_file() {
bail!("Path is not a file");
}
let bytes = tokio_fs::read(path)
.await
.map_err(|e| anyhow!("Cannot read file: {}", e))?;
let content = String::from_utf8_lossy(&bytes).into_owned();
let file_lines: Vec<&str> = content.lines().collect();
let total = file_lines.len();
if total == 0 {
return Ok(String::new());
}
let matcher = search::Matcher::new(pattern, regex)?;
let blocks = search::search_lines(&content, &matcher, context_lines);
if blocks.is_empty() {
return Ok(String::new());
}
let prefixes: Vec<String> = if is_hash_mode() {
compute_line_hashes(&file_lines)
} else {
(1..=total).map(|n| n.to_string()).collect()
};
let mut parts: Vec<String> = Vec::new();
for block in &blocks {
let mut rendered = Vec::new();
for &n in &block.line_numbers {
let idx = n - 1;
rendered.push(format!("{}:{}", prefixes[idx], file_lines[idx]));
}
parts.push(rendered.join("\n"));
}
Ok(parts.join("\n--\n"))
}
pub async fn create_file_spec(path: &Path, content: &str) -> Result<String> {
if path.exists() {
bail!(
"File already exists: {}. Do NOT retry `create` — use `str_replace` to replace specific content, `line_replace` to replace specific lines, or `insert` to add new content at a position.",
path.display()
);
}
if let Some(parent) = path.parent() {
if !parent.exists() {
tokio_fs::create_dir_all(parent)
.await
.map_err(|e| anyhow!("Permission denied. Cannot create directories: {}", e))?;
}
}
tokio_fs::write(path, content)
.await
.map_err(|e| anyhow!("Permission denied. Cannot write to file: {}", e))?;
Ok(format!(
"File created successfully with {} bytes",
content.len()
))
}
pub async fn view_many_files_spec(
paths: &[String],
workdir: &Path,
per_file_ranges: &[Option<(usize, i64)>],
) -> Result<String> {
let mut result_parts = Vec::new();
let mut success_count = 0;
for (i, path_str) in paths.iter().enumerate() {
let path = resolve_path(path_str, workdir);
let path_display = path_str.to_string();
let line_range = per_file_ranges.get(i).copied().flatten();
result_parts.push(path_display.clone());
if !path.exists() {
result_parts.push("✗ File does not exist".to_string());
result_parts.push("".to_string()); continue;
}
if path.is_dir() {
let mut entries = Vec::new();
if let Ok(mut read_dir) = tokio_fs::read_dir(&path).await {
while let Ok(Some(entry)) = read_dir.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(file_type) = entry.file_type().await {
entries.push(if file_type.is_dir() {
format!("{}/", name)
} else {
name
});
}
}
entries.sort();
result_parts.push(entries.join("\n"));
} else {
result_parts.push("✗ Permission denied. Cannot read directory".to_string());
}
result_parts.push("".to_string()); continue;
}
if !path.is_file() {
result_parts.push("✗ Path is not a file".to_string());
result_parts.push("".to_string()); continue;
}
let _metadata = match tokio_fs::metadata(&path).await {
Ok(meta) => {
if meta.len() > 1024 * 1024 * 5 {
result_parts.push("✗ File is too large (>5MB)".to_string());
result_parts.push("".to_string()); continue;
}
meta
}
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
if let Ok(sample) = tokio_fs::read(&path).await {
let sample_size = sample.len().min(512);
let null_count = sample[..sample_size].iter().filter(|&&b| b == 0).count();
if null_count > sample_size / 10 {
result_parts.push("✗ Binary file skipped".to_string());
result_parts.push("".to_string()); continue;
}
}
let content = match tokio_fs::read_to_string(&path).await {
Ok(content) => content,
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
let lines: Vec<&str> = content.lines().collect();
let content_with_numbers = format_file_content_with_numbers(&lines, line_range);
result_parts.push(content_with_numbers);
result_parts.push("".to_string()); success_count += 1;
}
if result_parts.last() == Some(&"".to_string()) {
result_parts.pop();
}
let final_content = result_parts.join("\n");
if success_count == 0 {
bail!("{}", final_content);
}
Ok(final_content)
}
pub async fn view_many_files(
paths: &[String],
workdir: &Path,
per_file_ranges: &[Option<(usize, i64)>],
) -> Result<String> {
let mut result_parts = Vec::new();
let mut success_count = 0;
for (i, path_str) in paths.iter().enumerate() {
let path = resolve_path(path_str, workdir);
let path_display = path_str.to_string();
let line_range = per_file_ranges.get(i).copied().flatten();
result_parts.push(path_display.clone());
if !path.exists() {
result_parts.push("✗ File does not exist".to_string());
result_parts.push("".to_string()); continue;
}
if path.is_dir() {
let mut entries = Vec::new();
if let Ok(mut read_dir) = tokio_fs::read_dir(&path).await {
while let Ok(Some(entry)) = read_dir.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(file_type) = entry.file_type().await {
entries.push(if file_type.is_dir() {
format!("{}/", name)
} else {
name
});
}
}
entries.sort();
result_parts.push(entries.join("\n"));
} else {
result_parts.push("✗ Permission denied. Cannot read directory".to_string());
}
result_parts.push("".to_string()); continue;
}
if !path.is_file() {
result_parts.push("✗ Path is not a file".to_string());
result_parts.push("".to_string()); continue;
}
let _metadata = match tokio_fs::metadata(&path).await {
Ok(meta) => {
if meta.len() > 1024 * 1024 * 5 {
result_parts.push("✗ File is too large (>5MB)".to_string());
result_parts.push("".to_string()); continue;
}
meta
}
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
if let Ok(sample) = tokio_fs::read(&path).await {
let sample_size = sample.len().min(512);
let null_count = sample[..sample_size].iter().filter(|&&b| b == 0).count();
if null_count > sample_size / 10 {
result_parts.push("✗ Binary file skipped".to_string());
result_parts.push("".to_string()); continue;
}
}
let content = match tokio_fs::read_to_string(&path).await {
Ok(content) => content,
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
let lines: Vec<&str> = content.lines().collect();
let content_with_numbers = format_file_content_with_numbers(&lines, line_range);
result_parts.push(content_with_numbers);
result_parts.push("".to_string()); success_count += 1;
}
if result_parts.last() == Some(&"".to_string()) {
result_parts.pop();
}
let final_content = result_parts.join("\n");
if success_count == 0 {
bail!("{}", final_content);
}
Ok(final_content)
}