Skip to content
This repository was archived by the owner on Apr 13, 2026. It is now read-only.

Commit a968e88

Browse files
EndeLiucongju.lt
andauthored
Feat/chokidar (#140)
* fix: 修正编辑器打开文件条件判断 * feat: 新增文件系统监听功能 --------- Co-authored-by: congju.lt <congju.lt@antgroup.com>
1 parent 71fd8b1 commit a968e88

6 files changed

Lines changed: 143 additions & 13 deletions

File tree

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"autoprefixer": "^10.4.24",
7373
"babel-plugin-react-compiler": "^1.0.0",
7474
"bumpp": "^10.4.0",
75+
"chokidar": "^5.0.0",
7576
"class-variance-authority": "^0.7.1",
7677
"clsx": "^2.1.1",
7778
"date-fns": "^4.1.0",

src/main/plugins/fs/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,30 @@ import type { Plugin } from '@neovate/code';
55
import { editorControllers } from './editor';
66
import { getFileTree } from './tree';
77
import { searchFiles } from './search';
8+
import { watchWorkspace, unwatchWorkspace } from './watch';
9+
10+
function debounce<T extends (...args: any[]) => any>(
11+
func: T,
12+
delay: number,
13+
): (...args: Parameters<T>) => void {
14+
let timeoutId: NodeJS.Timeout | null = null;
15+
16+
return (...args: Parameters<T>) => {
17+
if (timeoutId) {
18+
clearTimeout(timeoutId);
19+
}
20+
21+
timeoutId = setTimeout(() => {
22+
func(...args);
23+
}, delay);
24+
};
25+
}
826

927
export const fsPlugin: Plugin = {
1028
name: 'fs',
1129

1230
nodeBridgeHandler() {
31+
const context = this; // plugin context, include message bus
1332
return {
1433
'fs.tree': async (data: { cwd: string }) => {
1534
/** TODO: 后续考虑大项目的性能问题 */
@@ -102,6 +121,22 @@ export const fsPlugin: Plugin = {
102121
};
103122
}
104123
},
124+
'fs.watch': async (data: { cwd: string }) => {
125+
// 创建防抖后的回调函数
126+
const debouncedEmit = debounce((subType: string) => {
127+
console.log('onFsChange', subType);
128+
context.messageBus?.emitEvent?.('fs-change', { cwd: data.cwd });
129+
}, 600);
130+
131+
watchWorkspace(data.cwd, {
132+
onFsChange: debouncedEmit,
133+
});
134+
return { success: true, data: {} };
135+
},
136+
'fs.unwatch': async (data: { cwd: string }) => {
137+
unwatchWorkspace(data.cwd);
138+
return { success: true, data: {} };
139+
},
105140
...editorControllers,
106141
} as ReturnType<NonNullable<Plugin['nodeBridgeHandler']>>;
107142
},

src/main/plugins/fs/watch.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import chokidar from 'chokidar';
2+
import type { FSWatcher } from 'chokidar';
3+
4+
const WATCHING_SET = new Map<string, FSWatcher>();
5+
6+
export function watchWorkspace(
7+
dir: string,
8+
callbacks: {
9+
// includes file system change, add, removed etc, not include file content change
10+
onFsChange: (subType: string) => void;
11+
},
12+
) {
13+
const { onFsChange } = callbacks;
14+
if (WATCHING_SET.has(dir)) {
15+
return;
16+
}
17+
const watcher = chokidar.watch(dir, {
18+
ignored: [
19+
/(^|[\/\\])\../, // 隐藏文件
20+
/node_modules/,
21+
'**/package-lock.json',
22+
'**/yarn.lock',
23+
'**/dist/**',
24+
'**/build/**',
25+
'**/out/**',
26+
'**/.git/**',
27+
'**/.DS_Store',
28+
'**/.cache/**',
29+
'**/coverage/**',
30+
/.*\.(jpg|jpeg|png|gif|svg|mp4|mp3)$/i, // 媒体文件
31+
],
32+
depth: 5,
33+
// usePolling: true,
34+
// interval: 2000,
35+
followSymlinks: false,
36+
alwaysStat: false,
37+
persistent: true,
38+
ignoreInitial: true, // otherwise will trigger huge amount of events when init
39+
});
40+
WATCHING_SET.set(dir, watcher);
41+
watcher
42+
.on('add', (e) => {
43+
console.log('add', e);
44+
onFsChange('add');
45+
})
46+
.on('unlink', () => {
47+
onFsChange('unlink');
48+
})
49+
.on('addDir', () => {
50+
onFsChange('addDir');
51+
})
52+
.on('unlinkDir', () => {
53+
onFsChange('unlinkDir');
54+
})
55+
.on('error', (e) => {
56+
console.log('watcher error', e);
57+
unwatchWorkspace(dir);
58+
});
59+
}
60+
61+
export function unwatchWorkspace(dir: string) {
62+
const watcher = WATCHING_SET.get(dir);
63+
64+
if (!watcher) {
65+
return;
66+
}
67+
watcher.close();
68+
WATCHING_SET.delete(dir);
69+
}

src/renderer/components/ContentPanel/panes/EditorPane.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export function EditorPane({ isActive, onReady }: EditorPaneProps) {
9696

9797
// 文件打开行为承接
9898
useEffect(() => {
99-
if (pendingTabUri?.type === 'editor' && status === 'ready') {
99+
if (pendingTabUri?.type === 'editor' && extensionReady) {
100100
const uri = pendingTabUri.uri;
101101
// anchor request from search panel
102102
if (uri.includes(`#L`)) {
@@ -120,7 +120,7 @@ export function EditorPane({ isActive, onReady }: EditorPaneProps) {
120120
}
121121
request<any>('editor.open', { cwd: repoPath, filePath: uri });
122122
}
123-
}, [pendingTabUri, status]);
123+
}, [pendingTabUri, extensionReady]);
124124

125125
useEffect(() => {
126126
if (extensionReady) {

src/renderer/components/SecondarySidebar/FileTree.tsx

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,45 @@ export function FileTree(props: { active: boolean }) {
4545
const cwd = selectedWorkspaceId
4646
? workspaces[selectedWorkspaceId]?.worktreePath
4747
: null;
48+
const subscriptionRef = useRef<(() => void) | null>(null);
4849

4950
useEffect(() => {
5051
inited.current = false;
51-
init(true);
52+
if (cwd) {
53+
init(true);
54+
return () => {
55+
// stop watching to save system resources
56+
request<any>('fs.unwatch', { cwd });
57+
// stop waiting for notifications
58+
const messageBus = useStore.getState()?.messageBus;
59+
if (messageBus && subscriptionRef.current) {
60+
messageBus.removeEventHandler('fs-change', subscriptionRef.current);
61+
}
62+
};
63+
}
5264
}, [cwd]);
5365

54-
useEffect(() => {
55-
if (active && inited.current) {
56-
init(false);
66+
const waitForNotifications = (cwd: string) => {
67+
const messageBus = useStore.getState()?.messageBus;
68+
if (!messageBus || !cwd) {
69+
return;
70+
}
71+
if (subscriptionRef.current) {
72+
messageBus.removeEventHandler('fs-change', subscriptionRef.current);
5773
}
58-
}, [active]);
74+
// recreate handler with new cwd
75+
subscriptionRef.current = () => {
76+
console.log('sth changed in', cwd);
77+
init();
78+
};
79+
messageBus.onEvent('fs-change', subscriptionRef.current);
80+
};
5981

60-
const init = async (clear = false) => {
82+
const init = async (reset = false) => {
6183
if (!cwd) {
6284
return;
6385
}
64-
if (clear) {
86+
if (reset) {
6587
setSelectedKey(null);
6688
setExpandedKeys(new Set());
6789
setItemToDelete(null);
@@ -71,6 +93,12 @@ export function FileTree(props: { active: boolean }) {
7193
if (res?.data?.tree) {
7294
setTreeData(res.data.tree);
7395
}
96+
// start watch and wait for notifications
97+
if (reset) {
98+
request<any>('fs.watch', { cwd }).then(() => {
99+
waitForNotifications(cwd);
100+
});
101+
}
74102
});
75103
};
76104

@@ -109,10 +137,7 @@ export function FileTree(props: { active: boolean }) {
109137
const handleRename = async (oldPath: string, newPath: string) => {
110138
try {
111139
const result = await request<any>('fs.rename', { oldPath, newPath });
112-
113140
if (result.success) {
114-
init();
115-
116141
// 如果当前选中的文件就是被重命名的文件
117142
if (selectedKey === oldPath) {
118143
setSelectedKey(newPath);
@@ -134,7 +159,6 @@ export function FileTree(props: { active: boolean }) {
134159
});
135160

136161
if (result.success) {
137-
init();
138162
if (selectedKey === itemToDelete.fullPath) {
139163
setSelectedKey(null);
140164
}

0 commit comments

Comments
 (0)