Skip to content

Commit 86bab0e

Browse files
committed
feat(diagnostics): 添加文件行数统计和诊断视图增强
1 parent 2c64e62 commit 86bab0e

5 files changed

Lines changed: 677 additions & 158 deletions

File tree

src-tauri/src/diagnostics.rs

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use regex::Regex;
22
use serde::{Deserialize, Serialize};
33
use std::collections::HashSet;
44
use std::fs;
5+
use std::io::{BufRead, BufReader};
56
use std::path::Path;
67

78
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -253,8 +254,13 @@ fn scan_for_leaked_secrets(project_path: &Path) -> Vec<LeakedSecret> {
253254
// 要扫描的文件扩展名
254255
let scan_extensions = ["ts", "tsx", "js", "jsx", "py", "rs", "go", "java", "rb"];
255256

256-
// 要排除的目录
257-
let exclude_dirs = ["node_modules", "target", ".git", "dist", "build", "__pycache__", ".venv", "venv"];
257+
// 要排除的目录(包括构建产物)
258+
let exclude_dirs = [
259+
"node_modules", "target", ".git", "dist", "build", "__pycache__", ".venv", "venv",
260+
".next", ".nuxt", ".output", "out", ".turbo", ".vercel", ".netlify",
261+
"coverage", ".nyc_output", ".cache", ".parcel-cache",
262+
"chunks", "ssr", "static", // Next.js 内部目录
263+
];
258264

259265
scan_directory(project_path, &secret_pattern, &scan_extensions, &exclude_dirs, &mut secrets);
260266

@@ -335,3 +341,162 @@ fn scan_directory(
335341
}
336342
}
337343
}
344+
345+
/// 文件行数统计
346+
#[derive(Debug, Clone, Serialize, Deserialize)]
347+
pub struct FileLineCount {
348+
pub file: String,
349+
pub lines: usize,
350+
}
351+
352+
/// 扫描项目文件,按行数倒序返回
353+
pub fn scan_file_lines(project_path: &str, limit: usize, ignored_paths: &[String]) -> Result<Vec<FileLineCount>, String> {
354+
let path = Path::new(project_path);
355+
let mut files: Vec<FileLineCount> = Vec::new();
356+
357+
// 要扫描的文件扩展名
358+
let scan_extensions = [
359+
"ts", "tsx", "js", "jsx", "vue", "svelte",
360+
"py", "rs", "go", "java", "rb", "php",
361+
"css", "scss", "less",
362+
"html", "md", "json", "yaml", "yml", "toml",
363+
];
364+
365+
// 要排除的目录
366+
let exclude_dirs = [
367+
"node_modules", "target", ".git", "dist", "build", "__pycache__", ".venv", "venv",
368+
".next", ".nuxt", ".output", "out", ".turbo", ".vercel", ".netlify",
369+
"coverage", ".nyc_output", ".cache", ".parcel-cache",
370+
"chunks", "ssr", "static", ".svelte-kit",
371+
];
372+
373+
scan_files_recursive(path, path, &scan_extensions, &exclude_dirs, &mut files);
374+
375+
// 按行数倒序排序
376+
files.sort_by(|a, b| b.lines.cmp(&a.lines));
377+
378+
// 过滤掉用户忽略的路径(在限制条数之前)
379+
if !ignored_paths.is_empty() {
380+
files.retain(|f| {
381+
!ignored_paths.iter().any(|ignored| {
382+
f.file == *ignored || f.file.starts_with(&format!("{}/", ignored))
383+
})
384+
});
385+
}
386+
387+
// 限制返回数量
388+
files.truncate(limit);
389+
390+
Ok(files)
391+
}
392+
393+
fn scan_files_recursive(
394+
dir: &Path,
395+
root: &Path,
396+
extensions: &[&str],
397+
exclude_dirs: &[&str],
398+
files: &mut Vec<FileLineCount>,
399+
) {
400+
let entries = match fs::read_dir(dir) {
401+
Ok(e) => e,
402+
Err(_) => return,
403+
};
404+
405+
for entry in entries.flatten() {
406+
let path = entry.path();
407+
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
408+
409+
if path.is_dir() {
410+
if exclude_dirs.iter().any(|&d| file_name == d) {
411+
continue;
412+
}
413+
scan_files_recursive(&path, root, extensions, exclude_dirs, files);
414+
} else if path.is_file() {
415+
let ext = path.extension().unwrap_or_default().to_string_lossy();
416+
if !extensions.iter().any(|&e| ext == e) {
417+
continue;
418+
}
419+
420+
// 排除锁文件和自动生成的文件
421+
let excluded_files = [
422+
"package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb",
423+
"Cargo.lock", "poetry.lock", "Pipfile.lock", "composer.lock",
424+
".d.ts", // 类型声明文件
425+
];
426+
if excluded_files.iter().any(|&f| file_name.ends_with(f)) {
427+
continue;
428+
}
429+
430+
// 统计行数
431+
if let Ok(file) = fs::File::open(&path) {
432+
let reader = BufReader::new(file);
433+
let line_count = reader.lines().count();
434+
435+
// 获取相对路径(相对于项目根目录)
436+
let relative_path = path
437+
.strip_prefix(root)
438+
.unwrap_or(&path)
439+
.to_string_lossy()
440+
.to_string();
441+
442+
files.push(FileLineCount {
443+
file: relative_path,
444+
lines: line_count,
445+
});
446+
}
447+
}
448+
}
449+
}
450+
451+
/// 将 missing keys 添加到 .env 文件
452+
pub fn add_missing_keys_to_env(project_path: &str, keys: Vec<String>) -> Result<usize, String> {
453+
let path = Path::new(project_path);
454+
let env_path = path.join(".env");
455+
let env_example_path = path.join(".env.example");
456+
457+
// 读取 .env.example 获取默认值
458+
let example_values: std::collections::HashMap<String, String> = if env_example_path.exists() {
459+
let content = fs::read_to_string(&env_example_path).unwrap_or_default();
460+
content
461+
.lines()
462+
.filter_map(|line| {
463+
let line = line.trim();
464+
if line.is_empty() || line.starts_with('#') {
465+
return None;
466+
}
467+
line.find('=').map(|pos| {
468+
let key = line[..pos].trim().to_string();
469+
let value = line[pos + 1..].trim().to_string();
470+
(key, value)
471+
})
472+
})
473+
.collect()
474+
} else {
475+
std::collections::HashMap::new()
476+
};
477+
478+
// 读取现有 .env 内容
479+
let mut env_content = if env_path.exists() {
480+
fs::read_to_string(&env_path).unwrap_or_default()
481+
} else {
482+
String::new()
483+
};
484+
485+
// 确保以换行结尾
486+
if !env_content.is_empty() && !env_content.ends_with('\n') {
487+
env_content.push('\n');
488+
}
489+
490+
// 添加 missing keys
491+
let mut added_count = 0;
492+
for key in &keys {
493+
let default_value = example_values.get(key).cloned().unwrap_or_default();
494+
env_content.push_str(&format!("{}={}\n", key, default_value));
495+
added_count += 1;
496+
}
497+
498+
// 写入文件
499+
fs::write(&env_path, env_content).map_err(|e| e.to_string())?;
500+
501+
Ok(added_count)
502+
}

src/components/shared/FilePath.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ import {
99

1010
interface FilePathProps {
1111
path: string;
12+
basePath?: string;
1213
className?: string;
1314
showIcon?: boolean;
1415
}
1516

16-
export function FilePath({ path, className = "", showIcon = false }: FilePathProps) {
17+
export function FilePath({ path, basePath, className = "", showIcon = false }: FilePathProps) {
18+
// 显示相对路径,hover 显示绝对路径
19+
const displayPath = basePath && path.startsWith(basePath)
20+
? path.slice(basePath.length).replace(/^\//, '')
21+
: path;
22+
1723
const handleReveal = async () => {
1824
try {
1925
await invoke("reveal_path", { path });
@@ -46,7 +52,7 @@ export function FilePath({ path, className = "", showIcon = false }: FilePathPro
4652
title={path}
4753
>
4854
{showIcon && <FileIcon className="w-3 h-3 flex-shrink-0" />}
49-
<span className="truncate">{path}</span>
55+
<span className="truncate">{displayPath}</span>
5056
</span>
5157
</ContextMenuTrigger>
5258
<ContextMenuContent>

src/views/Workspace/GitHistory.tsx

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ interface GitHistoryProps {
2626
projectPath: string;
2727
features: Feature[];
2828
onRefresh?: () => void;
29+
embedded?: boolean;
2930
}
3031

3132
type ViewMode = "feats" | "timeline";
3233

33-
export function GitHistory({ projectPath, features, onRefresh }: GitHistoryProps) {
34+
export function GitHistory({ projectPath, features, onRefresh, embedded = false }: GitHistoryProps) {
3435
const [commits, setCommits] = useState<CommitInfo[]>([]);
3536
const [loading, setLoading] = useState(true);
3637
const [viewMode, setViewMode] = useState<ViewMode>("feats");
@@ -165,36 +166,54 @@ export function GitHistory({ projectPath, features, onRefresh }: GitHistoryProps
165166
}
166167

167168
return (
168-
<div className="border-t border-border">
169-
{/* Header */}
170-
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
171-
<div className="flex items-center gap-3">
172-
<h3 className="text-sm font-medium text-ink">Git History</h3>
169+
<div className={embedded ? "" : "border-t border-border"}>
170+
{/* Header - only show when not embedded */}
171+
{!embedded && (
172+
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
173+
<div className="flex items-center gap-3">
174+
<h3 className="text-sm font-medium text-ink">Git History</h3>
175+
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as ViewMode)}>
176+
<TabsList className="h-7">
177+
<TabsTrigger value="feats" className="text-xs px-2 py-1">
178+
Feats
179+
</TabsTrigger>
180+
<TabsTrigger value="timeline" className="text-xs px-2 py-1">
181+
Timeline
182+
</TabsTrigger>
183+
</TabsList>
184+
</Tabs>
185+
</div>
186+
<DropdownMenu>
187+
<DropdownMenuTrigger className="p-1 hover:bg-muted rounded">
188+
<DotsHorizontalIcon className="w-4 h-4 text-muted-foreground" />
189+
</DropdownMenuTrigger>
190+
<DropdownMenuContent align="end">
191+
<DropdownMenuItem onClick={handleExportChangelog}>
192+
Export Changelog
193+
</DropdownMenuItem>
194+
</DropdownMenuContent>
195+
</DropdownMenu>
196+
</div>
197+
)}
198+
199+
{/* Embedded mode: show tabs inline */}
200+
{embedded && (
201+
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
173202
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as ViewMode)}>
174-
<TabsList className="h-7">
175-
<TabsTrigger value="feats" className="text-xs px-2 py-1">
203+
<TabsList className="h-6">
204+
<TabsTrigger value="feats" className="text-[10px] px-2 py-0.5">
176205
Feats
177206
</TabsTrigger>
178-
<TabsTrigger value="timeline" className="text-xs px-2 py-1">
207+
<TabsTrigger value="timeline" className="text-[10px] px-2 py-0.5">
179208
Timeline
180209
</TabsTrigger>
181210
</TabsList>
182211
</Tabs>
183212
</div>
184-
<DropdownMenu>
185-
<DropdownMenuTrigger className="p-1 hover:bg-muted rounded">
186-
<DotsHorizontalIcon className="w-4 h-4 text-muted-foreground" />
187-
</DropdownMenuTrigger>
188-
<DropdownMenuContent align="end">
189-
<DropdownMenuItem onClick={handleExportChangelog}>
190-
Export Changelog
191-
</DropdownMenuItem>
192-
</DropdownMenuContent>
193-
</DropdownMenu>
194-
</div>
213+
)}
195214

196215
{/* Content */}
197-
<div className="max-h-[300px] overflow-y-auto">
216+
<div className={embedded ? "overflow-y-auto" : "max-h-[300px] overflow-y-auto"}>
198217
{viewMode === "feats" ? (
199218
<FeatsView
200219
commitsByFeat={commitsByFeat}

0 commit comments

Comments
 (0)