generated from nhcarrigan/template
feat: add built-in file editor with syntax highlighting (#79)
## Summary - Add CodeMirror 6 editor with syntax highlighting for 40+ languages - Add file browser sidebar with collapsible directory tree navigation - Add multi-tab support with dirty state indicators and close buttons - Add keyboard shortcuts (Ctrl+E toggle, Ctrl+B file browser, Ctrl+S save, Ctrl+W close tab) - Add editor toggle button to status bar (disabled when not connected) - Editor automatically uses current session's working directory - Add Tauri backend commands for file operations (list_directory, read_file_content, write_file_content) ## Test Plan - [ ] Connect to a session and verify the editor toggle button becomes enabled - [ ] Press Ctrl+E to open the editor and verify file tree shows the session's CWD - [ ] Navigate directories and open files to verify syntax highlighting works - [ ] Edit a file and verify the dirty indicator (*) appears - [ ] Save with Ctrl+S and verify the dirty indicator disappears - [ ] Open multiple files and verify tab switching works - [ ] Close tabs with Ctrl+W or the X button - [ ] Disconnect and verify the editor automatically closes - [ ] Verify keyboard shortcuts are documented in the shortcuts modal Closes #72 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #79 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #79.
This commit is contained in:
@@ -394,6 +394,168 @@ pub async fn get_file_size(file_path: String) -> Result<u64, String> {
|
||||
Ok(metadata.len())
|
||||
}
|
||||
|
||||
// ==================== Editor File Operations ====================
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct FileEntry {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
#[serde(rename = "isDirectory")]
|
||||
pub is_directory: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let dir_path = Path::new(&path);
|
||||
|
||||
if !dir_path.exists() {
|
||||
return Err(format!("Directory does not exist: {}", path));
|
||||
}
|
||||
|
||||
if !dir_path.is_dir() {
|
||||
return Err(format!("Path is not a directory: {}", path));
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(dir_path)
|
||||
.map_err(|e| format!("Failed to read directory: {}", e))?;
|
||||
|
||||
let mut file_entries = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
let name = entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Skip hidden files by default (can be made configurable later)
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
file_entries.push(FileEntry {
|
||||
name,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
is_directory: path.is_dir(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(file_entries)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_file_content(path: String) -> Result<String, String> {
|
||||
use std::fs;
|
||||
|
||||
fs::read_to_string(&path)
|
||||
.map_err(|e| format!("Failed to read file: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_file_content(path: String, content: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
fs::write(&path, content)
|
||||
.map_err(|e| format!("Failed to write file: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_file(path: String) -> Result<(), String> {
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
if file_path.exists() {
|
||||
return Err("File already exists".to_string());
|
||||
}
|
||||
|
||||
File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_directory(path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let dir_path = Path::new(&path);
|
||||
|
||||
if dir_path.exists() {
|
||||
return Err("Directory already exists".to_string());
|
||||
}
|
||||
|
||||
fs::create_dir_all(dir_path).map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_file(path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
if !file_path.exists() {
|
||||
return Err("File does not exist".to_string());
|
||||
}
|
||||
|
||||
if file_path.is_dir() {
|
||||
return Err("Path is a directory, use delete_directory instead".to_string());
|
||||
}
|
||||
|
||||
fs::remove_file(file_path).map_err(|e| format!("Failed to delete file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_directory(path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let dir_path = Path::new(&path);
|
||||
|
||||
if !dir_path.exists() {
|
||||
return Err("Directory does not exist".to_string());
|
||||
}
|
||||
|
||||
if !dir_path.is_dir() {
|
||||
return Err("Path is not a directory".to_string());
|
||||
}
|
||||
|
||||
fs::remove_dir_all(dir_path).map_err(|e| format!("Failed to delete directory: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let old = Path::new(&old_path);
|
||||
let new = Path::new(&new_path);
|
||||
|
||||
if !old.exists() {
|
||||
return Err("Path does not exist".to_string());
|
||||
}
|
||||
|
||||
if new.exists() {
|
||||
return Err("Destination already exists".to_string());
|
||||
}
|
||||
|
||||
fs::rename(old, new).map_err(|e| format!("Failed to rename: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -151,6 +151,14 @@ pub fn run() {
|
||||
search_clipboard_entries,
|
||||
get_clipboard_languages,
|
||||
update_clipboard_language,
|
||||
list_directory,
|
||||
read_file_content,
|
||||
write_file_content,
|
||||
create_file,
|
||||
create_directory,
|
||||
delete_file,
|
||||
delete_directory,
|
||||
rename_path,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
Reference in New Issue
Block a user