完善功能

This commit is contained in:
2026-07-10 16:07:53 +08:00
parent 0a3534a7e0
commit 78913c46b6
10 changed files with 971 additions and 31 deletions

View File

@@ -7,6 +7,7 @@ use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
use tauri::{Emitter, Manager};
use tauri_plugin_dialog::DialogExt;
use base64::Engine;
use regex::Regex;
#[derive(Debug, Clone, serde::Serialize)]
struct DirEntry {
@@ -33,6 +34,23 @@ struct WorkspaceWatchErrorPayload {
message: String,
}
#[derive(Debug, Clone, serde::Serialize)]
struct SearchMatch {
line: usize,
column: usize,
line_text: String,
context_before: Vec<String>,
context_after: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
struct FileSearchResult {
relative_path: String,
absolute_path: String,
matches: Vec<SearchMatch>,
match_count: usize,
}
fn is_hidden_entry(entry: &fs::DirEntry) -> bool {
let name = entry.file_name();
let is_dot_hidden = name.to_string_lossy().starts_with('.');
@@ -312,6 +330,162 @@ fn rename_entry(path: String, new_name: String) -> Result<String, String> {
Ok(new_path.to_string_lossy().to_string())
}
#[tauri::command]
fn search_files(
workspace: String,
keyword: String,
case_sensitive: bool,
use_regex: bool,
whole_word: bool,
extensions: Vec<String>,
) -> Result<Vec<FileSearchResult>, String> {
if keyword.is_empty() {
return Ok(Vec::new());
}
let pattern = if use_regex {
keyword.clone()
} else {
regex::escape(&keyword)
};
let mut builder = regex::RegexBuilder::new(&pattern);
builder.case_insensitive(!case_sensitive);
let re = builder
.build()
.map_err(|e| format!("正则表达式无效: {}", e))?;
let mut results: Vec<FileSearchResult> = Vec::new();
let workspace_path = std::path::Path::new(&workspace);
walk_dir(
workspace_path,
workspace_path,
&re,
whole_word,
&extensions,
&mut results,
)?;
results.sort_by(|a, b| b.match_count.cmp(&a.match_count));
Ok(results)
}
fn walk_dir(
base: &std::path::Path,
dir: &std::path::Path,
re: &Regex,
whole_word: bool,
extensions: &[String],
results: &mut Vec<FileSearchResult>,
) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|e| format!("无法读取目录: {}", e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?;
if is_hidden_entry(&entry) {
continue;
}
let path = entry.path();
if path.is_dir() {
walk_dir(base, &path, re, whole_word, extensions, results)?;
} else if path.is_file() {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if !extensions.iter().any(|e| e == &ext) {
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
let lines: Vec<&str> = content.lines().collect();
let mut matches: Vec<SearchMatch> = Vec::new();
for (i, line) in lines.iter().enumerate() {
let has_match = if whole_word {
re.find_iter(line).any(|m| {
let start = m.start();
let end = m.end();
let prev_char = line[..start]
.chars()
.last()
.map(|c| !c.is_alphanumeric() && c != '_')
.unwrap_or(true);
let next_char = line[end..]
.chars()
.next()
.map(|c| !c.is_alphanumeric() && c != '_')
.unwrap_or(true);
prev_char && next_char
})
} else {
re.is_match(line)
};
if has_match {
let column = re
.find(line)
.map(|m| line[..m.start()].chars().count() + 1)
.unwrap_or(1);
let context_before: Vec<String> = (0..i)
.rev()
.take(2)
.map(|j| lines[j].to_string())
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let context_after: Vec<String> = lines
.iter()
.skip(i + 1)
.take(2)
.map(|s| s.to_string())
.collect();
matches.push(SearchMatch {
line: i + 1,
column,
line_text: line.to_string(),
context_before,
context_after,
});
if matches.len() >= 100 {
break;
}
}
}
if !matches.is_empty() {
let relative_path = path
.strip_prefix(base)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
let match_count = matches.len();
results.push(FileSearchResult {
relative_path,
absolute_path: path.to_string_lossy().to_string(),
matches,
match_count,
});
}
}
}
Ok(())
}
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
@@ -336,7 +510,8 @@ pub fn run() {
create_file,
delete_entry,
rename_entry,
save_media_file
save_media_file,
search_files
])
.setup(|app| {
// ---- File menu ----
@@ -400,9 +575,14 @@ pub fn run() {
let toggle_outline = MenuItemBuilder::with_id("toggle_outline", "切换大纲")
.accelerator("CmdOrCtrl+B")
.build(app)?;
let search = MenuItemBuilder::with_id("search", "搜索")
.accelerator("CmdOrCtrl+Shift+F")
.build(app)?;
let view_menu = SubmenuBuilder::new(app, "视图")
.item(&toggle_outline)
.separator()
.item(&search)
.build()?;
// ---- Help menu ----
@@ -453,6 +633,9 @@ pub fn run() {
"about" => {
let _ = app_handle.emit("show-about", ());
}
"search" => {
let _ = app_handle.emit("menu-search", ());
}
_ => {
// Forward other menu events to frontend
let _ = app_handle.emit("menu-event", id);