11import * as vscode from "vscode" ;
22import * as path from "path" ;
3+ import * as fs from "fs" ;
4+ import * as os from "os" ;
5+ import * as crypto from "crypto" ;
36import { Logger } from "../../logger" ;
47
58// Helper for generating IDs if uuid is not available or too heavy
@@ -10,6 +13,11 @@ function generateId(): string {
1013 ) ;
1114}
1215
16+ // Helper for generating project key hash
17+ function hashPath ( input : string ) : string {
18+ return crypto . createHash ( "md5" ) . update ( input ) . digest ( "hex" ) . substring ( 0 , 12 ) ;
19+ }
20+
1321export interface FavoriteFile {
1422 id : string ;
1523 path : string ; // stored relative if possible
@@ -32,38 +40,41 @@ interface FavoriteData {
3240 categories : FavoriteCategory [ ] ;
3341}
3442
35- const STORAGE_KEY = " fusi-tools.projectFavorites.data" ;
43+ const STORAGE_DIR = path . join ( os . homedir ( ) , ". fusi-tools" , "favorites" ) ;
3644
3745export class FavoritesManager {
3846 private _data : FavoriteData ;
3947 private _context : vscode . ExtensionContext ;
48+ private _storagePath : string | null = null ;
4049 private _onDidChangeTreeData : vscode . EventEmitter <
4150 void | FavoriteCategory | undefined
4251 > = new vscode . EventEmitter < void | FavoriteCategory | undefined > ( ) ;
4352 readonly onDidChangeTreeData : vscode . Event <
4453 void | FavoriteCategory | undefined
4554 > = this . _onDidChangeTreeData . event ;
46-
55+
4756 // 文件存在性缓存
4857 private _fileExistenceCache : Map < string , { exists : boolean ; timestamp : number } > = new Map ( ) ;
4958 private _cacheExpirationMs : number = 30000 ; // 30秒缓存过期时间
5059 private _checkTimer : NodeJS . Timeout | undefined ;
5160
5261 constructor ( context : vscode . ExtensionContext ) {
5362 this . _context = context ;
54- this . _data = this . _context . workspaceState . get < FavoriteData > ( STORAGE_KEY , {
55- files : { } ,
56- categories : [
57- {
58- id : "default" ,
59- name : "默认分类" ,
60- expanded : true ,
61- fileIds : [ ] ,
62- } ,
63- ] ,
64- } ) ;
63+ this . _storagePath = this . getStoragePath ( ) ;
6564
66- // Ensure data integrity (in case of corruption or fresh start)
65+ if ( this . _storagePath && fs . existsSync ( this . _storagePath ) ) {
66+ try {
67+ const content = fs . readFileSync ( this . _storagePath , "utf-8" ) ;
68+ this . _data = JSON . parse ( content ) ;
69+ } catch ( e ) {
70+ Logger . warn ( "读取配置文件失败,使用默认配置" , e ) ;
71+ this . _data = this . getDefaultData ( ) ;
72+ }
73+ } else {
74+ this . _data = this . getDefaultData ( ) ;
75+ }
76+
77+ // Ensure data integrity
6778 if ( ! this . _data . categories || this . _data . categories . length === 0 ) {
6879 this . _data . categories = [
6980 {
@@ -82,8 +93,91 @@ export class FavoritesManager {
8293 this . startPeriodicFileCheck ( ) ;
8394 }
8495
96+ private getDefaultData ( ) : FavoriteData {
97+ return {
98+ files : { } ,
99+ categories : [
100+ {
101+ id : "default" ,
102+ name : "默认分类" ,
103+ expanded : true ,
104+ fileIds : [ ] ,
105+ } ,
106+ ] ,
107+ } ;
108+ }
109+
110+ /**
111+ * 获取项目唯一标识
112+ * Git 仓库使用 git-common-dir 路径 hash(支持 worktree)
113+ * 非 Git 仓库使用 workspace 根目录路径 hash
114+ */
115+ private getProjectKey ( ) : string | null {
116+ const workspaceFolders = vscode . workspace . workspaceFolders ;
117+ if ( ! workspaceFolders || workspaceFolders . length === 0 ) {
118+ return null ;
119+ }
120+
121+ const rootPath = workspaceFolders [ 0 ] . uri . fsPath ;
122+ const gitDir = path . join ( rootPath , ".git" ) ;
123+
124+ // 检查是否是 Git 仓库
125+ if ( fs . existsSync ( gitDir ) ) {
126+ try {
127+ // 如果 .git 是文件(worktree 情况),读取其内容获取真正的 git 目录
128+ const gitStat = fs . statSync ( gitDir ) ;
129+ if ( gitStat . isFile ( ) ) {
130+ const gitContent = fs . readFileSync ( gitDir , "utf-8" ) ;
131+ // 格式: gitdir: /path/to/.git/worktrees/name
132+ const match = gitContent . match ( / g i t d i r : \s * ( .+ ) / ) ;
133+ if ( match ) {
134+ // 获取 git common dir (主仓库的 .git 目录)
135+ const worktreeGitDir = match [ 1 ] . trim ( ) ;
136+ // worktree 的 gitdir 通常在 .git/worktrees/xxx 下
137+ // common dir 是 worktreeGitDir 的父目录的父目录
138+ const commonDir = path . dirname ( path . dirname ( worktreeGitDir ) ) ;
139+ return hashPath ( commonDir ) ;
140+ }
141+ } else if ( gitStat . isDirectory ( ) ) {
142+ // 普通仓库,直接使用 .git 目录路径
143+ return hashPath ( gitDir ) ;
144+ }
145+ } catch ( e ) {
146+ Logger . warn ( "获取 Git 目录失败" , e ) ;
147+ }
148+ }
149+
150+ // 非 Git 仓库,使用 workspace 根目录
151+ return hashPath ( rootPath ) ;
152+ }
153+
154+ /**
155+ * 获取存储文件路径
156+ */
157+ private getStoragePath ( ) : string | null {
158+ const projectKey = this . getProjectKey ( ) ;
159+ if ( ! projectKey ) {
160+ return null ;
161+ }
162+ return path . join ( STORAGE_DIR , `${ projectKey } .json` ) ;
163+ }
164+
85165 private saveData ( ) {
86- this . _context . workspaceState . update ( STORAGE_KEY , this . _data ) ;
166+ if ( ! this . _storagePath ) {
167+ // 没有工作区时,回退到 workspaceState
168+ this . _context . workspaceState . update ( "fusi-tools.projectFavorites.data" , this . _data ) ;
169+ } else {
170+ try {
171+ // 确保目录存在
172+ const dir = path . dirname ( this . _storagePath ) ;
173+ if ( ! fs . existsSync ( dir ) ) {
174+ fs . mkdirSync ( dir , { recursive : true } ) ;
175+ }
176+ fs . writeFileSync ( this . _storagePath , JSON . stringify ( this . _data , null , 2 ) , "utf-8" ) ;
177+ } catch ( e ) {
178+ Logger . error ( "保存配置文件失败" , e ) ;
179+ }
180+ }
87181 this . _onDidChangeTreeData . fire ( ) ;
88182 }
89183
0 commit comments