dsh-workspace-explorer 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,225 +1,452 @@
1
- import { Explorer, ExplorerTitle } from '../ui/explorer.js';
2
- import { explorerCss } from '../ui/styles.js';
3
- import { scrollHorizontally } from '../ui/horizontal-wheel.js';
4
- import { underPath } from '../ui/navigation.js';
5
- import { renameFavorites } from '../ui/favorites.js';
6
-
7
- const id = 'dsh-workspace-explorer';
8
- const inject = ['slots'];
9
- const h = react.createElement;
10
- const instancePrefix = 'dsh-resource://workspace-explorer/tab/';
11
- const occurrence = (sessionId, tabId) => JSON.stringify([sessionId, tabId]);
12
-
13
- function apply(ctx) {
14
- if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
15
- if (!navigator.platform.startsWith('Win')) return;
16
- ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session'], (scope) => {
17
- const sidebar = scope.sidebarRight;
18
- const views = new Map();
19
- const folderTabs = new Map();
20
- const listeners = new Set();
21
- let viewList = [];
22
- let activeView;
23
-
24
- function subscribe(listener) {
25
- listeners.add(listener);
26
- return () => listeners.delete(listener);
27
- }
28
-
29
- function notify() {
30
- for (const listener of listeners) listener();
31
- }
32
-
33
- function updateViews() {
34
- viewList = [...new Set(views.values())];
35
- notify();
36
- }
37
-
38
- const openLocal = async (path, action) => {
39
- const result = await scope.remote.session.openWorkspacePath({ path, action });
40
- if (!result.ok) throw result.error;
41
- };
42
-
43
- function openFolderTab(path, paneId) {
44
- const address = instancePrefix + crypto.randomUUID();
45
- folderTabs.set(address, path);
46
- try {
47
- sidebar.openResource(address, { kind: 'files', paneId });
48
- } catch (error) {
49
- folderTabs.delete(address);
50
- throw error;
51
- }
52
- }
53
-
54
- function refreshDirectories(paths, moved = []) {
55
- const changed = new Set(paths.map(path => path.toLowerCase()));
56
- for (const view of viewList) {
57
- if (view.path && (changed.has(view.path.toLowerCase()) || moved.some(root => underPath(view.path, root)))) {
58
- view.fileActions?.refresh();
59
- }
60
- }
61
- }
62
-
63
- function updateRenamedPath(from, to) {
64
- if (from === to) return;
65
- renameFavorites(from, to);
66
- for (const view of viewList) view.fileActions?.rename(from, to);
67
- }
68
-
69
- function Anchor({ sessionId, useTabInfo }) {
70
- const { tab } = useTabInfo();
71
- const anchor = react.useRef(null);
72
- const key = occurrence(sessionId, tab.id);
73
- react.useLayoutEffect(() => {
74
- if (!tab.visible) return;
75
- const element = anchor.current;
76
- let view = views.get(key);
77
- if (!view) {
78
- const host = document.createElement('div');
79
- host.className = 'dwe-host';
80
- const initialPath = folderTabs.get(tab.contentId);
81
- folderTabs.delete(tab.contentId);
82
- view = { host, address: initialPath ? tab.contentId : instancePrefix + crypto.randomUUID(), path: null, initialPath };
83
- view.getTarget = () => view.target;
84
- view.onPath = path => {
85
- if (view.path === path) return;
86
- view.path = path;
87
- notify();
88
- };
89
- view.bindFileActions = actions => {
90
- view.fileActions = actions;
91
- return () => { view.fileActions = undefined; };
92
- };
93
- views.set(key, view);
94
- updateViews();
95
- }
96
- const target = { sessionId, actions: tab.actions };
97
- view.target = target;
98
- element.appendChild(view.host);
99
- notify();
100
- const dispose = tab.actions.bindCommands?.({ refresh: () => view.fileActions?.refresh() });
101
- return () => {
102
- dispose?.();
103
- if (view.target !== target) return;
104
- view.target = undefined;
105
- if (view.host.parentNode === element) view.host.remove();
106
- if (activeView === view && sidebar.mounted.getSnapshot() === sessionId
107
- && (!sidebar.isExpanded() || sidebar.active()?.kind !== 'files')) {
108
- activeView = undefined;
109
- }
110
- notify();
111
- };
112
- }, [key, sessionId, tab.visible, tab.actions]);
113
- react.useLayoutEffect(() => {
114
- if (tab.visible && sidebar.active()?.id === tab.id) activeView = views.get(key);
115
- });
116
- return h('div', { className: 'dwe-host', ref: anchor });
117
- }
118
-
119
- function Title({ sessionId, useTabInfo }) {
120
- const { tab } = useTabInfo();
121
- const path = react.useSyncExternalStore(subscribe, () => views.get(occurrence(sessionId, tab.id))?.path ?? null);
122
- return h(ExplorerTitle, { path });
123
- }
124
-
125
- function View({ view, useSessions }) {
126
- const target = react.useSyncExternalStore(subscribe, view.getTarget);
127
- const sessionId = target?.sessionId;
128
- const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
129
- return reactDom.createPortal(h(Explorer, {
130
- sessionId, cwd, initialPath: view.initialPath, getTarget: view.getTarget, bindFileActions: view.bindFileActions,
131
- onPath: view.onPath, openLocal, openFolderTab, refreshDirectories, updateRenamedPath,
132
- }), view.host);
133
- }
134
-
135
- function Keeper({ useSessions }) {
136
- const sessionId = react.useSyncExternalStore(sidebar.mounted.subscribe, sidebar.mounted.getSnapshot);
137
- const currentViews = react.useSyncExternalStore(subscribe, () => viewList);
138
- const previous = react.useRef();
139
- react.useLayoutEffect(() => {
140
- if (!sessionId || previous.current === sessionId) return;
141
- const view = previous.current === undefined ? undefined : activeView;
142
- previous.current = sessionId;
143
- if (!view) return;
144
- // Wait for the new conversation's dock store to be adopted.
145
- queueMicrotask(() => {
146
- if (sidebar.mounted.getSnapshot() !== sessionId) return;
147
- const existing = sidebar.openTabs.getSnapshot().find(tab =>
148
- tab.sessionId === sessionId && views.get(occurrence(sessionId, tab.tabId)) === view);
149
- if (existing) {
150
- if (!sidebar.isExpanded()) sidebar.toggleExpanded();
151
- sidebar.focus(existing.tabId);
152
- return;
153
- }
154
- sidebar.openResource(view.address, { kind: 'files' });
155
- const tab = sidebar.active();
156
- if (tab?.kind !== 'files') return;
157
- const key = occurrence(sessionId, tab.id);
158
- if (!views.has(key)) {
159
- views.set(key, view);
160
- updateViews();
161
- }
162
- });
163
- }, [sessionId]);
164
- return currentViews.map(view => h(View, { key: view.address, view, useSessions }));
165
- }
166
-
167
- scope.effect(() => {
168
- const style = document.createElement('style');
169
- style.dataset.dshWorkspaceExplorer = '';
170
- style.textContent = explorerCss;
171
- document.head.appendChild(style);
172
- function wheel(event) {
173
- if (!(event.target instanceof Element)) return;
174
- const path = event.target.closest('.dwe-path');
175
- if (path) scrollHorizontally(event, path);
176
- else {
177
- const strip = event.target.closest('[data-dockkit-strip-tabs]');
178
- if (strip?.querySelector('[data-dwe-tab-title]')) scrollHorizontally(event, strip);
179
- }
180
- }
181
- document.addEventListener('wheel', wheel, { passive: false, capture: true });
182
- const dispose = sidebar.openTabs.subscribe(() => {
183
- const inventory = sidebar.openTabs.getSnapshot();
184
- const live = new Set(inventory.map(tab => occurrence(tab.sessionId, tab.tabId)));
185
- const addresses = new Set(inventory.map(tab => tab.contentId));
186
- for (const address of folderTabs.keys()) {
187
- if (!addresses.has(address)) folderTabs.delete(address);
188
- }
189
- let removed = false;
190
- for (const key of views.keys()) {
191
- if (live.has(key)) continue;
192
- views.delete(key);
193
- removed = true;
194
- }
195
- if (removed) {
196
- updateViews();
197
- if (!viewList.includes(activeView)) activeView = undefined;
198
- }
199
- });
200
- return () => {
201
- dispose();
202
- document.removeEventListener('wheel', wheel, true);
203
- style.remove();
204
- };
205
- });
206
- for (const [name, key, component] of [
207
- ['shell.overlay', undefined, Keeper],
208
- ['sidebar.right.pane.tab', id, Anchor],
209
- ['sidebar.right.pane.tab.title', id, Title],
210
- ]) {
211
- scope.effect(() => scope.slots.inject(name, () =>
212
- scope.slots.register(key ? { name, key } : { name, id }, component)));
213
- }
214
- scope.effect(() => scope.sidebarRightTabs.register({
215
- id, kind: 'files', priority: 'extension', title: () => '文件浏览', keepMounted: true, multiple: true,
216
- guide: [{
217
- id: 'workspace', commandId: 'workspace.files', order: 10, title: () => '文件浏览',
218
- description: () => '浏览本机磁盘,在下方独立预览文件',
219
- icon: primitives.GuideArtworkFiles,
220
- }],
221
- }));
222
- });
223
- }
224
-
1
+ import { Explorer, ExplorerTitle } from '../ui/explorer.js';
2
+ import { explorerCss } from '../ui/styles.js';
3
+ import { scrollHorizontally } from '../ui/horizontal-wheel.js';
4
+ import { parentOf, sessionFilePath, underPath, windowsPath } from '../ui/navigation.js';
5
+ import { renameFavorites } from '../ui/favorites.js';
6
+ import { installSidebarMotion } from './sidebar-motion.js';
7
+ import { installConversationDrop } from './conversation-drop.js';
8
+
9
+ const id = 'dsh-workspace-explorer';
10
+ const inject = ['slots'];
11
+ const h = react.createElement;
12
+ const instancePrefix = 'dsh-resource://workspace-explorer/tab/';
13
+
14
+ function apply(ctx) {
15
+ if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
16
+ if (!navigator.platform.startsWith('Win')) return;
17
+ ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session', 'remote.workspaceFiles'], (scope) => {
18
+ const sidebar = scope.sidebarRight;
19
+ const bindings = new Map();
20
+ const folderTabs = new Map();
21
+ const listeners = new Set();
22
+ let viewList = [];
23
+ let activeView;
24
+ let presentedSession;
25
+ let workspace;
26
+ let expanded;
27
+ let fileFocused = false;
28
+ let projecting = false;
29
+ let detached = false;
30
+ let captureQueued = false;
31
+ let handoffFrame;
32
+ let handoffAnimation;
33
+
34
+ function subscribe(listener) {
35
+ listeners.add(listener);
36
+ return () => listeners.delete(listener);
37
+ }
38
+
39
+ function notify() {
40
+ for (const listener of listeners) listener();
41
+ }
42
+
43
+ function filesIn(sessionId) {
44
+ return sidebar.openTabs.getSnapshot().filter(tab => tab.sessionId === sessionId && tab.kind === 'files');
45
+ }
46
+
47
+ function createView(initialPath) {
48
+ const host = document.createElement('div');
49
+ host.className = 'dwe-host';
50
+ const view = { host, address: instancePrefix + crypto.randomUUID(), path: initialPath ?? null, initialPath };
51
+ view.getTarget = () => view.target;
52
+ view.onPath = path => {
53
+ if (view.path === path) return;
54
+ view.path = path;
55
+ notify();
56
+ };
57
+ view.bindFileActions = actions => {
58
+ view.fileActions = actions;
59
+ if (view.pendingPreview) {
60
+ const preview = view.pendingPreview;
61
+ view.pendingPreview = undefined;
62
+ actions.reveal(...preview);
63
+ }
64
+ return () => { view.fileActions = undefined; };
65
+ };
66
+ return view;
67
+ }
68
+
69
+ function boundView(sessionId, tabId) {
70
+ return sessionId === presentedSession ? bindings.get(tabId) : undefined;
71
+ }
72
+
73
+ function rememberPresentation() {
74
+ if (projecting || detached || !presentedSession || sidebar.mounted.getSnapshot() !== presentedSession) return;
75
+ const tab = sidebar.active();
76
+ const view = tab && bindings.get(tab.id);
77
+ if (view) activeView = view;
78
+ fileFocused = Boolean(view);
79
+ expanded = sidebar.isExpanded();
80
+ }
81
+
82
+ function rememberOrder(sessionId) {
83
+ const selector = '[data-sidebar-right-session="' + CSS.escape(sessionId) + '"] [data-dockkit-tab]';
84
+ const ordered = new Set();
85
+ for (const chip of document.querySelectorAll(selector)) {
86
+ const view = bindings.get(chip.dataset.dockkitTab);
87
+ if (view) ordered.add(view);
88
+ }
89
+ const next = [...ordered, ...viewList.filter(view => !ordered.has(view))];
90
+ if (next.some((view, index) => view !== viewList[index])) {
91
+ viewList = next;
92
+ notify();
93
+ }
94
+ }
95
+
96
+ function capturePresentation(sessionId) {
97
+ if (captureQueued) return;
98
+ captureQueued = true;
99
+ queueMicrotask(() => {
100
+ captureQueued = false;
101
+ if (presentedSession !== sessionId || sidebar.mounted.getSnapshot() !== sessionId) return;
102
+ rememberPresentation();
103
+ rememberOrder(sessionId);
104
+ });
105
+ }
106
+
107
+ function syncTabs() {
108
+ if (projecting || detached || !presentedSession || sidebar.mounted.getSnapshot() !== presentedSession) return;
109
+ const tabs = filesIn(presentedSession);
110
+ const live = new Set(tabs.map(tab => tab.tabId));
111
+ const added = [];
112
+ const removed = new Set();
113
+ for (const [tabId, view] of bindings) {
114
+ if (live.has(tabId)) continue;
115
+ bindings.delete(tabId);
116
+ removed.add(view);
117
+ }
118
+ for (const tab of tabs) {
119
+ if (bindings.has(tab.tabId)) continue;
120
+ const view = createView(folderTabs.get(tab.contentId) ?? workspace);
121
+ bindings.set(tab.tabId, view);
122
+ added.push(view);
123
+ }
124
+ if (removed.size || added.length) {
125
+ viewList = [...viewList.filter(view => !removed.has(view)), ...added];
126
+ if (removed.has(activeView)) activeView = undefined;
127
+ notify();
128
+ }
129
+ rememberPresentation();
130
+ }
131
+
132
+ function closeDepartingTabs() {
133
+ const arriving = sidebar.mounted.getSnapshot();
134
+ if (!arriving || !presentedSession) return;
135
+ if (arriving === presentedSession) {
136
+ if (detached) queueMicrotask(() => {
137
+ if (detached && sidebar.mounted.getSnapshot() === arriving) present(arriving, workspace);
138
+ });
139
+ return;
140
+ }
141
+ if (detached) return;
142
+ rememberOrder(presentedSession);
143
+ detached = true;
144
+ if (expanded && viewList.length) beginHandoff();
145
+ // The old native store is still adopted before React commits the session change.
146
+ for (const tab of filesIn(presentedSession)) sidebar.closeIn?.(presentedSession, tab.tabId);
147
+ }
148
+
149
+ function beginHandoff() {
150
+ const frame = document.querySelector('[data-rightbar-col]')?.parentElement;
151
+ if (!frame) return;
152
+ if (handoffFrame !== frame) {
153
+ finishHandoff();
154
+ handoffFrame = frame;
155
+ frame.style.setProperty('--dwe-handoff-columns', getComputedStyle(frame).gridTemplateColumns);
156
+ frame.dataset.dweSidebarHandoff = '';
157
+ }
158
+ cancelAnimationFrame(handoffAnimation);
159
+ handoffAnimation = requestAnimationFrame(() => {
160
+ handoffAnimation = requestAnimationFrame(finishHandoff);
161
+ });
162
+ }
163
+
164
+ function finishHandoff() {
165
+ cancelAnimationFrame(handoffAnimation);
166
+ if (!handoffFrame) return;
167
+ delete handoffFrame.dataset.dweSidebarHandoff;
168
+ handoffFrame.style.removeProperty('--dwe-handoff-columns');
169
+ handoffFrame = undefined;
170
+ }
171
+
172
+ function placeView(view, options) {
173
+ sidebar.openResource(view.address, { kind: 'files', ...options });
174
+ const tab = sidebar.active();
175
+ if (tab?.kind === 'files' && tab.contentId === view.address) bindings.set(tab.id, view);
176
+ }
177
+
178
+ function present(sessionId, cwd) {
179
+ workspace = windowsPath(cwd);
180
+ if (sessionId !== presentedSession || detached) {
181
+ const first = !presentedSession;
182
+ presentedSession = sessionId;
183
+ detached = false;
184
+ bindings.clear();
185
+ if (first) {
186
+ syncTabs();
187
+ } else {
188
+ const nativeActive = sidebar.active();
189
+ const preserveExpansion = expanded;
190
+ projecting = true;
191
+ try {
192
+ for (const tab of filesIn(sessionId)) sidebar.close(tab.tabId);
193
+ const replaceTab = fileFocused && sidebar.active()?.kind === 'guide' ? sidebar.active()?.id : undefined;
194
+ viewList.forEach((view, index) => placeView(view, index === 0 ? { replaceTab } : undefined));
195
+ if (viewList.length) {
196
+ const restoreTab = !fileFocused && nativeActive?.kind !== 'files' ? nativeActive?.id
197
+ : [...bindings].find(([, view]) => view === activeView)?.[0];
198
+ if (restoreTab) sidebar.focus(restoreTab);
199
+ if (preserveExpansion !== undefined && sidebar.isExpanded() !== preserveExpansion) sidebar.toggleExpanded();
200
+ }
201
+ } finally {
202
+ projecting = false;
203
+ }
204
+ notify();
205
+ rememberPresentation();
206
+ }
207
+ }
208
+ }
209
+
210
+ const openLocal = async (path, action) => {
211
+ const result = await scope.remote.session.openWorkspacePath({ path, action });
212
+ if (!result.ok) throw result.error;
213
+ };
214
+
215
+ function openDocument(address, options) {
216
+ sidebar.openResource(address, { ...options, kind: 'text' });
217
+ }
218
+
219
+ function openFolderTab(path, paneId) {
220
+ const address = instancePrefix + crypto.randomUUID();
221
+ folderTabs.set(address, path);
222
+ try {
223
+ sidebar.openResource(address, { kind: 'files', paneId });
224
+ syncTabs();
225
+ const tab = sidebar.active();
226
+ return tab?.contentId === address ? bindings.get(tab.id) : undefined;
227
+ } finally {
228
+ folderTabs.delete(address);
229
+ }
230
+ }
231
+
232
+ async function statFile(sessionId, file, signal) {
233
+ try {
234
+ const result = await scope.remote.workspaceFiles.stat(sessionId, file, signal);
235
+ const absolute = windowsPath(result.ok && result.value.absolutePath);
236
+ if (absolute) return { absolute, name: absolute.split('\\').at(-1), size: result.value.bytes };
237
+ } catch { /* The built-in preview can report a failed session file lookup. */ }
238
+ }
239
+
240
+ function revealFile(view, file, line) {
241
+ const preview = [file.absolute, file.name, file.size, line];
242
+ if (view.fileActions) view.fileActions.reveal(...preview);
243
+ else view.pendingPreview = preview;
244
+ }
245
+
246
+ function fileRequest(address, options) {
247
+ const sessionId = sidebar.mounted.getSnapshot();
248
+ const file = workspace && sessionId === presentedSession && sessionFilePath(address, sessionId);
249
+ if (file) return { address, sessionId, file, params: options?.params };
250
+ }
251
+
252
+ function currentExplorer() {
253
+ return bindings.get(sidebar.active()?.id) ?? activeView ?? viewList[0];
254
+ }
255
+
256
+ async function openConversationFile(request, signal, { view, newTab, paneId }) {
257
+ const { address, sessionId, file, params } = request;
258
+ const metadata = await statFile(sessionId, file, signal);
259
+ if (signal.aborted || sidebar.mounted.getSnapshot() !== sessionId) return;
260
+ if (!metadata) {
261
+ openDocument(address, { params, paneId });
262
+ return;
263
+ }
264
+ const existing = newTab ? undefined : viewList.includes(view) ? view : currentExplorer();
265
+ const tabId = existing && [...bindings].find(([, item]) => item === existing)?.[0];
266
+ if (tabId) {
267
+ sidebar.focus(tabId);
268
+ if (!sidebar.isExpanded() && !existing.target?.isFloating) sidebar.toggleExpanded();
269
+ }
270
+ const target = tabId ? existing : openFolderTab(parentOf(metadata.absolute), paneId);
271
+ if (target) revealFile(target, metadata, params?.line);
272
+ }
273
+
274
+ function installFileOpener() {
275
+ const openResource = sidebar.openResource;
276
+ let fileClick;
277
+ let dragSource;
278
+ function open(address, options) {
279
+ const request = (!options || Object.keys(options).every(key => key === 'params')) && fileRequest(address, options);
280
+ if (!request) return openResource.call(this, address, options);
281
+ if (dragSource) {
282
+ dragSource.file = request;
283
+ return;
284
+ }
285
+ fileClick?.abort();
286
+ fileClick = new AbortController();
287
+ void openConversationFile(request, fileClick.signal, { view: currentExplorer() });
288
+ }
289
+ function readDragSource(button) {
290
+ // Let Harness resolve the same path and line as a real click, without opening a tab yet.
291
+ const capture = {};
292
+ dragSource = capture;
293
+ try { button.click(); }
294
+ finally { dragSource = undefined; }
295
+ return capture.file;
296
+ }
297
+ sidebar.openResource = open;
298
+ const stopDrop = installConversationDrop(sidebar.mounted, readDragSource, (request, paneId, signal) => {
299
+ void openConversationFile(request, signal, { newTab: true, paneId });
300
+ });
301
+ const stop = sidebar.mounted.subscribe(() => fileClick?.abort());
302
+ return () => {
303
+ stopDrop();
304
+ stop();
305
+ fileClick?.abort();
306
+ if (sidebar.openResource === open) sidebar.openResource = openResource;
307
+ };
308
+ }
309
+
310
+ function refreshDirectories(paths, moved = []) {
311
+ const changed = new Set(paths.map(path => path.toLowerCase()));
312
+ for (const view of viewList) {
313
+ if (view.path && (changed.has(view.path.toLowerCase()) || moved.some(root => underPath(view.path, root)))) {
314
+ view.fileActions?.refresh();
315
+ }
316
+ }
317
+ }
318
+
319
+ function updateRenamedPath(from, to) {
320
+ if (from === to) return;
321
+ renameFavorites(from, to);
322
+ for (const view of viewList) view.fileActions?.rename(from, to);
323
+ }
324
+
325
+ function Anchor({ sessionId, useTabInfo }) {
326
+ const { tab, panel } = useTabInfo();
327
+ const anchor = react.useRef(null);
328
+ const view = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id));
329
+ const navigation = tab.navigation;
330
+ react.useLayoutEffect(() => {
331
+ if (!tab.visible || !view) return;
332
+ const element = anchor.current;
333
+ const target = { sessionId, paneId: panel.id, isFloating: panel.isFloating };
334
+ view.target = target;
335
+ element.appendChild(view.host);
336
+ notify();
337
+ capturePresentation(sessionId);
338
+ const dispose = tab.actions.bindCommands?.({ refresh: () => view.fileActions?.refresh() });
339
+ return () => {
340
+ dispose?.();
341
+ if (view.target !== target) return;
342
+ view.target = undefined;
343
+ if (view.host.parentNode === element) view.host.remove();
344
+ notify();
345
+ capturePresentation(sessionId);
346
+ };
347
+ }, [view, sessionId, tab.visible, tab.actions, panel.id, panel.isFloating]);
348
+ react.useEffect(() => {
349
+ if (!tab.visible || !view || view.handledNavigation === navigation) return;
350
+ const { address, params } = navigation;
351
+ const file = sessionFilePath(address, sessionId);
352
+ if (!file) return;
353
+
354
+ const controller = new AbortController();
355
+ const { signal } = controller;
356
+ const isCurrent = () => !signal.aborted && !tab.signal?.aborted
357
+ && sidebar.mounted.getSnapshot() === sessionId && boundView(sessionId, tab.id) === view;
358
+ async function reveal() {
359
+ const metadata = await statFile(sessionId, file, signal);
360
+ if (!isCurrent()) return;
361
+ view.handledNavigation = navigation;
362
+ if (metadata) {
363
+ view.revealedFile = true;
364
+ revealFile(view, metadata, params?.line);
365
+ } else {
366
+ openDocument(address, { paneId: panel.id, params, replaceTab: view.revealedFile ? undefined : tab.id });
367
+ }
368
+ }
369
+ void reveal();
370
+ return () => controller.abort();
371
+ }, [view, sessionId, tab.id, tab.visible, navigation, panel.id]);
372
+ return h('div', { className: 'dwe-host', ref: anchor });
373
+ }
374
+
375
+ function Title({ sessionId, useTabInfo }) {
376
+ const { tab } = useTabInfo();
377
+ const path = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id)?.path ?? null);
378
+ react.useLayoutEffect(() => {
379
+ if (tab.visible) capturePresentation(sessionId);
380
+ });
381
+ return h(ExplorerTitle, { path });
382
+ }
383
+
384
+ function View({ view, cwd }) {
385
+ const target = react.useSyncExternalStore(subscribe, view.getTarget);
386
+ const sessionId = target?.sessionId;
387
+ return reactDom.createPortal(h(Explorer, {
388
+ sessionId, cwd, initialPath: view.initialPath, getTarget: view.getTarget, bindFileActions: view.bindFileActions,
389
+ onPath: view.onPath, openLocal, openDocument, openFolderTab, refreshDirectories, updateRenamedPath,
390
+ }), view.host);
391
+ }
392
+
393
+ function Keeper({ useSessions }) {
394
+ const sessionId = react.useSyncExternalStore(sidebar.mounted.subscribe, sidebar.mounted.getSnapshot);
395
+ const currentViews = react.useSyncExternalStore(subscribe, () => viewList);
396
+ const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
397
+ react.useLayoutEffect(() => {
398
+ if (sessionId && sidebar.mounted.getSnapshot() === sessionId) present(sessionId, cwd);
399
+ }, [sessionId, cwd]);
400
+ return currentViews.map(view => h(View, { key: view.address, view, cwd }));
401
+ }
402
+
403
+ scope.effect(() => {
404
+ const style = document.createElement('style');
405
+ style.dataset.dshWorkspaceExplorer = '';
406
+ style.textContent = explorerCss;
407
+ document.head.appendChild(style);
408
+ function wheel(event) {
409
+ if (!(event.target instanceof Element)) return;
410
+ const path = event.target.closest('.dwe-path');
411
+ if (path) scrollHorizontally(event, path);
412
+ else {
413
+ const strip = event.target.closest('[data-dockkit-strip-tabs]');
414
+ if (strip?.querySelector('[data-dwe-tab-title]')) scrollHorizontally(event, strip);
415
+ }
416
+ }
417
+ document.addEventListener('wheel', wheel, { passive: false, capture: true });
418
+ const disposeFileOpener = installFileOpener();
419
+ const disposeMotion = installSidebarMotion(sidebar);
420
+ const disposeTabs = sidebar.openTabs.subscribe(syncTabs);
421
+ const disposeSwitch = sidebar.mounted.subscribe(closeDepartingTabs);
422
+ return () => {
423
+ disposeFileOpener();
424
+ disposeMotion();
425
+ disposeTabs();
426
+ disposeSwitch();
427
+ detached = false;
428
+ finishHandoff();
429
+ document.removeEventListener('wheel', wheel, true);
430
+ style.remove();
431
+ };
432
+ });
433
+ for (const [name, key, component] of [
434
+ ['shell.overlay', undefined, Keeper],
435
+ ['sidebar.right.pane.tab', id, Anchor],
436
+ ['sidebar.right.pane.tab.title', id, Title],
437
+ ]) {
438
+ scope.effect(() => scope.slots.inject(name, () =>
439
+ scope.slots.register(key ? { name, key } : { name, id }, component)));
440
+ }
441
+ scope.effect(() => scope.sidebarRightTabs.register({
442
+ id, kind: 'files', priority: 'extension', title: () => '文件浏览', multiple: true,
443
+ guide: [{
444
+ id: 'workspace', commandId: 'workspace.files', order: 10, title: () => '文件浏览',
445
+ description: () => '浏览本机磁盘,在下方独立预览文件',
446
+ icon: primitives.GuideArtworkFiles,
447
+ }],
448
+ }));
449
+ });
450
+ }
451
+
225
452
  export { apply, inject };