mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 09:55:56 +08:00
fix: v5.1 代码审查修复 — ESLint/CSV/测试隔离/CLI 去重
- ESLint: 迁移到 flat config ignores,删除已废弃的 .eslintignore
- CSV: Rust/TS 格式对齐,统一 type,path,enabled 3 列
- JSON: 导入导出统一为 {path, enabled} 对象格式
- scanner: 移除未使用的 max_threads 死代码 + TempDirGuard 测试清理
- profiles: rename_profile 添加目标存在检查
- CLI: 抽取 load_operate_save helper,简化 cmd_remove/cmd_edit
- PathTable: 抽取 usePathValidation hook,消除 set-state-in-effect
- 测试隔离: disabled/profiles 通过 #[cfg(test)] 重定向到 temp dir
- toolchain: 新增 rust-toolchain.toml 固定 stable-x86_64-pc-windows-gnu
- docs: 更新 CLAUDE.md/README.md 测试计数 + 架构树
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { TargetType } from '@/core/undo-redo';
|
||||
import { usePathValidation } from '@/hooks/use-path-validation';
|
||||
import type { ValidationState } from '@/hooks/use-path-validation';
|
||||
|
||||
interface PathTableProps {
|
||||
tabId: 'system' | 'user';
|
||||
@@ -14,9 +15,6 @@ interface PathRow {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
type ValidationState = 'valid' | 'invalid' | 'unknown';
|
||||
const DEFAULT_VALIDATION_STATE: ValidationState = 'valid';
|
||||
|
||||
export function PathTable({ tabId }: PathTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const sysPaths = useAppStore((s) => s.sysPaths);
|
||||
@@ -29,42 +27,9 @@ export function PathTable({ tabId }: PathTableProps) {
|
||||
const paths = tabId === 'system' ? sysPaths : userPaths;
|
||||
const isActive = activeTab === tabId;
|
||||
|
||||
// 本次会话中已验证过的路径缓存(key=path, value=ValidationState)
|
||||
const [validationCache, setValidationCache] = useState<Map<string, ValidationState>>(new Map());
|
||||
// 环境变量展开结果缓存(key=path, value=expanded)
|
||||
const [expandedCache, setExpandedCache] = useState<Map<string, string>>(new Map());
|
||||
const { validationCache, expandedCache } = usePathValidation(paths);
|
||||
|
||||
const validatedRef = useRef<Set<string>>(new Set());
|
||||
const expandedRef = useRef<Set<string>>(new Set());
|
||||
|
||||
// 清理不再存在的路径缓存
|
||||
useEffect(() => {
|
||||
const currentKeys = new Set(paths.map(p => p.path));
|
||||
setValidationCache(prev => {
|
||||
let changed = false;
|
||||
const next = new Map(prev);
|
||||
for (const key of next.keys()) {
|
||||
if (!currentKeys.has(key)) { next.delete(key); changed = true; }
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
setExpandedCache(prev => {
|
||||
let changed = false;
|
||||
const next = new Map(prev);
|
||||
for (const key of next.keys()) {
|
||||
if (!currentKeys.has(key)) { next.delete(key); changed = true; }
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
for (const key of [...validatedRef.current]) {
|
||||
if (!currentKeys.has(key)) validatedRef.current.delete(key);
|
||||
}
|
||||
for (const key of [...expandedRef.current]) {
|
||||
if (!currentKeys.has(key)) expandedRef.current.delete(key);
|
||||
}
|
||||
}, [paths]);
|
||||
|
||||
// 过滤搜索
|
||||
// 搜索过滤
|
||||
const filtered = useMemo<PathRow[]>(() => {
|
||||
if (!searchQuery) return paths.map((p, i) => ({ path: p.path, index: i, enabled: p.enabled }));
|
||||
const q = searchQuery.toLowerCase();
|
||||
@@ -76,79 +41,15 @@ export function PathTable({ tabId }: PathTableProps) {
|
||||
return result;
|
||||
}, [paths, searchQuery]);
|
||||
|
||||
// 异步验证未缓存的路径
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const toValidate = paths.filter((p) => !validatedRef.current.has(p.path));
|
||||
if (toValidate.length === 0) return;
|
||||
|
||||
const batch = toValidate.slice(0, 20);
|
||||
Promise.all(
|
||||
batch.map(async (p): Promise<[string, ValidationState]> => {
|
||||
try {
|
||||
if (p.path.includes('%')) return [p.path, 'valid'];
|
||||
const valid: boolean = await invoke('validate_path', { path: p.path });
|
||||
return [p.path, valid ? 'valid' : 'invalid'];
|
||||
} catch {
|
||||
return [p.path, 'unknown'];
|
||||
}
|
||||
}),
|
||||
).then((results) => {
|
||||
if (cancelled) return;
|
||||
for (const [p] of results) validatedRef.current.add(p);
|
||||
setValidationCache((prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const [p, v] of results) next.set(p, v);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [paths]);
|
||||
|
||||
// 异步展开环境变量(用于 tooltip)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const toExpand = paths.filter(
|
||||
(p) => p.path.includes('%') && !expandedRef.current.has(p.path),
|
||||
);
|
||||
if (toExpand.length === 0) return;
|
||||
|
||||
const batch = toExpand.slice(0, 20);
|
||||
Promise.all(
|
||||
batch.map(async (p): Promise<[string, string]> => {
|
||||
try {
|
||||
const expanded: string = await invoke('expand_env_vars', { path: p.path });
|
||||
return [p.path, expanded !== p.path ? expanded : ''];
|
||||
} catch {
|
||||
return [p.path, ''];
|
||||
}
|
||||
}),
|
||||
).then((results) => {
|
||||
if (cancelled) return;
|
||||
for (const [p] of results) expandedRef.current.add(p);
|
||||
setExpandedCache((prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const [p, v] of results) next.set(p, v);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [paths]);
|
||||
|
||||
// 所有路径默认有效(异步验证结果回来后再精确染色)
|
||||
// 计算验证状态(含去重检测)
|
||||
const validations = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
return filtered.map(({ path }) => {
|
||||
const lower = path.toLowerCase();
|
||||
const isDuplicate = seen.has(lower);
|
||||
seen.add(lower);
|
||||
return {
|
||||
state: validationCache.get(path) ?? DEFAULT_VALIDATION_STATE,
|
||||
isDuplicate,
|
||||
isEnvVar: path.includes('%'),
|
||||
};
|
||||
const state: ValidationState = validationCache.get(path) ?? 'valid';
|
||||
return { state, isDuplicate, isEnvVar: path.includes('%') };
|
||||
});
|
||||
}, [filtered, validationCache]);
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { PathEntry } from '@/core/path-entry';
|
||||
|
||||
export type ValidationState = 'valid' | 'invalid' | 'unknown';
|
||||
|
||||
/**
|
||||
* 异步验证路径目录是否真实存在 + 展开环境变量
|
||||
* 缓存结果避免重复 IPC 调用。
|
||||
* setState 仅在异步 .then() 回调中调用(符合 React 规则),
|
||||
* 不存在路径的缓存清理通过 useMemo 派生。
|
||||
*/
|
||||
export function usePathValidation(paths: readonly PathEntry[]) {
|
||||
const validatedRef = useRef<Set<string>>(new Set());
|
||||
const expandedRef = useRef<Set<string>>(new Set());
|
||||
const [validationCache, setValidationCache] = useState<Map<string, ValidationState>>(new Map());
|
||||
const [expandedCache, setExpandedCache] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// 仅保留当前 paths 中存在的条目(派生 state,不在 effect 中同步 setState)
|
||||
const currentKeys = useMemo(() => new Set(paths.map((p) => p.path)), [paths]);
|
||||
const cleanedValidationCache = useMemo(() => {
|
||||
const next = new Map(validationCache);
|
||||
let changed = false;
|
||||
for (const key of next.keys()) {
|
||||
if (!currentKeys.has(key)) {
|
||||
next.delete(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : validationCache;
|
||||
}, [validationCache, currentKeys]);
|
||||
|
||||
const cleanedExpandedCache = useMemo(() => {
|
||||
const next = new Map(expandedCache);
|
||||
let changed = false;
|
||||
for (const key of next.keys()) {
|
||||
if (!currentKeys.has(key)) {
|
||||
next.delete(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : expandedCache;
|
||||
}, [expandedCache, currentKeys]);
|
||||
|
||||
// 同步清理 ref(ref 不能在 render 期间修改,放在 effect 中不 setState 是安全的)
|
||||
useEffect(() => {
|
||||
for (const key of validatedRef.current) {
|
||||
if (!currentKeys.has(key)) validatedRef.current.delete(key);
|
||||
}
|
||||
for (const key of expandedRef.current) {
|
||||
if (!currentKeys.has(key)) expandedRef.current.delete(key);
|
||||
}
|
||||
}, [currentKeys]);
|
||||
|
||||
// 异步验证路径(setState 在 .then() 回调中,符合 React 规则)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const toValidate = paths.filter((p) => !validatedRef.current.has(p.path));
|
||||
if (toValidate.length === 0) return;
|
||||
|
||||
const batch = toValidate.slice(0, 20);
|
||||
Promise.all(
|
||||
batch.map(
|
||||
async (p): Promise<[string, ValidationState]> => {
|
||||
try {
|
||||
if (p.path.includes('%')) return [p.path, 'valid'];
|
||||
const valid: boolean = await invoke('validate_path', { path: p.path });
|
||||
return [p.path, valid ? 'valid' : 'invalid'];
|
||||
} catch {
|
||||
return [p.path, 'unknown'];
|
||||
}
|
||||
},
|
||||
),
|
||||
).then((results) => {
|
||||
if (cancelled) return;
|
||||
for (const [p] of results) validatedRef.current.add(p);
|
||||
setValidationCache((prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const [p, v] of results) next.set(p, v);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [paths]);
|
||||
|
||||
// 异步展开环境变量(setState 在 .then() 回调中)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const toExpand = paths.filter(
|
||||
(p) => p.path.includes('%') && !expandedRef.current.has(p.path),
|
||||
);
|
||||
if (toExpand.length === 0) return;
|
||||
|
||||
const batch = toExpand.slice(0, 20);
|
||||
Promise.all(
|
||||
batch.map(
|
||||
async (p): Promise<[string, string]> => {
|
||||
try {
|
||||
const expanded: string = await invoke('expand_env_vars', { path: p.path });
|
||||
return [p.path, expanded !== p.path ? expanded : ''];
|
||||
} catch {
|
||||
return [p.path, ''];
|
||||
}
|
||||
},
|
||||
),
|
||||
).then((results) => {
|
||||
if (cancelled) return;
|
||||
for (const [p] of results) expandedRef.current.add(p);
|
||||
setExpandedCache((prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const [p, v] of results) next.set(p, v);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [paths]);
|
||||
|
||||
return { validationCache: cleanedValidationCache, expandedCache: cleanedExpandedCache };
|
||||
}
|
||||
Reference in New Issue
Block a user