dsh-workspace-explorer 1.1.0 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +7 -5
- package/lib/client.js +783 -162
- package/package.json +53 -52
- package/scripts/build-client.mjs +1 -0
- package/src/client/conversation-drop.js +142 -0
- package/src/client/main.js +318 -91
- package/src/client/sidebar-motion.js +69 -0
- package/src/host/files.js +1 -1
- package/src/ui/explorer.js +42 -42
- package/src/ui/favorites.js +54 -2
- package/src/ui/navigation.js +9 -0
- package/src/ui/preview.js +58 -12
- package/src/ui/styles.js +74 -15
package/src/client/main.js
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
import { Explorer, ExplorerTitle } from '../ui/explorer.js';
|
|
2
2
|
import { explorerCss } from '../ui/styles.js';
|
|
3
3
|
import { scrollHorizontally } from '../ui/horizontal-wheel.js';
|
|
4
|
-
import { underPath } from '../ui/navigation.js';
|
|
4
|
+
import { parentOf, sessionFilePath, underPath, windowsPath } from '../ui/navigation.js';
|
|
5
5
|
import { renameFavorites } from '../ui/favorites.js';
|
|
6
|
+
import { installSidebarMotion } from './sidebar-motion.js';
|
|
7
|
+
import { installConversationDrop } from './conversation-drop.js';
|
|
6
8
|
|
|
7
9
|
const id = 'dsh-workspace-explorer';
|
|
8
10
|
const inject = ['slots'];
|
|
9
11
|
const h = react.createElement;
|
|
10
12
|
const instancePrefix = 'dsh-resource://workspace-explorer/tab/';
|
|
11
|
-
const occurrence = (sessionId, tabId) => JSON.stringify([sessionId, tabId]);
|
|
12
13
|
|
|
13
14
|
function apply(ctx) {
|
|
14
15
|
if (location.protocol !== 'dsh-app:' || location.hostname !== 'app') return;
|
|
15
16
|
if (!navigator.platform.startsWith('Win')) return;
|
|
16
|
-
ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session'], (scope) => {
|
|
17
|
+
ctx.inject(['sidebarRightTabs', 'sidebarRight', 'remote', 'remote.session', 'remote.workspaceFiles'], (scope) => {
|
|
17
18
|
const sidebar = scope.sidebarRight;
|
|
18
|
-
const
|
|
19
|
+
const bindings = new Map();
|
|
19
20
|
const folderTabs = new Map();
|
|
20
21
|
const listeners = new Set();
|
|
21
22
|
let viewList = [];
|
|
22
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;
|
|
23
33
|
|
|
24
34
|
function subscribe(listener) {
|
|
25
35
|
listeners.add(listener);
|
|
@@ -30,9 +40,171 @@ function apply(ctx) {
|
|
|
30
40
|
for (const listener of listeners) listener();
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
36
208
|
}
|
|
37
209
|
|
|
38
210
|
const openLocal = async (path, action) => {
|
|
@@ -40,17 +212,101 @@ function apply(ctx) {
|
|
|
40
212
|
if (!result.ok) throw result.error;
|
|
41
213
|
};
|
|
42
214
|
|
|
215
|
+
function openDocument(address, options) {
|
|
216
|
+
sidebar.openResource(address, { ...options, kind: 'text' });
|
|
217
|
+
}
|
|
218
|
+
|
|
43
219
|
function openFolderTab(path, paneId) {
|
|
44
220
|
const address = instancePrefix + crypto.randomUUID();
|
|
45
221
|
folderTabs.set(address, path);
|
|
46
222
|
try {
|
|
47
223
|
sidebar.openResource(address, { kind: 'files', paneId });
|
|
48
|
-
|
|
224
|
+
syncTabs();
|
|
225
|
+
const tab = sidebar.active();
|
|
226
|
+
return tab?.contentId === address ? bindings.get(tab.id) : undefined;
|
|
227
|
+
} finally {
|
|
49
228
|
folderTabs.delete(address);
|
|
50
|
-
throw error;
|
|
51
229
|
}
|
|
52
230
|
}
|
|
53
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
|
+
|
|
54
310
|
function refreshDirectories(paths, moved = []) {
|
|
55
311
|
const changed = new Set(paths.map(path => path.toLowerCase()));
|
|
56
312
|
for (const view of viewList) {
|
|
@@ -67,101 +323,81 @@ function apply(ctx) {
|
|
|
67
323
|
}
|
|
68
324
|
|
|
69
325
|
function Anchor({ sessionId, useTabInfo }) {
|
|
70
|
-
const { tab } = useTabInfo();
|
|
326
|
+
const { tab, panel } = useTabInfo();
|
|
71
327
|
const anchor = react.useRef(null);
|
|
72
|
-
const
|
|
328
|
+
const view = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id));
|
|
329
|
+
const navigation = tab.navigation;
|
|
73
330
|
react.useLayoutEffect(() => {
|
|
74
|
-
if (!tab.visible) return;
|
|
331
|
+
if (!tab.visible || !view) return;
|
|
75
332
|
const element = anchor.current;
|
|
76
|
-
|
|
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 };
|
|
333
|
+
const target = { sessionId, paneId: panel.id, isFloating: panel.isFloating };
|
|
97
334
|
view.target = target;
|
|
98
335
|
element.appendChild(view.host);
|
|
99
336
|
notify();
|
|
337
|
+
capturePresentation(sessionId);
|
|
100
338
|
const dispose = tab.actions.bindCommands?.({ refresh: () => view.fileActions?.refresh() });
|
|
101
339
|
return () => {
|
|
102
340
|
dispose?.();
|
|
103
341
|
if (view.target !== target) return;
|
|
104
342
|
view.target = undefined;
|
|
105
343
|
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
344
|
notify();
|
|
345
|
+
capturePresentation(sessionId);
|
|
111
346
|
};
|
|
112
|
-
}, [
|
|
113
|
-
react.
|
|
114
|
-
if (tab.visible
|
|
115
|
-
|
|
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]);
|
|
116
372
|
return h('div', { className: 'dwe-host', ref: anchor });
|
|
117
373
|
}
|
|
118
374
|
|
|
119
375
|
function Title({ sessionId, useTabInfo }) {
|
|
120
376
|
const { tab } = useTabInfo();
|
|
121
|
-
const path = react.useSyncExternalStore(subscribe, () =>
|
|
377
|
+
const path = react.useSyncExternalStore(subscribe, () => boundView(sessionId, tab.id)?.path ?? null);
|
|
378
|
+
react.useLayoutEffect(() => {
|
|
379
|
+
if (tab.visible) capturePresentation(sessionId);
|
|
380
|
+
});
|
|
122
381
|
return h(ExplorerTitle, { path });
|
|
123
382
|
}
|
|
124
383
|
|
|
125
|
-
function View({ view,
|
|
384
|
+
function View({ view, cwd }) {
|
|
126
385
|
const target = react.useSyncExternalStore(subscribe, view.getTarget);
|
|
127
386
|
const sessionId = target?.sessionId;
|
|
128
|
-
const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
|
|
129
387
|
return reactDom.createPortal(h(Explorer, {
|
|
130
388
|
sessionId, cwd, initialPath: view.initialPath, getTarget: view.getTarget, bindFileActions: view.bindFileActions,
|
|
131
|
-
onPath: view.onPath, openLocal, openFolderTab, refreshDirectories, updateRenamedPath,
|
|
389
|
+
onPath: view.onPath, openLocal, openDocument, openFolderTab, refreshDirectories, updateRenamedPath,
|
|
132
390
|
}), view.host);
|
|
133
391
|
}
|
|
134
392
|
|
|
135
393
|
function Keeper({ useSessions }) {
|
|
136
394
|
const sessionId = react.useSyncExternalStore(sidebar.mounted.subscribe, sidebar.mounted.getSnapshot);
|
|
137
395
|
const currentViews = react.useSyncExternalStore(subscribe, () => viewList);
|
|
138
|
-
const
|
|
396
|
+
const cwd = useSessions(state => sessionId ? state.byId[sessionId]?.cwd : undefined);
|
|
139
397
|
react.useLayoutEffect(() => {
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
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 }));
|
|
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 }));
|
|
165
401
|
}
|
|
166
402
|
|
|
167
403
|
scope.effect(() => {
|
|
@@ -179,26 +415,17 @@ function apply(ctx) {
|
|
|
179
415
|
}
|
|
180
416
|
}
|
|
181
417
|
document.addEventListener('wheel', wheel, { passive: false, capture: true });
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
});
|
|
418
|
+
const disposeFileOpener = installFileOpener();
|
|
419
|
+
const disposeMotion = installSidebarMotion(sidebar);
|
|
420
|
+
const disposeTabs = sidebar.openTabs.subscribe(syncTabs);
|
|
421
|
+
const disposeSwitch = sidebar.mounted.subscribe(closeDepartingTabs);
|
|
200
422
|
return () => {
|
|
201
|
-
|
|
423
|
+
disposeFileOpener();
|
|
424
|
+
disposeMotion();
|
|
425
|
+
disposeTabs();
|
|
426
|
+
disposeSwitch();
|
|
427
|
+
detached = false;
|
|
428
|
+
finishHandoff();
|
|
202
429
|
document.removeEventListener('wheel', wheel, true);
|
|
203
430
|
style.remove();
|
|
204
431
|
};
|
|
@@ -212,7 +439,7 @@ function apply(ctx) {
|
|
|
212
439
|
scope.slots.register(key ? { name, key } : { name, id }, component)));
|
|
213
440
|
}
|
|
214
441
|
scope.effect(() => scope.sidebarRightTabs.register({
|
|
215
|
-
id, kind: 'files', priority: 'extension', title: () => '文件浏览',
|
|
442
|
+
id, kind: 'files', priority: 'extension', title: () => '文件浏览', multiple: true,
|
|
216
443
|
guide: [{
|
|
217
444
|
id: 'workspace', commandId: 'workspace.files', order: 10, title: () => '文件浏览',
|
|
218
445
|
description: () => '浏览本机磁盘,在下方独立预览文件',
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const dockItems = ':is([data-dockkit-host="dock"], [data-dockkit-empty], [data-dockkit-divider])';
|
|
2
|
+
const toggleButtons = 'button:is([data-sidebar-right-expand], [data-sidebar-right-toggle])';
|
|
3
|
+
|
|
4
|
+
export function installSidebarMotion(sidebar) {
|
|
5
|
+
let allowed;
|
|
6
|
+
let collectFrame;
|
|
7
|
+
const instantFrames = new Map();
|
|
8
|
+
|
|
9
|
+
function stopMotion() {
|
|
10
|
+
cancelAnimationFrame(collectFrame);
|
|
11
|
+
if (!allowed) return;
|
|
12
|
+
for (const item of allowed.items) delete item.dataset.dweSidebarToggleMotion;
|
|
13
|
+
allowed = undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function allowButtonMotion(event) {
|
|
17
|
+
const button = event.target instanceof Element && event.target.closest(toggleButtons);
|
|
18
|
+
if (!button || button.disabled) return;
|
|
19
|
+
const sessionId = sidebar.mounted.getSnapshot();
|
|
20
|
+
const panel = sessionId && document.querySelector(
|
|
21
|
+
'[data-sidebar-right-panel][data-sidebar-right-session="' + CSS.escape(sessionId) + '"]',
|
|
22
|
+
);
|
|
23
|
+
const frame = panel?.closest('[data-rightbar-col]')?.parentElement;
|
|
24
|
+
if (!frame) return;
|
|
25
|
+
|
|
26
|
+
// A tab selected during the slide must not inherit permission to animate.
|
|
27
|
+
stopMotion();
|
|
28
|
+
const items = [...panel.querySelectorAll(dockItems + ':not([hidden])')];
|
|
29
|
+
const motion = { frame, items };
|
|
30
|
+
allowed = motion;
|
|
31
|
+
for (const item of items) item.dataset.dweSidebarToggleMotion = '';
|
|
32
|
+
collectFrame = requestAnimationFrame(() => {
|
|
33
|
+
const transitions = [...frame.getAnimations(), ...items.flatMap(item => item.getAnimations())]
|
|
34
|
+
.filter(animation => ['transform', 'grid-template-columns'].includes(animation.transitionProperty));
|
|
35
|
+
Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
|
|
36
|
+
if (allowed === motion) stopMotion();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The shell shares one grid transition between the left and right columns.
|
|
42
|
+
// Snap right-track changes outside the toggle controls so the left controls still animate.
|
|
43
|
+
const track = new MutationObserver(records => {
|
|
44
|
+
for (const frame of new Set(records.map(record => record.target))) {
|
|
45
|
+
if (allowed?.frame === frame || !frame.querySelector(':scope > [data-rightbar-col]')) continue;
|
|
46
|
+
cancelAnimationFrame(instantFrames.get(frame));
|
|
47
|
+
frame.dataset.dweSidebarTrackInstant = '';
|
|
48
|
+
instantFrames.set(frame, requestAnimationFrame(() => {
|
|
49
|
+
instantFrames.set(frame, requestAnimationFrame(() => {
|
|
50
|
+
delete frame.dataset.dweSidebarTrackInstant;
|
|
51
|
+
instantFrames.delete(frame);
|
|
52
|
+
}));
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
track.observe(document.documentElement, { subtree: true, attributeFilter: ['data-rightbar-collapsed'] });
|
|
57
|
+
document.addEventListener('click', allowButtonMotion, true);
|
|
58
|
+
const disposeSwitch = sidebar.mounted.subscribe(stopMotion);
|
|
59
|
+
return () => {
|
|
60
|
+
stopMotion();
|
|
61
|
+
disposeSwitch();
|
|
62
|
+
track.disconnect();
|
|
63
|
+
for (const [frame, request] of instantFrames) {
|
|
64
|
+
cancelAnimationFrame(request);
|
|
65
|
+
delete frame.dataset.dweSidebarTrackInstant;
|
|
66
|
+
}
|
|
67
|
+
document.removeEventListener('click', allowButtonMotion, true);
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/host/files.js
CHANGED
|
@@ -281,7 +281,7 @@ function createDiskAccess() {
|
|
|
281
281
|
const target = await resolve(absolute, signal, workspace);
|
|
282
282
|
const info = await stat(target);
|
|
283
283
|
if (info.isDirectory()) return { path: absolute, nativePath: target, type: 'directory' };
|
|
284
|
-
if (info.isFile()) return { path: absolute, nativePath: target, type: 'file', name: path.basename(absolute) };
|
|
284
|
+
if (info.isFile()) return { path: absolute, nativePath: target, type: 'file', name: path.basename(absolute), size: info.size };
|
|
285
285
|
throw new BrowseError('该路径不指向普通文件或文件夹');
|
|
286
286
|
}
|
|
287
287
|
|