mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 18:15:55 +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:
@@ -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