feat: 0.7.0 文件夹对比增强(语义判等/子目录树形/搜索与状态过滤/报告导出/差异文件导航/大目录单点失败降级)+ 字符级分块锚定恢复(0.6.3 算法)与滚动回弹根因修复(重算期间保留旧结果撑住容器高度)+ E2E 滚动稳定性回归锚

This commit is contained in:
2026-08-18 18:40:03 +08:00
parent 74d52ec0b0
commit 1c52f5d3cb
25 changed files with 2703 additions and 281 deletions
+34 -6
View File
@@ -5,7 +5,27 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import fs from 'fs'
import { parseState, clampBounds, type AppState } from './windowState'
import { decodeText, type DecodeResult } from './decode'
import { scanFolders } from './folderScan'
import { scanFolders, SEMANTIC_MAX_BYTES } from './folderScan'
/**
* 文本类扩展名清单(小写,不含点):
* 与渲染进程 TEXT_EXTENSIONSsrc/renderer/src/diff/textUtils.ts)完全对齐,两进程无法共享模块,
* 改动需双向同步。供打开文件对话框筛选与文件夹语义判等的文本文件判定共用。
*/
const TEXT_FILE_EXTENSIONS = [
'txt', 'text', 'md', 'markdown', 'json', 'js', 'mjs', 'cjs', 'ts', 'mts', 'cts', 'tsx', 'jsx',
'css', 'scss', 'less', 'html', 'htm', 'xml', 'yml', 'yaml', 'toml', 'ini', 'cfg', 'conf',
'config', 'properties', 'env', 'py', 'java', 'go', 'rs', 'c', 'h', 'cpp', 'hpp', 'cc', 'cs',
'swift', 'kt', 'rb', 'php', 'pl', 'sh', 'zsh', 'bash', 'fish', 'bat', 'cmd', 'ps1', 'log',
'csv', 'tsv', 'sql', 'vue', 'svelte', 'graphql', 'gql', 'gradle', 'lock'
]
/** 相对路径是否为文本文件(按扩展名,语义判等文本判定共用) */
function isTextRel(rel: string): boolean {
const dot = rel.lastIndexOf('.')
if (dot < 0) return true
return TEXT_FILE_EXTENSIONS.includes(rel.slice(dot + 1).toLowerCase())
}
/** 应用状态持久化文件(窗口 bounds + 上次文件目录),位于 userData 目录 */
const stateFile = (): string => join(app.getPath('userData'), 'window-state.json')
@@ -182,8 +202,7 @@ ipcMain.handle('file:open', async (_event, side: 'left' | 'right' | null) => {
defaultPath: appState.lastDir,
properties: ['openFile'],
filters: [
// 与渲染进程 TEXT_EXTENSIONSsrc/renderer/src/diff/textUtils.ts)完全对齐;两进程无法共享模块,改动需双向同步
{ name: '文本文件', extensions: ['txt', 'text', 'md', 'markdown', 'json', 'js', 'mjs', 'cjs', 'ts', 'mts', 'cts', 'tsx', 'jsx', 'css', 'scss', 'less', 'html', 'htm', 'xml', 'yml', 'yaml', 'toml', 'ini', 'cfg', 'conf', 'config', 'properties', 'env', 'py', 'java', 'go', 'rs', 'c', 'h', 'cpp', 'hpp', 'cc', 'cs', 'swift', 'kt', 'rb', 'php', 'pl', 'sh', 'zsh', 'bash', 'fish', 'bat', 'cmd', 'ps1', 'log', 'csv', 'tsv', 'sql', 'vue', 'svelte', 'graphql', 'gql', 'gradle', 'lock'] },
{ name: '文本文件', extensions: TEXT_FILE_EXTENSIONS },
// 兜底入口:允许打开任意扩展名文件
{ name: '所有文件', extensions: ['*'] }
]
@@ -244,10 +263,19 @@ ipcMain.handle('folder:pick', async () => {
return dirPath
})
/** IPC: 扫描对比两个文件夹(递归对齐 + 字节级判定,异步 fs 不阻塞主进程) */
ipcMain.handle('folder:scan', async (_event, leftDir: string, rightDir: string) => {
/** IPC: 扫描对比两个文件夹(递归对齐 + 字节级判定 + 可选语义判等;异步 fs 与解码 worker 不阻塞主进程) */
ipcMain.handle('folder:scan', async (_event, leftDir: string, rightDir: string, semantic: boolean) => {
try {
return await scanFolders(leftDir, rightDir)
return await scanFolders(leftDir, rightDir, semantic
? {
// 语义判等解码复用解码 worker(大缓冲后台解码,与文件打开共用同一管线)
semantic: {
maxBytes: SEMANTIC_MAX_BYTES,
isTextFile: isTextRel,
decode: async (buf) => (await decodeMaybeWorker(buf)).text
}
}
: {})
} catch {
// 目录不存在/不可读等:转为错误标记,渲染端提示
return { error: 'scan-failed' as const }