mod diff_preview;
mod list;
mod path_policy;
mod read;
mod tool;
mod write;
use anyhow::{Context, Result};
use std::path::Path;
use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader};
use vtcode_commons::async_utils::read_exact_uninit;
pub use diff_preview::{build_diff_preview, diff_preview_error_skip, diff_preview_size_skip, diff_preview_suppressed};
pub use tool::FileOpsTool;
pub use vtcode_commons::fs::is_image_path;
pub(crate) struct ByteRangeReadResult {
pub content: String,
pub has_more: bool,
pub lines_read: usize,
}
pub(crate) async fn read_byte_range(
file_path: &Path,
offset_bytes: u64,
page_size_bytes: usize,
line_numbers: bool,
) -> Result<ByteRangeReadResult> {
let metadata = tokio::fs::metadata(file_path)
.await
.with_context(|| format!("Failed to read metadata for: {}", file_path.display()))?;
if !metadata.is_file() {
anyhow::bail!("Path is not a file: {}", file_path.display());
}
let file_size = metadata.len();
if offset_bytes >= file_size {
return Ok(ByteRangeReadResult {
content: String::new(),
has_more: false,
lines_read: 0,
});
}
let mut file = tokio::fs::File::open(file_path)
.await
.with_context(|| format!("Failed to open: {}", file_path.display()))?;
file.seek(std::io::SeekFrom::Start(offset_bytes))
.await
.with_context(|| format!("Failed to seek to offset {} in: {}", offset_bytes, file_path.display()))?;
let page_size_u64 = page_size_bytes as u64;
let end_pos = offset_bytes
.checked_add(page_size_u64)
.map(|end| std::cmp::min(end, file_size))
.unwrap_or(file_size);
let actual_read_size = (end_pos - offset_bytes) as usize;
let buffer = read_exact_uninit(&mut file, actual_read_size).await.with_context(|| {
format!("Failed to read {} bytes from offset {} in: {}", actual_read_size, offset_bytes, file_path.display())
})?;
let raw = String::from_utf8_lossy(&buffer).into_owned();
let has_more = end_pos < file_size;
if !line_numbers {
return Ok(ByteRangeReadResult { content: raw, has_more, lines_read: 0 });
}
let start_line = count_lines_before(file_path, offset_bytes).await?;
let at_line_boundary = if offset_bytes == 0 {
true
} else {
let mut pre = tokio::fs::File::open(file_path)
.await
.with_context(|| format!("Failed to open: {}", file_path.display()))?;
pre.seek(std::io::SeekFrom::Start(offset_bytes - 1))
.await
.context("failed to seek for boundary check")?;
let mut prev_byte = [0u8; 1];
tokio::io::AsyncReadExt::read_exact(&mut pre, &mut prev_byte)
.await
.context("failed to read boundary byte")?;
prev_byte[0] == b'\n'
};
let lines: Vec<&str> = raw.split('\n').collect();
let mut formatted_lines = Vec::new();
let mut current_line = start_line;
for (i, line) in lines.iter().enumerate() {
if i == 0 && !at_line_boundary && !line.is_empty() {
current_line += 1;
continue;
}
if i == lines.len() - 1 && has_more && line.is_empty() {
continue;
}
let trimmed = line.trim_end_matches('\r');
if !trimmed.is_empty() || i < lines.len() - 1 {
formatted_lines.push(format!("{current_line}: {trimmed}"));
current_line += 1;
}
}
let lines_read = formatted_lines.len();
let content = formatted_lines.join("\n");
Ok(ByteRangeReadResult { content, has_more, lines_read })
}
async fn count_lines_before(file_path: &Path, offset_bytes: u64) -> Result<usize> {
if offset_bytes == 0 {
return Ok(1);
}
let file = tokio::fs::File::open(file_path)
.await
.with_context(|| format!("Failed to open: {}", file_path.display()))?;
let mut reader = BufReader::new(file);
let mut line_count: usize = 1; let mut bytes_read: u64 = 0;
let mut buffer = Vec::new();
while bytes_read < offset_bytes {
buffer.clear();
let n = reader.read_until(b'\n', &mut buffer).await.context("failed to count lines")?;
if n == 0 {
break;
}
bytes_read += n as u64;
if bytes_read <= offset_bytes {
line_count += 1;
}
}
Ok(line_count)
}
pub(crate) fn restore_exact_text_content(content: &str, size_bytes: u64) -> Option<String> {
let content_size = content.len() as u64;
match size_bytes.checked_sub(content_size) {
Some(0) => Some(content.to_string()),
Some(1) => Some(format!("{content}\n")),
Some(2) if content.contains("\r\n") || !content.contains('\n') => Some(format!("{content}\r\n")),
_ => None,
}
}