vtcode-core 0.136.0

Core library for VT Code - a Rust-based terminal coding agent
use super::super::read_byte_range;
use super::FileOpsTool;
use crate::config::constants::tools;
use crate::tools::builder::ToolResponseBuilder;
use crate::tools::types::Input;
use crate::utils::file_utils::read_file_with_context;
use anyhow::{Context, Result, anyhow};
use serde_json::{Value, json};
use std::path::Path;

impl FileOpsTool {
    pub(super) async fn read_file_paged(&self, file_path: &Path, input: &Input) -> Result<(String, Value, bool)> {
        // Get file metadata to verify file exists and get size
        let file_metadata = tokio::fs::metadata(file_path)
            .await
            .with_context(|| format!("Failed to read metadata for file: {}", file_path.display()))?;

        if !file_metadata.is_file() {
            return Err(anyhow!("Path is not a file: {}", file_path.display()));
        }

        let file_size = file_metadata.len();

        // Calculate the final content based on whether we're using byte or line-based paging
        let (final_content, is_truncated) = if input.offset_lines.is_some() || input.page_size_lines.is_some() {
            // Line-based paging
            self.read_file_by_lines(file_path, input, file_size as usize).await?
        } else {
            // Byte-based paging (default)
            self.read_file_by_bytes(file_path, input, file_size).await?
        };

        // Create builder and metadata object
        let mut builder = ToolResponseBuilder::new(tools::READ_FILE)
            .success()
            .message(format!("Successfully read file {} (paged)", self.workspace_relative_display(file_path)))
            .content(final_content.clone())
            .data("size_bytes", json!(file_size))
            .data("size_lines", json!(final_content.lines().count()))
            .data("is_truncated", json!(is_truncated))
            .data("content_kind", json!("text"))
            .data("encoding", json!("utf8"));

        // Copy paging parameters to metadata
        if let Some(offset_bytes) = input.offset_bytes {
            builder = builder.data("offset_bytes", json!(offset_bytes));
        }
        if let Some(page_size_bytes) = input.page_size_bytes {
            builder = builder.data("page_size_bytes", json!(page_size_bytes));
        }
        if let Some(offset_lines) = input.offset_lines {
            builder = builder.data("offset_lines", json!(offset_lines));
        }
        if let Some(page_size_lines) = input.page_size_lines {
            builder = builder.data("page_size_lines", json!(page_size_lines));
        }

        Ok((final_content, builder.build_json()["metadata"].clone(), is_truncated))
    }

    /// Read file content by lines with offset and page size
    async fn read_file_by_lines(&self, file_path: &Path, input: &Input, _file_size: usize) -> Result<(String, bool)> {
        // Validate and extract parameters
        let offset_lines = input.offset_lines.unwrap_or(0);
        let page_size_lines = input.page_size_lines.unwrap_or(1000); // Reasonable default: 1000 lines

        // Validate offset and page size
        if offset_lines > usize::MAX / 2 {
            return Err(anyhow!("Offset too large: {offset_lines}"));
        }
        if page_size_lines == 0 {
            return Err(anyhow!("Page size must be greater than 0"));
        }

        // Check for overflow before adding
        if offset_lines > usize::MAX - page_size_lines {
            return Err(anyhow!("Offset_lines + page_size_lines would overflow: {offset_lines} + {page_size_lines}"));
        }

        let content = read_file_with_context(file_path, "file content")
            .await
            .with_context(|| format!("Failed to read file content: {}", file_path.display()))?;

        let all_lines: Vec<&str> = content.lines().collect();
        let total_lines = all_lines.len();

        // Handle empty file or offset beyond bounds
        if total_lines == 0 || offset_lines >= total_lines {
            return Ok((String::new(), false));
        }

        // Calculate end position (safe because we validated overflow above)
        let end_pos = std::cmp::min(offset_lines + page_size_lines, total_lines);
        let selected_lines = &all_lines[offset_lines..end_pos];

        let final_content = selected_lines.join("\n");
        let is_truncated = end_pos < total_lines;

        Ok((final_content, is_truncated))
    }

    /// Read file content by bytes with offset and page size
    async fn read_file_by_bytes(&self, file_path: &Path, input: &Input, _file_size: u64) -> Result<(String, bool)> {
        let offset_bytes = input.offset_bytes.unwrap_or(0);
        let page_size_bytes = input.page_size_bytes.unwrap_or(8192);

        if page_size_bytes == 0 {
            return Err(anyhow!("Page size must be greater than 0"));
        }

        // Legacy path: raw bytes without line numbers
        let result = read_byte_range(file_path, offset_bytes, page_size_bytes, false).await?;
        Ok((result.content, result.has_more))
    }
}