增加文件管理器,增加文件编辑

This commit is contained in:
2025-08-09 13:44:52 +08:00
parent 5e532ad83f
commit 1f13548039
13 changed files with 4384 additions and 10 deletions

View File

@@ -0,0 +1,264 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use tauri::Emitter;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileNode {
pub name: String,
pub path: String,
pub file_type: String, // "file" or "directory"
pub children: Option<Vec<FileNode>>,
pub size: Option<u64>,
pub modified: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileSystemChange {
pub path: String,
pub change_type: String, // "created", "modified", "deleted"
}
/// 读取文件内容
#[tauri::command]
pub async fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path)
.map_err(|e| format!("Failed to read file: {}", e))
}
/// 写入文件内容
#[tauri::command]
pub async fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content)
.map_err(|e| format!("Failed to write file: {}", e))
}
/// 读取目录树结构
#[tauri::command]
pub async fn read_directory_tree(
path: String,
max_depth: Option<u32>,
ignore_patterns: Option<Vec<String>>,
) -> Result<FileNode, String> {
let path = Path::new(&path);
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
let max_depth = max_depth.unwrap_or(5);
let ignore_patterns = ignore_patterns.unwrap_or_else(|| vec![
String::from("node_modules"),
String::from(".git"),
String::from("target"),
String::from("dist"),
String::from("build"),
String::from(".idea"),
String::from(".vscode"),
String::from("__pycache__"),
String::from(".DS_Store"),
]);
read_directory_recursive(path, 0, max_depth, &ignore_patterns)
.map_err(|e| e.to_string())
}
fn read_directory_recursive(
path: &Path,
current_depth: u32,
max_depth: u32,
ignore_patterns: &[String],
) -> std::io::Result<FileNode> {
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let metadata = fs::metadata(path)?;
let node = if metadata.is_dir() {
let mut children = Vec::new();
if current_depth < max_depth {
// Check if directory should be ignored
let should_ignore = ignore_patterns.iter().any(|pattern| {
&name == pattern || name.starts_with('.')
});
if !should_ignore {
let entries = fs::read_dir(path)?;
for entry in entries {
let entry = entry?;
let child_path = entry.path();
// Skip symlinks to avoid infinite loops
if let Ok(meta) = entry.metadata() {
if !meta.file_type().is_symlink() {
if let Ok(child_node) = read_directory_recursive(
&child_path,
current_depth + 1,
max_depth,
ignore_patterns,
) {
children.push(child_node);
}
}
}
}
// Sort children: directories first, then files, alphabetically
children.sort_by(|a, b| {
match (a.file_type.as_str(), b.file_type.as_str()) {
("directory", "file") => std::cmp::Ordering::Less,
("file", "directory") => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
}
});
}
}
FileNode {
name,
path: path.to_string_lossy().to_string(),
file_type: String::from("directory"),
children: Some(children),
size: None,
modified: metadata.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs()),
}
} else {
FileNode {
name,
path: path.to_string_lossy().to_string(),
file_type: String::from("file"),
children: None,
size: Some(metadata.len()),
modified: metadata.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs()),
}
};
Ok(node)
}
/// 搜索文件
#[tauri::command]
pub async fn search_files_by_name(
base_path: String,
query: String,
max_results: Option<usize>,
) -> Result<Vec<String>, String> {
let base_path = Path::new(&base_path);
if !base_path.exists() {
return Err(format!("Path does not exist: {}", base_path.display()));
}
let query_lower = query.to_lowercase();
let max_results = max_results.unwrap_or(100);
let mut results = Vec::new();
search_recursive(base_path, &query_lower, &mut results, max_results)?;
Ok(results)
}
fn search_recursive(
dir: &Path,
query: &str,
results: &mut Vec<String>,
max_results: usize,
) -> Result<(), String> {
if results.len() >= max_results {
return Ok(());
}
let entries = fs::read_dir(dir)
.map_err(|e| format!("Failed to read directory: {}", e))?;
for entry in entries {
if results.len() >= max_results {
break;
}
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let path = entry.path();
let file_name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
if file_name.contains(query) {
results.push(path.to_string_lossy().to_string());
}
if path.is_dir() {
// Skip hidden directories and common ignore patterns
if !file_name.starts_with('.')
&& file_name != "node_modules"
&& file_name != "target"
&& file_name != "dist" {
let _ = search_recursive(&path, query, results, max_results);
}
}
}
Ok(())
}
/// 获取文件信息
#[tauri::command]
pub async fn get_file_info(path: String) -> Result<FileNode, String> {
let path = Path::new(&path);
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
let metadata = fs::metadata(path)
.map_err(|e| format!("Failed to get metadata: {}", e))?;
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
Ok(FileNode {
name,
path: path.to_string_lossy().to_string(),
file_type: if metadata.is_dir() {
String::from("directory")
} else {
String::from("file")
},
children: None,
size: if metadata.is_file() {
Some(metadata.len())
} else {
None
},
modified: metadata.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs()),
})
}
/// 监听文件系统变化(简化版本)
#[tauri::command]
pub async fn watch_directory(
app: tauri::AppHandle,
path: String,
) -> Result<(), String> {
// 这里可以集成 notify crate 来实现文件系统监听
// 为了简化,先返回成功
// 发送测试事件
app.emit("file-system-change", FileSystemChange {
path: path.clone(),
change_type: String::from("watching"),
}).map_err(|e| e.to_string())?;
Ok(())
}

View File

@@ -0,0 +1,426 @@
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitStatus {
pub branch: String,
pub ahead: u32,
pub behind: u32,
pub staged: Vec<GitFileStatus>,
pub modified: Vec<GitFileStatus>,
pub untracked: Vec<GitFileStatus>,
pub conflicted: Vec<GitFileStatus>,
pub is_clean: bool,
pub remote_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitFileStatus {
pub path: String,
pub status: String, // "modified", "added", "deleted", "renamed"
pub staged: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitCommit {
pub hash: String,
pub short_hash: String,
pub author: String,
pub email: String,
pub date: String,
pub message: String,
pub files_changed: u32,
pub insertions: u32,
pub deletions: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GitBranch {
pub name: String,
pub is_current: bool,
pub remote: Option<String>,
pub last_commit: Option<String>,
}
/// 获取 Git 状态
#[tauri::command]
pub async fn get_git_status(path: String) -> Result<GitStatus, String> {
let path = Path::new(&path);
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
// Check if it's a git repository
let git_check = Command::new("git")
.arg("rev-parse")
.arg("--git-dir")
.current_dir(path)
.output()
.map_err(|e| format!("Failed to execute git command: {}", e))?;
if !git_check.status.success() {
return Err("Not a git repository".to_string());
}
// Get current branch
let branch_output = Command::new("git")
.args(&["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to get branch: {}", e))?;
let branch = String::from_utf8_lossy(&branch_output.stdout)
.trim()
.to_string();
// Get remote tracking info
let (ahead, behind) = get_tracking_info(path)?;
// Get status
let status_output = Command::new("git")
.args(&["status", "--porcelain=v1"])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to get status: {}", e))?;
let status_text = String::from_utf8_lossy(&status_output.stdout);
let (staged, modified, untracked, conflicted) = parse_git_status(&status_text);
// Get remote URL
let remote_output = Command::new("git")
.args(&["remote", "get-url", "origin"])
.current_dir(path)
.output()
.ok();
let remote_url = remote_output
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
});
let is_clean = staged.is_empty() && modified.is_empty() && untracked.is_empty();
Ok(GitStatus {
branch,
ahead,
behind,
staged,
modified,
untracked,
conflicted,
is_clean,
remote_url,
})
}
fn get_tracking_info(path: &Path) -> Result<(u32, u32), String> {
// Get ahead/behind counts
let ahead_output = Command::new("git")
.args(&["rev-list", "--count", "@{u}..HEAD"])
.current_dir(path)
.output();
let behind_output = Command::new("git")
.args(&["rev-list", "--count", "HEAD..@{u}"])
.current_dir(path)
.output();
let ahead = ahead_output
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u32>()
.ok()
} else {
None
}
})
.unwrap_or(0);
let behind = behind_output
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u32>()
.ok()
} else {
None
}
})
.unwrap_or(0);
Ok((ahead, behind))
}
fn parse_git_status(status_text: &str) -> (Vec<GitFileStatus>, Vec<GitFileStatus>, Vec<GitFileStatus>, Vec<GitFileStatus>) {
let mut staged = Vec::new();
let mut modified = Vec::new();
let mut untracked = Vec::new();
let mut conflicted = Vec::new();
for line in status_text.lines() {
if line.len() < 3 {
continue;
}
let status_code = &line[0..2];
let file_path = line[3..].trim().to_string();
match status_code {
"M " => modified.push(GitFileStatus {
path: file_path,
status: "modified".to_string(),
staged: false,
}),
" M" => modified.push(GitFileStatus {
path: file_path,
status: "modified".to_string(),
staged: false,
}),
"MM" => {
staged.push(GitFileStatus {
path: file_path.clone(),
status: "modified".to_string(),
staged: true,
});
modified.push(GitFileStatus {
path: file_path,
status: "modified".to_string(),
staged: false,
});
},
"A " | "AM" => staged.push(GitFileStatus {
path: file_path,
status: "added".to_string(),
staged: true,
}),
"D " => staged.push(GitFileStatus {
path: file_path,
status: "deleted".to_string(),
staged: true,
}),
" D" => modified.push(GitFileStatus {
path: file_path,
status: "deleted".to_string(),
staged: false,
}),
"R " => staged.push(GitFileStatus {
path: file_path,
status: "renamed".to_string(),
staged: true,
}),
"??" => untracked.push(GitFileStatus {
path: file_path,
status: "untracked".to_string(),
staged: false,
}),
"UU" | "AA" | "DD" => conflicted.push(GitFileStatus {
path: file_path,
status: "conflicted".to_string(),
staged: false,
}),
_ => {}
}
}
(staged, modified, untracked, conflicted)
}
/// 获取 Git 提交历史
#[tauri::command]
pub async fn get_git_history(
path: String,
limit: Option<usize>,
branch: Option<String>,
) -> Result<Vec<GitCommit>, String> {
let path = Path::new(&path);
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
let limit = limit.unwrap_or(50);
let branch = branch.unwrap_or_else(|| "HEAD".to_string());
// Get commit logs with stats
let log_output = Command::new("git")
.args(&[
"log",
&branch,
&format!("-{}", limit),
"--pretty=format:%H|%h|%an|%ae|%ad|%s",
"--date=iso",
"--numstat",
])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to get git history: {}", e))?;
if !log_output.status.success() {
return Err("Failed to get git history".to_string());
}
let log_text = String::from_utf8_lossy(&log_output.stdout);
parse_git_log(&log_text)
}
fn parse_git_log(log_text: &str) -> Result<Vec<GitCommit>, String> {
let mut commits = Vec::new();
let mut current_commit: Option<GitCommit> = None;
let mut files_changed = 0u32;
let mut insertions = 0u32;
let mut deletions = 0u32;
for line in log_text.lines() {
if line.contains('|') && line.matches('|').count() == 5 {
// Save previous commit if exists
if let Some(mut commit) = current_commit.take() {
commit.files_changed = files_changed;
commit.insertions = insertions;
commit.deletions = deletions;
commits.push(commit);
}
// Reset counters
files_changed = 0;
insertions = 0;
deletions = 0;
// Parse new commit
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 6 {
current_commit = Some(GitCommit {
hash: parts[0].to_string(),
short_hash: parts[1].to_string(),
author: parts[2].to_string(),
email: parts[3].to_string(),
date: parts[4].to_string(),
message: parts[5].to_string(),
files_changed: 0,
insertions: 0,
deletions: 0,
});
}
} else if !line.trim().is_empty() && current_commit.is_some() {
// Parse numstat line
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(added) = parts[0].parse::<u32>() {
insertions += added;
}
if let Ok(removed) = parts[1].parse::<u32>() {
deletions += removed;
}
files_changed += 1;
}
}
}
// Save last commit
if let Some(mut commit) = current_commit {
commit.files_changed = files_changed;
commit.insertions = insertions;
commit.deletions = deletions;
commits.push(commit);
}
Ok(commits)
}
/// 获取 Git 分支列表
#[tauri::command]
pub async fn get_git_branches(path: String) -> Result<Vec<GitBranch>, String> {
let path = Path::new(&path);
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
// Get all branches
let branch_output = Command::new("git")
.args(&["branch", "-a", "-v"])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to get branches: {}", e))?;
if !branch_output.status.success() {
return Err("Failed to get branches".to_string());
}
let branch_text = String::from_utf8_lossy(&branch_output.stdout);
let mut branches = Vec::new();
for line in branch_text.lines() {
let is_current = line.starts_with('*');
let line = line.trim_start_matches('*').trim();
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let name = parts[0].to_string();
let last_commit = if parts.len() > 1 {
Some(parts[1].to_string())
} else {
None
};
let remote = if name.starts_with("remotes/") {
Some(name.trim_start_matches("remotes/").to_string())
} else {
None
};
branches.push(GitBranch {
name: name.trim_start_matches("remotes/").to_string(),
is_current,
remote,
last_commit,
});
}
Ok(branches)
}
/// 获取文件的 Git diff
#[tauri::command]
pub async fn get_git_diff(
path: String,
file_path: Option<String>,
staged: Option<bool>,
) -> Result<String, String> {
let path = Path::new(&path);
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
let mut cmd = Command::new("git");
cmd.arg("diff");
if staged.unwrap_or(false) {
cmd.arg("--cached");
}
if let Some(file) = file_path {
cmd.arg(file);
}
let diff_output = cmd
.current_dir(path)
.output()
.map_err(|e| format!("Failed to get diff: {}", e))?;
if !diff_output.status.success() {
return Err("Failed to get diff".to_string());
}
Ok(String::from_utf8_lossy(&diff_output.stdout).to_string())
}