完善功能
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -4932,6 +4932,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"notify",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
||||
@@ -26,4 +26,5 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
time = ">=0.3.0, <0.3.47"
|
||||
notify = "8.2.0"
|
||||
regex = "1"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user