dsh-vscode-mode 0.17.0 → 0.19.0
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/README.md +8 -4
- package/lib/client.debug.js +934 -0
- package/lib/client.debug.js.map +1 -0
- package/lib/client.js +1425 -1724
- package/lib/client.js.map +1 -1
- package/lib/client.rules.js +793 -0
- package/lib/client.rules.js.map +1 -0
- package/lib/index.js +1078 -286
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/client/index.ts +24 -0
- package/src/client/lazyPanel.ts +68 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/sidebar/PanelBoundary.ts +57 -0
- package/src/client/sidebar/SidebarView.ts +58 -21
- package/src/client/sidebar/panels/FileExplorer.ts +80 -27
- package/src/client/sidebar/panels/SvnPanel.ts +24 -8
- package/src/client/sidebar/panels/index.ts +3 -4
- package/src/client/sidebar/panels/treeWindow.ts +106 -0
- package/src/client/sidebar/panels/useExplorerReveal.ts +5 -2
- package/src/client/sidebar/registry.ts +4 -1
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/styles/editor.css +13 -1
- package/src/client/textPages.ts +135 -0
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +147 -29
- package/src/client/ui/LargeTextView.ts +94 -0
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/mcp.ts +1 -0
- package/src/mcpBridge.ts +163 -0
- package/src/mcpDiscover.ts +38 -0
- package/src/mcpPool.ts +245 -0
- package/src/mcpProject.ts +15 -12
- package/src/mcpRuntime.ts +299 -254
- package/src/rpc.ts +42 -8
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +8 -2
- package/src/shared/textPages.ts +28 -0
- package/src/textPages.ts +149 -0
- package/src/treeIndex.ts +62 -37
|
@@ -28,7 +28,7 @@ import { useExplorerSvn } from './useExplorerSvn.js'
|
|
|
28
28
|
import { useExplorerReveal, treeRevealPath } from './useExplorerReveal.js'
|
|
29
29
|
import type { SidebarCtx } from '../types.js'
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
import { flattenTree, treeAnchorTop, treeRevealTop, treeWindow, TREE_ROW_HEIGHT } from './treeWindow.js'
|
|
32
32
|
const SAVE_DEBOUNCE_MS = 300
|
|
33
33
|
const FOLLOW_INTERVAL_MS = 10_000
|
|
34
34
|
const PREFETCH_MAX = 4
|
|
@@ -126,7 +126,8 @@ export function FileExplorer(props) {
|
|
|
126
126
|
const saveTimerRef = React.useRef(null)
|
|
127
127
|
const loadDirRef = React.useRef(null)
|
|
128
128
|
const refreshRef = React.useRef(null)
|
|
129
|
-
const treeRef = React.useRef(null) //
|
|
129
|
+
const treeRef = React.useRef(null) // 唯一目录树滚动容器
|
|
130
|
+
const [viewport, setViewport] = React.useState({ scope, top: 0, height: 0 })
|
|
130
131
|
const reloadDirRef = React.useRef(null) // loadDir 最新闭包(文件变化监听读取,防陈旧闭包)
|
|
131
132
|
const changedTimerRef = React.useRef(null) // 文件变化合并去抖计时器
|
|
132
133
|
const changedRelRef = React.useRef(new Set()) // 待重列的相对目录集合(去抖窗口内合并)
|
|
@@ -338,8 +339,6 @@ export function FileExplorer(props) {
|
|
|
338
339
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
339
340
|
}, [])
|
|
340
341
|
|
|
341
|
-
// 在恢复工作区展开状态之后跟随活动文件;隐藏期间目标由编辑器保留,重新挂载时兑现。
|
|
342
|
-
const { revealPath } = useExplorerReveal({ scope, activePath, treeRef, prepare: prepareReveal })
|
|
343
342
|
const activeTreePath = treeRevealPath(activePath)
|
|
344
343
|
|
|
345
344
|
// 10s 轻量跟随:已展开目录后台刷新(命中 host 索引,近零成本),树跟随 agent 写入
|
|
@@ -355,24 +354,71 @@ export function FileExplorer(props) {
|
|
|
355
354
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
356
355
|
}, [scope])
|
|
357
356
|
|
|
358
|
-
/** @private @author ddj 2026年10月
|
|
359
|
-
function flatten() {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
result.push({ key: e.path, e, depth, isDir, isOpen })
|
|
368
|
-
if (isDir && isOpen) visit(e.path, depth + 1)
|
|
369
|
-
}
|
|
370
|
-
if (entries.length >= DIR_CAP) result.push({ key: rel + ':cap', depth, text: '目录条目过多,仅显示前 ' + DIR_CAP + ' 项' })
|
|
357
|
+
/** @private @author ddj 2026年10月22号 @returns Complete expanded model; icon-only updates do not flatten again. */
|
|
358
|
+
function flatten() { return flattenTree(entriesOf, expanded, loading) }
|
|
359
|
+
const treeRows = React.useMemo(flatten, [dirs, expanded, loading, scope])
|
|
360
|
+
const previousRows = React.useRef({ scope, rows: treeRows })
|
|
361
|
+
let anchoredTop = 0
|
|
362
|
+
if (viewport.scope === scope) {
|
|
363
|
+
anchoredTop = viewport.top
|
|
364
|
+
if (previousRows.current.scope === scope && previousRows.current.rows !== treeRows) {
|
|
365
|
+
anchoredTop = treeAnchorTop(previousRows.current.rows, treeRows, viewport.top)
|
|
371
366
|
}
|
|
372
|
-
visit('', 0)
|
|
373
|
-
return result
|
|
374
367
|
}
|
|
375
|
-
const
|
|
368
|
+
const currentViewport = { top: anchoredTop, height: viewport.height }
|
|
369
|
+
const windowRows = treeWindow(treeRows.length, currentViewport)
|
|
370
|
+
|
|
371
|
+
/** @private @author ddj 2026年10月22号 Read current geometry after scroll, resize or scope restoration. */
|
|
372
|
+
function measureTree() {
|
|
373
|
+
const host = treeRef.current
|
|
374
|
+
if (!host || host.clientHeight <= 0) return
|
|
375
|
+
setViewport({ scope, top: host.scrollTop, height: host.clientHeight })
|
|
376
|
+
}
|
|
377
|
+
const scrollFrame = React.useRef(null)
|
|
378
|
+
/** @private @author ddj 2026年10月22号 Coalesce high-frequency scrolling into one measured render per frame. */
|
|
379
|
+
function scrollTree() {
|
|
380
|
+
if (scrollFrame.current !== null) return
|
|
381
|
+
scrollFrame.current = requestAnimationFrame(readScroll)
|
|
382
|
+
}
|
|
383
|
+
/** @private @author ddj 2026年10月22号 Read only after the pending animation frame fires. */
|
|
384
|
+
function readScroll() { scrollFrame.current = null; measureTree() }
|
|
385
|
+
/** @private @author ddj 2026年10月22号 @returns Resize observer and pending frame cleanup. Keep hidden trees measurable when shown. */
|
|
386
|
+
function watchTree() {
|
|
387
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measureTree) : null
|
|
388
|
+
if (treeRef.current) observer?.observe(treeRef.current)
|
|
389
|
+
measureTree()
|
|
390
|
+
/** @private @author ddj 2026年10月22号 Stop all viewport callbacks owned by this scope. */
|
|
391
|
+
function cleanup() {
|
|
392
|
+
observer?.disconnect()
|
|
393
|
+
if (scrollFrame.current !== null) cancelAnimationFrame(scrollFrame.current)
|
|
394
|
+
scrollFrame.current = null
|
|
395
|
+
}
|
|
396
|
+
return cleanup
|
|
397
|
+
}
|
|
398
|
+
React.useLayoutEffect(watchTree, [scope])
|
|
399
|
+
/** @private @author ddj 2026年10月22号 Clamp actual scroll after collapse, filtering, or workspace switch. */
|
|
400
|
+
function clampTree() {
|
|
401
|
+
const host = treeRef.current
|
|
402
|
+
previousRows.current = { scope, rows: treeRows }
|
|
403
|
+
if (!host || host.clientHeight <= 0) return
|
|
404
|
+
if (host.scrollTop !== windowRows.top) host.scrollTop = windowRows.top
|
|
405
|
+
if (viewport.scope !== scope || viewport.top !== windowRows.top) {
|
|
406
|
+
setViewport({ scope, top: windowRows.top, height: host.clientHeight })
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
React.useLayoutEffect(clampTree, [scope, windowRows.top, treeRows])
|
|
410
|
+
/** @private @author ddj 2026年10月22号 @param path Requested file. @returns Whether its complete model row is available. */
|
|
411
|
+
function locateTree(path) {
|
|
412
|
+
const host = treeRef.current
|
|
413
|
+
if (!host) return false
|
|
414
|
+
const top = treeRevealTop(treeRows, path, { top: host.scrollTop, height: host.clientHeight })
|
|
415
|
+
if (top === null) return false
|
|
416
|
+
host.scrollTop = top
|
|
417
|
+
setViewport({ scope, top, height: host.clientHeight })
|
|
418
|
+
return true
|
|
419
|
+
}
|
|
420
|
+
// 定位由完整数据索引兑现,目标无需预先挂载 DOM;异步祖先加载后 hook 会重试。
|
|
421
|
+
const { revealPath } = useExplorerReveal({ scope, activePath, treeRef, prepare: prepareReveal, locate: locateTree })
|
|
376
422
|
/** @private @author ddj 2026年10月10号 @returns Stable RPC row projection, independent of icon-only renders. */
|
|
377
423
|
function projectRows() { return treeRows.filter((row) => row.e).map((row) => ({ path: row.e.path, isDir: row.isDir })) }
|
|
378
424
|
const svnRows = React.useMemo(projectRows, [treeRows])
|
|
@@ -384,10 +430,10 @@ export function FileExplorer(props) {
|
|
|
384
430
|
/** @private @author ddj 2026年10月10号 @param path Row path. @param type Entry type. @param event Context trigger. */
|
|
385
431
|
function contextRow(path, type, event) {
|
|
386
432
|
event.preventDefault(); event.stopPropagation()
|
|
387
|
-
setMenu({ x: event.clientX, y: event.clientY, target: { path, type } })
|
|
433
|
+
setMenu({ scope, x: event.clientX, y: event.clientY, target: { path, type } })
|
|
388
434
|
}
|
|
389
435
|
const onSelect = React.useCallback((path, isDir) => selectRef.current(path, isDir), [])
|
|
390
|
-
const onContext = React.useCallback(contextRow, [])
|
|
436
|
+
const onContext = React.useCallback(contextRow, [scope])
|
|
391
437
|
const rootName = root ? String(root).split(/[\\/]/).pop() || root : ''
|
|
392
438
|
|
|
393
439
|
/** @private @author ddj 2026年10月10号 @param row Flat descriptor. @returns Memo row; unaffected status objects retain identity. */
|
|
@@ -405,10 +451,14 @@ export function FileExplorer(props) {
|
|
|
405
451
|
ev.preventDefault()
|
|
406
452
|
ev.stopPropagation()
|
|
407
453
|
if (!ev.currentTarget.contains(ev.target)) return
|
|
408
|
-
setMenu({ x: ev.clientX, y: ev.clientY, target: { path: '', type: 'directory' } })
|
|
454
|
+
setMenu({ scope, x: ev.clientX, y: ev.clientY, target: { path: '', type: 'directory' } })
|
|
409
455
|
}
|
|
456
|
+
const menuValid = menu?.scope === scope && (menu.target.path === '' || treeRows.some((row) => row.e?.path === menu.target.path))
|
|
457
|
+
/** @private @author ddj 2026年10月22号 Close stale path menus instead of rebinding recycled rows. */
|
|
458
|
+
function closeLostMenu() { if (menu && !menuValid) setMenu(null) }
|
|
459
|
+
React.useEffect(closeLostMenu, [menu, menuValid])
|
|
410
460
|
// 当前右键目标的可视菜单项(构建时过滤/排序,registry 变化下次打开生效)。
|
|
411
|
-
const menuEntries =
|
|
461
|
+
const menuEntries = menuValid
|
|
412
462
|
? buildTreeMenu(ctx?.fileMenuItems, menu.target, ctx).map((item) => ({
|
|
413
463
|
id: item.id,
|
|
414
464
|
label: item.label,
|
|
@@ -443,13 +493,16 @@ export function FileExplorer(props) {
|
|
|
443
493
|
}, '↓') : null,
|
|
444
494
|
React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: () => emitRefresh({ scope, kind: 'unknown' }) }, refreshIconEl())),
|
|
445
495
|
svn.message ? React.createElement('div', { className: 'edrv-tree-status', role: 'status' }, svn.message) : null,
|
|
446
|
-
|
|
447
|
-
(errorText
|
|
496
|
+
(errorText
|
|
448
497
|
? React.createElement('div', { className: 'edrv-tree-error' },
|
|
449
498
|
React.createElement('span', null, String(errorText)),
|
|
450
499
|
React.createElement('button', { className: 'edrv-side-btn', onClick: () => emitRefresh({ scope, kind: 'unknown' }) }, '重试'))
|
|
451
500
|
: null),
|
|
452
|
-
|
|
501
|
+
React.createElement('div', { className: 'edrv-tree edrv-tree-virtual', ref: treeRef, onScroll: scrollTree },
|
|
502
|
+
React.createElement('div', { 'aria-hidden': true, style: { height: windowRows.start * TREE_ROW_HEIGHT } }),
|
|
503
|
+
treeRows.slice(windowRows.start, windowRows.end).map(renderRow),
|
|
504
|
+
React.createElement('div', { 'aria-hidden': true, style: { height: (treeRows.length - windowRows.end) * TREE_ROW_HEIGHT } })),
|
|
505
|
+
|
|
453
506
|
(menu && menuEntries.length
|
|
454
507
|
? React.createElement(ContextMenu, { x: menu.x, y: menu.y, entries: menuEntries, onClose: () => setMenu(null) })
|
|
455
508
|
: null))
|
|
@@ -484,7 +484,7 @@ export function SvnPanel(props) {
|
|
|
484
484
|
const [remote, setRemote] = React.useState(null)
|
|
485
485
|
const [remoteBusy, setRemoteBusy] = React.useState(false)
|
|
486
486
|
// W2-5:配对候选的文件大小(path → size|null)与成对结果
|
|
487
|
-
const [
|
|
487
|
+
const [sizeSnapshot, setSizes] = React.useState(null)
|
|
488
488
|
// 渐进渲染预算:过滤/作用域变化时重置,避免过滤后仍停留在旧的大预算
|
|
489
489
|
const [renderLimit, setRenderLimit] = React.useState(RENDER_STEP)
|
|
490
490
|
// ctx 由编辑区每轮渲染重建;行动作回调改读 ref 取最新值,回调身份才能稳定(行 React.memo 生效前提)
|
|
@@ -606,14 +606,30 @@ export function SvnPanel(props) {
|
|
|
606
606
|
() => [...missingEntries, ...unversionedEntries].slice(0, FILE_SIZES_CAP).map((entry) => entry.path),
|
|
607
607
|
[missingEntries, unversionedEntries],
|
|
608
608
|
)
|
|
609
|
-
const candidateKey =
|
|
610
|
-
const
|
|
609
|
+
const candidateKey = JSON.stringify(requested)
|
|
610
|
+
const sizeOwner = JSON.stringify([scope, sessionId, candidateKey, managed])
|
|
611
|
+
const sizeOwnerRef = React.useRef(sizeOwner)
|
|
612
|
+
sizeOwnerRef.current = sizeOwner
|
|
613
|
+
const sizes = sizeSnapshot?.owner === sizeOwner ? sizeSnapshot.table : {}
|
|
614
|
+
const pairsNeeded = managed && missingEntries.length > 0 && unversionedEntries.length > 0
|
|
611
615
|
const pairsKnown = pairsNeeded && requested.every((path) => path in sizes)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
616
|
+
|
|
617
|
+
/** @private @author ddj 2026年10月22号 @returns Cleanup invalidating this request. @description Keep sizes scoped to this candidate generation; failures do not retry in a render loop. */
|
|
618
|
+
function loadPairSizes() {
|
|
619
|
+
if (!pairsNeeded || !requested.length) return
|
|
620
|
+
let disposed = false
|
|
621
|
+
/** @author ddj 2026年10月22号 @param table Candidate sizes. @description Publish only to the current request owner. */
|
|
622
|
+
function acceptSizes(table) {
|
|
623
|
+
if (!disposed && sizeOwnerRef.current === sizeOwner) setSizes({ owner: sizeOwner, table })
|
|
624
|
+
}
|
|
625
|
+
/** @author ddj 2026年10月22号 @description Failed reads leave pairing unknown without breaking the changes list. */
|
|
626
|
+
function ignoreFailure() { /* A future candidate/session change may retry. */ }
|
|
627
|
+
/** @author ddj 2026年10月22号 @description Revoke this request on dependency change or unmount. */
|
|
628
|
+
function cancelSizes() { disposed = true }
|
|
629
|
+
void svnFileSizes(sessionId, requested).then(acceptSizes, ignoreFailure)
|
|
630
|
+
return cancelSizes
|
|
631
|
+
}
|
|
632
|
+
React.useEffect(loadPairSizes, [sizeOwner, pairsNeeded])
|
|
617
633
|
|
|
618
634
|
const pairNoteOf = React.useMemo(() => {
|
|
619
635
|
if (!pairsKnown) return {}
|
|
@@ -10,9 +10,8 @@ import { IconBranchOutline16, IconFolderOpenOutline16, IconListPenOutline16, Ico
|
|
|
10
10
|
import { dapStore } from '../../dap/store.js'
|
|
11
11
|
import { FileExplorer } from './FileExplorer.js'
|
|
12
12
|
import { SearchPanel } from './SearchPanel.js'
|
|
13
|
-
import {
|
|
13
|
+
import { LazyPanel } from '../../lazyPanel.js'
|
|
14
14
|
import { SvnPanel } from './SvnPanel.js'
|
|
15
|
-
import { DebugPanel } from './DebugPanel.js'
|
|
16
15
|
import type { SidebarPanelDef, SidebarCtx } from '../types.js'
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -57,7 +56,7 @@ export function createRulesPanel(): SidebarPanelDef {
|
|
|
57
56
|
title: '规则',
|
|
58
57
|
icon: IconListPenOutline16,
|
|
59
58
|
order: 20,
|
|
60
|
-
render: (ctx: SidebarCtx) => React.createElement(
|
|
59
|
+
render: (ctx: SidebarCtx) => React.createElement(LazyPanel, { key: 'rules', panel: 'rules', ctx }),
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
@@ -113,6 +112,6 @@ export function createDebugPanel(): SidebarPanelDef {
|
|
|
113
112
|
icon: '🐞',
|
|
114
113
|
order: 22,
|
|
115
114
|
badge: () => (dapStore.getSnapshot().phase === 'paused' ? 1 : null),
|
|
116
|
-
render: (ctx: SidebarCtx) => React.createElement(
|
|
115
|
+
render: (ctx: SidebarCtx) => React.createElement(LazyPanel, { key: 'debug', panel: 'debug', ctx }),
|
|
117
116
|
}
|
|
118
117
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** File explorer fixed-height row model. @author ddj 2026年10月22号 */
|
|
2
|
+
import type { TreeEntry } from '../../../shared/rpc.js'
|
|
3
|
+
import { updateRange } from '../../ui/svnUpdateModel.js'
|
|
4
|
+
|
|
5
|
+
/** Fixed geometry shared with file and loading row CSS. */
|
|
6
|
+
export const TREE_ROW_HEIGHT = 24
|
|
7
|
+
|
|
8
|
+
export interface TreeRow {
|
|
9
|
+
key: string
|
|
10
|
+
depth: number
|
|
11
|
+
e?: TreeEntry
|
|
12
|
+
isDir?: boolean
|
|
13
|
+
isOpen?: boolean
|
|
14
|
+
text?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TreeViewport {
|
|
18
|
+
top: number
|
|
19
|
+
height: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @public @author ddj 2026年10月22号
|
|
24
|
+
* @description Preserve the first visible path when rows above it expand or collapse.
|
|
25
|
+
* @param previous Previous complete model.
|
|
26
|
+
* @param rows New complete model.
|
|
27
|
+
* @param top Previous scroll offset.
|
|
28
|
+
* @returns Anchored offset, or original offset for subsequent clamping if path disappeared.
|
|
29
|
+
*/
|
|
30
|
+
export function treeAnchorTop(previous: TreeRow[], rows: TreeRow[], top: number): number {
|
|
31
|
+
const index = Math.floor(Math.max(0, top) / TREE_ROW_HEIGHT)
|
|
32
|
+
const anchor = previous[index]?.key
|
|
33
|
+
if (!anchor) return top
|
|
34
|
+
const nextIndex = rows.findIndex((row) => row.key === anchor)
|
|
35
|
+
if (nextIndex < 0) return top
|
|
36
|
+
return nextIndex * TREE_ROW_HEIGHT + top % TREE_ROW_HEIGHT
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @public @author ddj 2026年10月22号
|
|
41
|
+
* @description Flatten all expanded entries in their supplied order, including metadata files.
|
|
42
|
+
* @param entriesOf Directory snapshot lookup; null means unavailable.
|
|
43
|
+
* @param expanded Current directory expansion flags.
|
|
44
|
+
* @param loading Current loading flags.
|
|
45
|
+
* @returns Complete row model, independent of viewport mounting.
|
|
46
|
+
*/
|
|
47
|
+
export function flattenTree(entriesOf: (rel: string) => TreeEntry[] | null,
|
|
48
|
+
expanded: Record<string, boolean>, loading: Record<string, boolean>): TreeRow[] {
|
|
49
|
+
const rows: TreeRow[] = []
|
|
50
|
+
const pending: { rel: string; depth: number; entry?: TreeEntry }[] = [{ rel: '', depth: 0 }]
|
|
51
|
+
while (pending.length) {
|
|
52
|
+
const current = pending.pop()!
|
|
53
|
+
const entry = current.entry
|
|
54
|
+
if (entry) {
|
|
55
|
+
const isDir = entry.type === 'directory'
|
|
56
|
+
const isOpen = expanded[entry.path] === true
|
|
57
|
+
rows.push({ key: entry.path, e: entry, depth: current.depth, isDir, isOpen })
|
|
58
|
+
if (isDir && isOpen) pending.push({ rel: entry.path, depth: current.depth + 1 })
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entries = entriesOf(current.rel)
|
|
63
|
+
if (!entries) {
|
|
64
|
+
rows.push({ key: current.rel + ':loading', depth: current.depth,
|
|
65
|
+
text: loading[current.rel] ? '加载中…' : '无法加载' })
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
// Stack reverses insertion order; push backwards to preserve host sorting.
|
|
69
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
70
|
+
pending.push({ rel: current.rel, depth: current.depth, entry: entries[index] })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return rows
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @public @author ddj 2026年10月22号
|
|
78
|
+
* @description Clamp scroll state before reusing the existing fixed-height range calculation.
|
|
79
|
+
* @param count Complete row count.
|
|
80
|
+
* @param viewport Latest scroll position and measured viewport height.
|
|
81
|
+
* @returns Clamped scroll offset and overscanned exclusive row range.
|
|
82
|
+
*/
|
|
83
|
+
export function treeWindow(count: number, viewport: TreeViewport) {
|
|
84
|
+
const height = Math.max(0, viewport.height)
|
|
85
|
+
const top = Math.min(Math.max(0, viewport.top), Math.max(0, count * TREE_ROW_HEIGHT - height))
|
|
86
|
+
return { top, ...updateRange(count, top, height, TREE_ROW_HEIGHT) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @public @author ddj 2026年10月22号
|
|
91
|
+
* @description Locate an unmounted row using model indices, without querying row DOM.
|
|
92
|
+
* @param rows Complete flattened model.
|
|
93
|
+
* @param path Requested relative file path.
|
|
94
|
+
* @param viewport Current viewport geometry.
|
|
95
|
+
* @returns New scroll offset, or null while ancestors/viewport are unavailable.
|
|
96
|
+
*/
|
|
97
|
+
export function treeRevealTop(rows: TreeRow[], path: string, viewport: TreeViewport): number | null {
|
|
98
|
+
if (viewport.height <= 0) return null
|
|
99
|
+
const index = rows.findIndex((row) => row.e?.path === path)
|
|
100
|
+
if (index < 0) return null
|
|
101
|
+
const current = treeWindow(rows.length, viewport).top
|
|
102
|
+
const rowTop = index * TREE_ROW_HEIGHT
|
|
103
|
+
if (rowTop >= current && rowTop + TREE_ROW_HEIGHT <= current + viewport.height) return current
|
|
104
|
+
return treeWindow(rows.length, { top: rowTop - (viewport.height - TREE_ROW_HEIGHT) / 2,
|
|
105
|
+
height: viewport.height }).top
|
|
106
|
+
}
|
|
@@ -27,6 +27,8 @@ interface RevealOptions {
|
|
|
27
27
|
activePath: string | null
|
|
28
28
|
treeRef: React.RefObject<HTMLElement>
|
|
29
29
|
prepare: (path: string) => void
|
|
30
|
+
/** Optional virtual-list locator; success does not depend on mounted row DOM. */
|
|
31
|
+
locate?: (path: string) => boolean
|
|
30
32
|
}
|
|
31
33
|
interface RevealTarget {
|
|
32
34
|
scope: string
|
|
@@ -60,12 +62,13 @@ export function useExplorerReveal(options: RevealOptions) {
|
|
|
60
62
|
setTarget(next)
|
|
61
63
|
if (next) current.prepare(next.path)
|
|
62
64
|
}
|
|
63
|
-
/** @private @author ddj 2026年10月
|
|
65
|
+
/** @private @author ddj 2026年10月22号 Attempts virtual data-index location or the original mounted-row fallback after commit/resize; consumes only a successful current request. */
|
|
64
66
|
function tryReveal(): void {
|
|
65
67
|
const next = pending.current
|
|
66
68
|
const current = latest.current
|
|
67
69
|
if (!next || next.scope !== current.scope || next.active !== current.activePath) return
|
|
68
|
-
|
|
70
|
+
const located = current.locate ? current.locate(next.path) : revealTreeRow(current.treeRef.current, next.path)
|
|
71
|
+
if (!located) return
|
|
69
72
|
pending.current = null
|
|
70
73
|
setRevealPath(next.path)
|
|
71
74
|
clearHighlight()
|
|
@@ -28,8 +28,11 @@ function panelRegister(entries: Map<string, SidebarPanelDef>, notify: () => void
|
|
|
28
28
|
export function createSidebarPanelRegistry(): SidebarPanelRegistry {
|
|
29
29
|
const entries = new Map<string, SidebarPanelDef>()
|
|
30
30
|
const listeners = new Set<() => void>()
|
|
31
|
+
/** @private @author ddj 2026年10月22号 @description Isolate subscribers and snapshot notification membership against reentrant changes. */
|
|
31
32
|
const notify = (): void => {
|
|
32
|
-
for (const listener of listeners)
|
|
33
|
+
for (const listener of [...listeners]) {
|
|
34
|
+
try { listener() } catch { /* One extension must not block other subscribers or registration teardown. */ }
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
37
|
const list = (): readonly SidebarPanelDef[] =>
|
|
35
38
|
[...entries.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100))
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — Monaco model 预热调度层。
|
|
3
|
+
*
|
|
4
|
+
* 背景:Monaco 的 GotoDefinitionAtPositionEditorContribution 在 Ctrl+hover 渲染定义链接
|
|
5
|
+
* 预览时,对目标的 edrv:// uri 调 textModelResolverService.createModelReference 后
|
|
6
|
+
* **只挂 then、没有 catch**;目标文件若从未在本会话打开过,该 uri 没有已注册 model,
|
|
7
|
+
* createModelReference 直接 reject("Model not found"),成为未捕获的 promise 异常。
|
|
8
|
+
* 本构建的 vendored Monaco 也没有 registerTextModelContentProvider 可供惰性喂内容,
|
|
9
|
+
* 因此修法是在 provideDefinition 返回前把目标 model 建出来(预热)。
|
|
10
|
+
*
|
|
11
|
+
* 本模块只负责「让某路径的 model 存在」这件事的调度:安装/卸载实现、并发单飞、
|
|
12
|
+
* 失败冷却与等待上限。真正建 model 的实现由 EditorView 注入(它才持有 monaco 与文档缓存)。
|
|
13
|
+
* 纯逻辑,不依赖 monaco,可单测。
|
|
14
|
+
* 作者 ddj 2026年10月20号
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// --region 常量与状态
|
|
18
|
+
|
|
19
|
+
/** 单次预热调用的等待上限:超过即放行 hover(预热仍在后台继续)。 */
|
|
20
|
+
export const WARM_TIMEOUT_MS = 400
|
|
21
|
+
|
|
22
|
+
/** 预热失败后该路径的冷却窗口,避免每次 hover 都重试同一失败目标。 */
|
|
23
|
+
export const WARM_COOLDOWN_MS = 10_000
|
|
24
|
+
|
|
25
|
+
/** 冷却表条目上限(超出按插入序丢最旧),防长会话里无限增长。 */
|
|
26
|
+
export const WARM_COOLDOWN_CAP = 64
|
|
27
|
+
|
|
28
|
+
/** 预热实现:给定工作区相对路径,成功让 model 存在则返回 true。 */
|
|
29
|
+
export type WarmImpl = (path: string) => Promise<boolean>
|
|
30
|
+
|
|
31
|
+
/** 当前安装的预热实现(EditorView 挂载时注入,未挂载则为 null)。 */
|
|
32
|
+
let impl: WarmImpl | null = null
|
|
33
|
+
/** path → 进行中的预热(单飞:连续 hover 同一目标只读一次盘)。 */
|
|
34
|
+
const inFlight = new Map<string, Promise<boolean>>()
|
|
35
|
+
/** path → 冷却截止时间戳(毫秒)。 */
|
|
36
|
+
const cooled = new Map<string, number>()
|
|
37
|
+
// --endregion
|
|
38
|
+
|
|
39
|
+
// --region 状态维护
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 清空进行中/冷却状态(安装或卸载实现时复位,避免跨实现串味)。
|
|
43
|
+
* @author ddj 2026年10月20号
|
|
44
|
+
*/
|
|
45
|
+
function resetWarmerState(): void {
|
|
46
|
+
inFlight.clear()
|
|
47
|
+
cooled.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 该路径是否处于失败冷却窗口内(过期的顺手清掉)。
|
|
52
|
+
* @author ddj 2026年10月20号
|
|
53
|
+
* @param path 工作区相对路径
|
|
54
|
+
* @returns 仍在冷却中返回 true
|
|
55
|
+
*/
|
|
56
|
+
function isCooling(path: string): boolean {
|
|
57
|
+
const until = cooled.get(path)
|
|
58
|
+
if (until === undefined) return false
|
|
59
|
+
if (until > Date.now()) return true
|
|
60
|
+
|
|
61
|
+
cooled.delete(path)
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 记录一次失败冷却,并把冷却表裁剪到上限以内。
|
|
67
|
+
* @author ddj 2026年10月20号
|
|
68
|
+
* @param path 工作区相对路径
|
|
69
|
+
*/
|
|
70
|
+
function coolDown(path: string): void {
|
|
71
|
+
cooled.delete(path)
|
|
72
|
+
cooled.set(path, Date.now() + WARM_COOLDOWN_MS)
|
|
73
|
+
|
|
74
|
+
while (cooled.size > WARM_COOLDOWN_CAP) {
|
|
75
|
+
const oldest = cooled.keys().next()
|
|
76
|
+
if (oldest.done) break
|
|
77
|
+
|
|
78
|
+
cooled.delete(oldest.value)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 结算一次预热:清 inFlight(仅当仍是自己那条)、按结果维护冷却。
|
|
84
|
+
* @author ddj 2026年10月20号
|
|
85
|
+
* @param path 工作区相对路径
|
|
86
|
+
* @param run 该次预热的 promise(用于识别是否已被更新的一次替换)
|
|
87
|
+
* @param ok 实现返回的成功与否
|
|
88
|
+
*/
|
|
89
|
+
function settleWarm(path: string, run: Promise<boolean>, ok: boolean): void {
|
|
90
|
+
if (inFlight.get(path) === run) inFlight.delete(path)
|
|
91
|
+
if (ok) {
|
|
92
|
+
cooled.delete(path)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
coolDown(path)
|
|
97
|
+
}
|
|
98
|
+
// --endregion
|
|
99
|
+
|
|
100
|
+
// --region 底层预热
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 启动一次底层预热:登记 inFlight、把实现异常折叠成 false(绝不向调用方抛)。
|
|
104
|
+
* @author ddj 2026年10月20号
|
|
105
|
+
* @param path 工作区相对路径
|
|
106
|
+
* @param fn 预热实现
|
|
107
|
+
* @returns 永不 reject 的布尔 promise(已登记到 inFlight)
|
|
108
|
+
*/
|
|
109
|
+
function startWarm(path: string, fn: WarmImpl): Promise<boolean> {
|
|
110
|
+
const run = Promise.resolve()
|
|
111
|
+
.then(() => fn(path))
|
|
112
|
+
.then(
|
|
113
|
+
(ok) => ok === true,
|
|
114
|
+
() => false,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
inFlight.set(path, run)
|
|
118
|
+
run.then((ok) => settleWarm(path, run, ok))
|
|
119
|
+
return run
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 给底层预热挂一个等待上限:超时只是本次调用放行,底层继续跑(仍可能建成 model
|
|
124
|
+
* 惠及下一次 hover),因此超时既不取消 promise,也不提前摘掉 inFlight。
|
|
125
|
+
* @author ddj 2026年10月20号
|
|
126
|
+
* @param path 工作区相对路径
|
|
127
|
+
* @param run 底层预热 promise(永不 reject)
|
|
128
|
+
* @returns 最多 WARM_TIMEOUT_MS 内结算的布尔结果
|
|
129
|
+
*/
|
|
130
|
+
function withTimeout(path: string, run: Promise<boolean>): Promise<boolean> {
|
|
131
|
+
return new Promise<boolean>((resolve) => {
|
|
132
|
+
let settled = false
|
|
133
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
134
|
+
|
|
135
|
+
const finish = (ok: boolean): void => {
|
|
136
|
+
if (settled) return
|
|
137
|
+
|
|
138
|
+
settled = true
|
|
139
|
+
if (timer) clearTimeout(timer)
|
|
140
|
+
resolve(ok)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
timer = setTimeout(() => {
|
|
144
|
+
coolDown(path)
|
|
145
|
+
finish(false)
|
|
146
|
+
}, WARM_TIMEOUT_MS)
|
|
147
|
+
|
|
148
|
+
run.then(finish, () => finish(false))
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
// --endregion
|
|
152
|
+
|
|
153
|
+
// --region 安装与卸载
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 安装预热实现(EditorView 挂载时调用;传 null 视同卸载)。
|
|
157
|
+
* @author ddj 2026年10月20号
|
|
158
|
+
* @param fn 预热实现,或 null 表示卸载
|
|
159
|
+
*/
|
|
160
|
+
export function setModelWarmer(fn: WarmImpl | null): void {
|
|
161
|
+
impl = typeof fn === 'function' ? fn : null
|
|
162
|
+
if (!impl) resetWarmerState()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 卸载预热实现:仅当传入的仍是当前实现才清(防卸载竞态误清新挂载的实现)。
|
|
167
|
+
* @author ddj 2026年10月20号
|
|
168
|
+
* @param fn 当初安装的实现
|
|
169
|
+
*/
|
|
170
|
+
export function clearModelWarmer(fn: WarmImpl): void {
|
|
171
|
+
if (impl !== fn) return
|
|
172
|
+
|
|
173
|
+
impl = null
|
|
174
|
+
resetWarmerState()
|
|
175
|
+
}
|
|
176
|
+
// --endregion
|
|
177
|
+
|
|
178
|
+
// --region 预热调度
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 确保目标路径的 Monaco model 存在(供 hover 定义预览的 createModelReference 命中)。
|
|
182
|
+
*
|
|
183
|
+
* 降级语义:未安装实现(编辑器页签未挂载)、路径为空、处于失败冷却、实现失败或超时,
|
|
184
|
+
* 一律返回 false 且不抛异常——此时行为等价于修复前(预览不显示,但不再产生未捕获异常)。
|
|
185
|
+
* @author ddj 2026年10月20号
|
|
186
|
+
* @param path 工作区相对路径(或绝对路径,由实现决定如何解析)
|
|
187
|
+
* @returns 预热成功返回 true;被跳过/失败/超时返回 false
|
|
188
|
+
*/
|
|
189
|
+
export function warmModelFor(path: string): Promise<boolean> {
|
|
190
|
+
if (!path || !impl) return Promise.resolve(false)
|
|
191
|
+
|
|
192
|
+
const running = inFlight.get(path)
|
|
193
|
+
if (running) return withTimeout(path, running)
|
|
194
|
+
if (isCooling(path)) return Promise.resolve(false)
|
|
195
|
+
|
|
196
|
+
return withTimeout(path, startWarm(path, impl))
|
|
197
|
+
}
|
|
198
|
+
// --endregion
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
/* 固定页签:📌 标记 + 免关闭按钮(固定页签不渲染 ×,仅可经右键菜单取消固定) */
|
|
17
17
|
[data-edrv-view] .edrv-tab .edrv-tab-pin { flex-shrink: 0; font-size: 10px; line-height: 1; opacity: .85; }
|
|
18
18
|
[data-edrv-view] .edrv-tab.edrv-tab-pinned { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,.04)); }
|
|
19
|
+
/* 页签行 = 可横滚的页签区 + 行尾固定工具列(计划预览页签与磁盘页签同处这一行的滚动区)@author ddj 2026年10月20号 */
|
|
20
|
+
[data-edrv-view] .edrv-tabs-row { display: flex; align-items: center; min-width: 0; flex: 0 0 auto; background: var(--dsw-alias-bg-layer-1, transparent); }
|
|
21
|
+
/* 特异性需盖过下面 .edrv-group .edrv-tabs 的 flex: 0 0 auto,页签区才能吃掉行走宽、把工具列压到行尾 */
|
|
22
|
+
[data-edrv-view] .edrv-group .edrv-tabs-row > .edrv-tabs { flex: 1 1 auto; min-width: 0; background: transparent; }
|
|
23
|
+
[data-edrv-view] .edrv-tabs-tools { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; padding: 0 6px 0 2px; }
|
|
19
24
|
[data-edrv-view] .edrv-tab-add { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-left: 2px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); font-size: 14px; cursor: pointer; flex-shrink: 0; }
|
|
20
25
|
[data-edrv-view] .edrv-tab-add:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
|
|
21
26
|
[data-edrv-view] .edrv-pathbar { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; background: var(--dsw-alias-bg-layer-1, #ffffff); border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); font-size: 12px; flex-shrink: 0; overflow: hidden; }
|
|
@@ -300,6 +305,14 @@
|
|
|
300
305
|
[data-edrv-view] .edrv-side-btn { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); font-size: 12px; cursor: pointer; flex-shrink: 0; }
|
|
301
306
|
[data-edrv-view] .edrv-side-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
302
307
|
[data-edrv-view] .edrv-tree { flex: 1; min-height: 0; overflow: auto; padding: 4px 6px 12px; font-size: 12px; }
|
|
308
|
+
/* Virtual rows and placeholders share the same geometry as treeWindow.ts. @author ddj 2026年10月22号 */
|
|
309
|
+
[data-edrv-view] .edrv-tree.edrv-tree-virtual { padding-top: 0; padding-bottom: 0; }
|
|
310
|
+
[data-edrv-view] .edrv-tree-virtual .edrv-tree-row,
|
|
311
|
+
[data-edrv-view] .edrv-tree-virtual .edrv-tree-loading { height: 24px; min-height: 24px; box-sizing: border-box; }
|
|
312
|
+
[data-edrv-view] .edrv-tree-virtual .edrv-tree-loading { line-height: 24px; }
|
|
313
|
+
[data-edrv-view] .edrv-large-text { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
|
314
|
+
[data-edrv-view] .edrv-large-text-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
315
|
+
[data-edrv-view] .edrv-large-text-content { margin: 0; flex: 1; min-height: 0; overflow: auto; white-space: pre; font-family: var(--ds-font-family-code, ui-monospace, monospace); user-select: text; }
|
|
303
316
|
[data-edrv-view] .edrv-tree-row { display: flex; align-items: center; gap: 4px; height: 22px; padding-right: 6px; border-radius: 5px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; white-space: nowrap; }
|
|
304
317
|
[data-edrv-view] .edrv-tree-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
305
318
|
[data-edrv-view] .edrv-tree-row.edrv-tree-active { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); }
|
|
@@ -988,7 +1001,6 @@
|
|
|
988
1001
|
[data-edrv-view] .edrv-group { border: 0; }
|
|
989
1002
|
[data-edrv-view] .edrv-group .edrv-tabs { flex: 0 0 auto; touch-action: none; }
|
|
990
1003
|
[data-edrv-view] .edrv-group-tab-name { overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
|
|
991
|
-
[data-edrv-view] .edrv-workbench-tools { flex: 0 0 auto; }
|
|
992
1004
|
[data-edrv-view] .edrv-split { display: flex; flex: 1 1 auto; min-width: 0; min-height: 0; overflow: hidden; }
|
|
993
1005
|
[data-edrv-view] .edrv-split-vertical { flex-direction: column; }
|
|
994
1006
|
[data-edrv-view] .edrv-split-child { display: flex; min-width: 0; min-height: 0; overflow: hidden; }
|