fix: view can recognise a dir (#4701)
This commit is contained in:
@@ -2963,6 +2963,157 @@ mod tests {
|
|||||||
assert!(text.text.contains("100: Line 100"));
|
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]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn test_shell_output_truncation() {
|
fn test_shell_output_truncation() {
|
||||||
|
|||||||
@@ -468,15 +468,105 @@ pub fn recommend_read_range(path: &Path, total_lines: usize) -> Result<Vec<Conte
|
|||||||
), None))
|
), 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(
|
pub async fn text_editor_view(
|
||||||
path: &PathBuf,
|
path: &PathBuf,
|
||||||
view_range: Option<(usize, i64)>,
|
view_range: Option<(usize, i64)>,
|
||||||
) -> Result<Vec<Content>, ErrorData> {
|
) -> Result<Vec<Content>, ErrorData> {
|
||||||
|
// Check if path is a directory
|
||||||
|
if path.is_dir() {
|
||||||
|
return list_directory_contents(path);
|
||||||
|
}
|
||||||
|
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
return Err(ErrorData::new(
|
return Err(ErrorData::new(
|
||||||
ErrorCode::INTERNAL_ERROR,
|
ErrorCode::INTERNAL_ERROR,
|
||||||
format!(
|
format!(
|
||||||
"The path '{}' does not exist or is not a file.",
|
"The path '{}' does not exist or is not accessible.",
|
||||||
path.display()
|
path.display()
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
|
|||||||
Reference in New Issue
Block a user