fix: view can recognise a dir (#4701)

This commit is contained in:
Michael Neale
2025-09-23 22:58:28 +10:00
committed by GitHub
parent ac61217730
commit d9e5ceaa49
2 changed files with 242 additions and 1 deletions
@@ -2963,6 +2963,157 @@ mod tests {
assert!(text.text.contains("100: Line 100"));
}
#[tokio::test]
#[serial]
async fn test_text_editor_view_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path();
// Set the current directory before creating the server
std::env::set_current_dir(&temp_path).unwrap();
// Create some test files and directories
fs::create_dir(temp_path.join("subdir1")).unwrap();
fs::create_dir(temp_path.join("subdir2")).unwrap();
fs::create_dir(temp_path.join("another_dir")).unwrap();
fs::write(temp_path.join("file1.txt"), "content1").unwrap();
fs::write(temp_path.join("file2.rs"), "content2").unwrap();
fs::write(temp_path.join("README.md"), "content3").unwrap();
let server = create_test_server();
// Test viewing a directory
let result = server
.text_editor(Parameters(TextEditorParams {
command: "view".to_string(),
path: temp_path.to_str().unwrap().to_string(),
view_range: None,
file_text: None,
old_str: None,
new_str: None,
insert_line: None,
diff: None,
}))
.await;
assert!(result.is_ok());
let content = result.unwrap().content;
assert_eq!(content.len(), 1);
// Check the content is a text message with directory listing
let text_content = content[0].as_text().expect("Expected text content");
let output = &text_content.text;
// Check that it identifies as a directory
assert!(output.contains("is a directory"));
assert!(output.contains("Contents:"));
// Check directories are listed with trailing slash
assert!(output.contains("Directories:"));
assert!(output.contains("another_dir/"));
assert!(output.contains("subdir1/"));
assert!(output.contains("subdir2/"));
// Check files are listed
assert!(output.contains("Files:"));
assert!(output.contains("file1.txt"));
assert!(output.contains("file2.rs"));
assert!(output.contains("README.md"));
}
#[tokio::test]
#[serial]
async fn test_text_editor_view_directory_with_many_files() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path();
// Set the current directory before creating the server
std::env::set_current_dir(&temp_path).unwrap();
// Create more than 50 files to test the limit
for i in 0..60 {
fs::write(
temp_path.join(format!("file{:03}.txt", i)),
format!("content{}", i),
)
.unwrap();
}
// Create some directories too
for i in 0..10 {
fs::create_dir(temp_path.join(format!("dir{:02}", i))).unwrap();
}
let server = create_test_server();
let result = server
.text_editor(Parameters(TextEditorParams {
command: "view".to_string(),
path: temp_path.to_str().unwrap().to_string(),
view_range: None,
file_text: None,
old_str: None,
new_str: None,
insert_line: None,
diff: None,
}))
.await;
assert!(result.is_ok());
let content = result.unwrap().content;
assert_eq!(content.len(), 1);
let text_content = content[0].as_text().expect("Expected text content");
let output = &text_content.text;
// Check that it shows the limit message
assert!(output.contains("... and"));
assert!(output.contains("more items"));
assert!(output.contains("(showing first 50 items)"));
// Count the actual number of items shown (should be 50)
let dir_count = output.matches("/\n").count(); // directories end with /
let file_count = output.matches(".txt\n").count(); // only counting .txt files for simplicity
assert!(dir_count + file_count <= 50);
}
#[tokio::test]
#[serial]
async fn test_text_editor_view_empty_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path();
// Set the current directory before creating the server
std::env::set_current_dir(&temp_path).unwrap();
let server = create_test_server();
let result = server
.text_editor(Parameters(TextEditorParams {
command: "view".to_string(),
path: temp_path.to_str().unwrap().to_string(),
view_range: None,
file_text: None,
old_str: None,
new_str: None,
insert_line: None,
diff: None,
}))
.await;
assert!(result.is_ok());
let content = result.unwrap().content;
assert_eq!(content.len(), 1);
let text_content = content[0].as_text().expect("Expected text content");
let output = &text_content.text;
// Check that it shows empty directory message
assert!(output.contains("is a directory"));
assert!(output.contains("(empty directory)"));
}
#[test]
#[serial]
fn test_shell_output_truncation() {
+91 -1
View File
@@ -468,15 +468,105 @@ pub fn recommend_read_range(path: &Path, total_lines: usize) -> Result<Vec<Conte
), None))
}
/// Lists the contents of a directory with a maximum number of items
fn list_directory_contents(path: &Path) -> Result<Vec<Content>, ErrorData> {
const MAX_ITEMS: usize = 50; // Maximum number of items to display
// List files in the directory (similar to ls output)
let entries = std::fs::read_dir(path).map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to read directory: {}", e),
None,
)
})?;
let mut files = Vec::new();
let mut dirs = Vec::new();
let mut total_count = 0;
for entry in entries {
let entry = entry.map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to read directory entry: {}", e),
None,
)
})?;
total_count += 1;
// Only process up to MAX_ITEMS entries
if dirs.len() + files.len() < MAX_ITEMS {
let metadata = entry.metadata().map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to read metadata: {}", e),
None,
)
})?;
let name = entry.file_name().to_string_lossy().to_string();
if metadata.is_dir() {
dirs.push(format!("{}/", name));
} else {
files.push(name);
}
}
}
// Sort for consistent output
dirs.sort();
files.sort();
let mut output = format!("'{}' is a directory. Contents:\n\n", path.display());
if !dirs.is_empty() {
output.push_str("Directories:\n");
for dir in &dirs {
output.push_str(&format!(" {}\n", dir));
}
output.push('\n');
}
if !files.is_empty() {
output.push_str("Files:\n");
for file in &files {
output.push_str(&format!(" {}\n", file));
}
}
if dirs.is_empty() && files.is_empty() {
output.push_str(" (empty directory)\n");
}
// If we hit the limit, indicate there are more items
if total_count > MAX_ITEMS {
output.push_str(&format!(
"\n... and {} more items (showing first {} items)\n",
total_count - MAX_ITEMS,
MAX_ITEMS
));
}
Ok(vec![Content::text(output)])
}
pub async fn text_editor_view(
path: &PathBuf,
view_range: Option<(usize, i64)>,
) -> Result<Vec<Content>, ErrorData> {
// Check if path is a directory
if path.is_dir() {
return list_directory_contents(path);
}
if !path.is_file() {
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!(
"The path '{}' does not exist or is not a file.",
"The path '{}' does not exist or is not accessible.",
path.display()
),
None,