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