dsh-vscode-mode 0.5.3 → 0.7.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 +72 -2
- package/lib/client.js +12082 -6420
- package/lib/client.js.map +1 -1
- package/lib/index.js +2750 -147
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/copyText.ts +28 -0
- package/src/client/dap/BpWidget.ts +262 -0
- package/src/client/dap/bpMenu.ts +91 -0
- package/src/client/dap/bpRowMenu.ts +53 -0
- package/src/client/dap/breakpoints.ts +222 -0
- package/src/client/dap/decorate.ts +128 -0
- package/src/client/dap/hintLine.ts +29 -0
- package/src/client/dap/hover.ts +166 -0
- package/src/client/dap/hoverMode.ts +159 -0
- package/src/client/dap/hoverTree.ts +722 -0
- package/src/client/dap/launchInsert.ts +229 -0
- package/src/client/dap/launchSnippetProvider.ts +206 -0
- package/src/client/dap/modelPath.ts +23 -0
- package/src/client/dap/panelSplit.ts +105 -0
- package/src/client/dap/pidPick.ts +24 -0
- package/src/client/dap/store.ts +571 -0
- package/src/client/dap/toolbarDrag.ts +51 -0
- package/src/client/dap/trace.ts +22 -0
- package/src/client/dap/variableTree.ts +62 -0
- package/src/client/fileClipboard.ts +46 -0
- package/src/client/index.ts +12 -3
- package/src/client/monaco/lsp/index.ts +15 -0
- package/src/client/monaco/lsp/providers.ts +2 -0
- package/src/client/monaco/theme.ts +15 -0
- package/src/client/openWithDialog.ts +66 -0
- package/src/client/searchSeed.ts +36 -0
- package/src/client/sidebar/contextMenu.ts +51 -10
- package/src/client/sidebar/menuItems.ts +473 -50
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SearchPanel.ts +24 -3
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +8 -0
- package/src/client/styles/editor.css +176 -0
- package/src/client/svnActions.ts +18 -1
- package/src/client/svnLog.ts +3 -3
- package/src/client/svnStatus.ts +140 -2
- package/src/client/tabActions.ts +22 -10
- package/src/client/ui/EditorView.ts +625 -23
- package/src/client/ui/OfficialSideTab.ts +1 -0
- package/src/client/ui/PromptDialog.ts +63 -0
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/client/ui/SvnDiffPanel.ts +21 -8
- package/src/client/ui/SvnLogDialog.ts +44 -2
- package/src/client/ui/SvnPatchDialog.ts +63 -0
- package/src/client/ui/SvnSumDialog.ts +77 -0
- package/src/client/ui/commandCatalog.ts +63 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/dap/configSnippets.ts +269 -0
- package/src/dap/discovery.ts +198 -0
- package/src/dap/launchConfig.ts +127 -0
- package/src/dap/manager.ts +588 -0
- package/src/dap/processList.ts +76 -0
- package/src/dap/protocol.ts +80 -0
- package/src/dap/provider.ts +49 -0
- package/src/dap/rpc.ts +192 -0
- package/src/dap/sourcePath.ts +82 -0
- package/src/index.ts +7 -3
- package/src/lsp/providers.ts +3 -2
- package/src/reveal.ts +31 -20
- package/src/rpc.ts +209 -5
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/fsNames.ts +115 -0
- package/src/shared/keybindings.ts +9 -0
- package/src/shared/rpc.ts +52 -2
- package/src/shared/svn.ts +147 -0
- package/src/shared/svnActions.ts +20 -0
- package/src/svn.ts +320 -11
- package/src/workspace.ts +0 -75
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — DAP 变量树纯函数。
|
|
3
|
+
* 把 DAP variablesReference 缓存展平成可渲染 sibling rows,避免把子行嵌套进父级
|
|
4
|
+
* 横向 flex 行导致变量重叠;同时限制递归深度与总行数,保护侧栏性能。
|
|
5
|
+
* 作者 ddj 2026年09月29号
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** DAP 变量最小视图。 */
|
|
9
|
+
export interface VariableLike {
|
|
10
|
+
name: string
|
|
11
|
+
value: string
|
|
12
|
+
type?: string
|
|
13
|
+
ref: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 展平后的变量行。 */
|
|
17
|
+
export interface VariableRow {
|
|
18
|
+
key: string
|
|
19
|
+
variable: VariableLike
|
|
20
|
+
depth: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 展平选项。 */
|
|
24
|
+
export interface VariableTreeOptions {
|
|
25
|
+
maxRows?: number
|
|
26
|
+
maxDepth?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 将变量树展平为 sibling rows;循环 ref 只展开一次,避免异常适配器数据造成递归。
|
|
31
|
+
* @author ddj 2026年09月29号
|
|
32
|
+
* @param roots 根变量
|
|
33
|
+
* @param expanded 已展开的 variablesReference
|
|
34
|
+
* @param cache ref → 子变量缓存
|
|
35
|
+
* @param options 行数/深度上限
|
|
36
|
+
*/
|
|
37
|
+
export function flattenVariableRows(
|
|
38
|
+
roots: readonly VariableLike[],
|
|
39
|
+
expanded: ReadonlySet<number>,
|
|
40
|
+
cache: ReadonlyMap<number, readonly VariableLike[]>,
|
|
41
|
+
options: VariableTreeOptions = {},
|
|
42
|
+
): VariableRow[] {
|
|
43
|
+
const rows: VariableRow[] = []
|
|
44
|
+
const maxRows = options.maxRows ?? 1000
|
|
45
|
+
const maxDepth = options.maxDepth ?? 32
|
|
46
|
+
const walk = (items: readonly VariableLike[], depth: number, ancestry: ReadonlySet<number>, prefix: string): void => {
|
|
47
|
+
if (depth > maxDepth || rows.length >= maxRows) return
|
|
48
|
+
for (let index = 0; index < items.length && rows.length < maxRows; index += 1) {
|
|
49
|
+
const variable = items[index]
|
|
50
|
+
const key = prefix + '/' + index + ':' + variable.name
|
|
51
|
+
rows.push({ key, variable, depth })
|
|
52
|
+
if (variable.ref <= 0 || !expanded.has(variable.ref) || ancestry.has(variable.ref)) continue
|
|
53
|
+
const children = cache.get(variable.ref)
|
|
54
|
+
if (!children?.length) continue
|
|
55
|
+
const nextAncestry = new Set(ancestry)
|
|
56
|
+
nextAncestry.add(variable.ref)
|
|
57
|
+
walk(children, depth + 1, nextAncestry, key)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
walk(roots, 0, new Set(), 'root')
|
|
61
|
+
return rows
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 文件剪贴板(剪切/复制/粘贴的应用内单槽)。
|
|
3
|
+
*
|
|
4
|
+
* 浏览器拿不到系统文件剪贴板,按 VS Code 语义做应用内文件剪贴板:
|
|
5
|
+
* 「剪切/复制」只登记源路径,「粘贴」时才走 edrv.fsMove / edrv.fsCopy 真正落盘。
|
|
6
|
+
* 模块级单槽(同 searchSeed 一次性槽模式),不依赖 React/DOM,可 node 单测。
|
|
7
|
+
* 作者 ddj 2026年09月22号
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 文件剪贴板内容。 */
|
|
11
|
+
export interface FileClip {
|
|
12
|
+
/** copy = 复制;cut = 剪切(粘贴后源被移走)。 */
|
|
13
|
+
mode: 'copy' | 'cut'
|
|
14
|
+
/** 源路径(工作区相对)。 */
|
|
15
|
+
path: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 单槽剪贴板(null = 空)。 */
|
|
19
|
+
let clip: FileClip | null = null
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 写入文件剪贴板(重复以最后一次为准)。
|
|
23
|
+
* @author ddj 2026年09月22号
|
|
24
|
+
* @param mode copy = 复制;cut = 剪切
|
|
25
|
+
* @param path 源路径(工作区相对)
|
|
26
|
+
*/
|
|
27
|
+
export function setFileClip(mode: FileClip['mode'], path: string): void {
|
|
28
|
+
clip = { mode, path }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 读取当前剪贴板(不清空;粘贴执行与「粘贴」灰显判定共用)。
|
|
33
|
+
* @author ddj 2026年09月22号
|
|
34
|
+
* @returns 剪贴板内容;空槽返回 null
|
|
35
|
+
*/
|
|
36
|
+
export function fileClipOf(): FileClip | null {
|
|
37
|
+
return clip
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 清空文件剪贴板(剪切粘贴成功后;测试隔离用)。
|
|
42
|
+
* @author ddj 2026年09月22号
|
|
43
|
+
*/
|
|
44
|
+
export function clearFileClip(): void {
|
|
45
|
+
clip = null
|
|
46
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { createFilePanel } from './sidebar/panels/index.js'
|
|
|
41
41
|
import { createSearchPanel } from './sidebar/panels/index.js'
|
|
42
42
|
import { createRulesPanel } from './sidebar/panels/index.js'
|
|
43
43
|
import { createSvnPanel } from './sidebar/panels/index.js'
|
|
44
|
+
import { createDebugPanel } from './sidebar/panels/index.js'
|
|
44
45
|
import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
|
|
45
46
|
import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
46
47
|
import { createOutlinePanel } from './outline/index.js'
|
|
@@ -54,8 +55,10 @@ import { editorLimitApply } from './editorLimit.js'
|
|
|
54
55
|
import { log } from './log.js'
|
|
55
56
|
import { setupLsp, setSession, disposeLsp } from './monaco/lsp/index.js'
|
|
56
57
|
import { disposeSnippets } from './snippets/provider.js'
|
|
58
|
+
import { disposeLaunchJson } from './dap/launchSnippetProvider.js'
|
|
57
59
|
import { disposeAiInline } from './ai/inlineProvider.js'
|
|
58
60
|
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
61
|
+
import { dapStore } from './dap/store.js'
|
|
59
62
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
60
63
|
|
|
61
64
|
// ⚠️ inject 只列必需服务:webUiSettings 是 @linxin666/dsh-client-ui-web-ui-settings 提供的
|
|
@@ -194,6 +197,11 @@ export function apply(ctx: any): void {
|
|
|
194
197
|
ctx.effect(() => sidebarPanels.register(createRulesPanel()), 'vscode-mode: sidebar panel rules')
|
|
195
198
|
// SVN 变更面板:活动栏「SVN 变更」页签(工作副本状态列表;visible 守卫使非 SVN 工作区不出现)
|
|
196
199
|
ctx.effect(() => sidebarPanels.register(createSvnPanel()), 'vscode-mode: sidebar panel svn')
|
|
200
|
+
// 调试面板:活动栏「调试」页签(VS Code 调试视图四段布局 + REPL;DAP 桥接)
|
|
201
|
+
ctx.effect(() => sidebarPanels.register(createDebugPanel()), 'vscode-mode: sidebar panel debug')
|
|
202
|
+
// 客户端(重)装配对账:host 的 DapSession 常驻,而本模块状态随刷新/HMR 归零,
|
|
203
|
+
// 不对账则调试面板与暂停态 hover 静默失效(见 dapStore.resync)。
|
|
204
|
+
void dapStore.resync()
|
|
197
205
|
// 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
|
|
198
206
|
const fileMenuItems = createTreeMenuRegistry()
|
|
199
207
|
ctx.provide('edrvFileContextMenuItems', fileMenuItems)
|
|
@@ -216,7 +224,7 @@ export function apply(ctx: any): void {
|
|
|
216
224
|
return sidebar ? registry.register(sidebar) : undefined
|
|
217
225
|
}, 'vscode-mode: sidebar file opener')
|
|
218
226
|
// 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
|
|
219
|
-
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
|
|
227
|
+
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions }))
|
|
220
228
|
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
|
|
221
229
|
const syncFileClaim = (): void => {
|
|
222
230
|
const official = officialService
|
|
@@ -326,7 +334,7 @@ export function apply(ctx: any): void {
|
|
|
326
334
|
order: 5,
|
|
327
335
|
label: '文件编辑',
|
|
328
336
|
inject: (sessionId: string) => ({ sessionId }),
|
|
329
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
|
|
337
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })))
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
// 侧边栏三形态互斥切换:官方(DSH 0.1.5+)> better-sidebar(归档,仅旧版回退)> 中央页签。
|
|
@@ -377,7 +385,7 @@ export function apply(ctx: any): void {
|
|
|
377
385
|
if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
|
|
378
386
|
sideDisposer = ctx.effect(() => installSideEditor({
|
|
379
387
|
service: sideService as NonNullable<typeof sideService>,
|
|
380
|
-
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })),
|
|
388
|
+
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })),
|
|
381
389
|
activeSession: () => {
|
|
382
390
|
const scope = readSessionScope(ctx)
|
|
383
391
|
if (!scope.sessionId) return undefined
|
|
@@ -448,6 +456,7 @@ export function apply(ctx: any): void {
|
|
|
448
456
|
// 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
|
|
449
457
|
ctx.effect(() => () => {
|
|
450
458
|
try { disposeSnippets() } catch { /* 卸载异常不得阻断其余清理 */ }
|
|
459
|
+
try { disposeLaunchJson() } catch { /* 同上 */ }
|
|
451
460
|
try { disposeAiInline() } catch { /* 同上 */ }
|
|
452
461
|
try { disposeLsp() } catch { /* 同上 */ }
|
|
453
462
|
}, 'vscode-mode: monaco providers teardown')
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* 作者 ddj 2026-08-27
|
|
7
7
|
*/
|
|
8
8
|
import { registerLspProviders, disposeLspProviders, hideReferencesOverlay } from './providers.js'
|
|
9
|
+
import { registerDapHover, disposeDapHover } from '../../dap/hover.js'
|
|
10
|
+
import { installHoverTree, disposeHoverTree } from '../../dap/hoverTree.js'
|
|
11
|
+
import { installHoverMode, disposeHoverMode } from '../../dap/hoverMode.js'
|
|
9
12
|
import { setLspSession, refreshStatus, lspStatusFor, onLspProgress, bindLspSession } from './lspClient.js'
|
|
10
13
|
|
|
11
14
|
let monacoRef = null
|
|
@@ -19,7 +22,16 @@ const LSP_SESSION_GLOBAL = '__edrvLspSessionUnbind__'
|
|
|
19
22
|
/** Monaco 加载后装配(幂等;重复调用仅刷新会话)。 */
|
|
20
23
|
export function setupLsp(monaco) {
|
|
21
24
|
monacoRef = monaco
|
|
25
|
+
// 注册顺序决定同分 provider 的 ordinal:LanguageFeatureRegistry._compareByScoreAndTime 对
|
|
26
|
+
// 同分选择器按 _time 倒序(后注册者排前),hover 部件再按 ordinal 升序渲染 —— 因此 DAP
|
|
27
|
+
// 必须**最后**注册,暂停态的调试值行才会稳定渲染在 LSP 文档行之前;反序会让调试值被
|
|
28
|
+
// LSP 长文档挤到浮窗下方(超出 maxHeight 需滚动,等同看不到)。
|
|
22
29
|
registerLspProviders(monaco)
|
|
30
|
+
registerDapHover(monaco)
|
|
31
|
+
// hover 变量树的 DOM 绑定(观察 hover 面板插入/重渲染;幂等)
|
|
32
|
+
installHoverTree()
|
|
33
|
+
// Alt 跟踪:暂停态默认调试值浮窗、按住 Alt 切 LSP 信息(对齐 CodeBuddy)
|
|
34
|
+
installHoverMode()
|
|
23
35
|
// 会话广播订阅只装一次:跨重载时上一代已订阅(取消函数落 window,模块级状态会复位),
|
|
24
36
|
// 只判 sessionBound 会重复订阅 → 同一 LSP 会话事件被处理多次。
|
|
25
37
|
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
@@ -52,6 +64,9 @@ export function disposeLspOverlay() {
|
|
|
52
64
|
*/
|
|
53
65
|
export function disposeLsp() {
|
|
54
66
|
disposeLspProviders()
|
|
67
|
+
disposeDapHover()
|
|
68
|
+
disposeHoverTree()
|
|
69
|
+
disposeHoverMode()
|
|
55
70
|
const unbind = sessionUnbind
|
|
56
71
|
if (typeof unbind === 'function') {
|
|
57
72
|
try { unbind() } catch { /* 已解绑 */ }
|
|
@@ -136,6 +136,8 @@ export function registerLspProviders(monaco) {
|
|
|
136
136
|
}),
|
|
137
137
|
monaco.languages.registerHoverProvider(LSP_LANGS, {
|
|
138
138
|
provideHover: async (model, position, token) => {
|
|
139
|
+
// 暂停态的 LSP 行由调试 hover 层默认隐藏、按住 Alt 时显示(见 dap/hoverTree.ts),
|
|
140
|
+
// 因此这里照常返回内容,不再按位置让位。
|
|
139
141
|
const path = pathOfModel(model)
|
|
140
142
|
if (!path) return null
|
|
141
143
|
const hover = await fetchHover(path, model.getValue(), position)
|
|
@@ -137,6 +137,14 @@ const DARK_COLORS = {
|
|
|
137
137
|
'scrollbarSlider.background': '#79797966',
|
|
138
138
|
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
139
139
|
'scrollbarSlider.activeBackground': '#bfbfbf66',
|
|
140
|
+
// 差异视图色块(本仓库 vendored Monaco 无 .line-insert 静态背景规则,靠主题键保证;
|
|
141
|
+
// 色相取自研差异 UI 同源:绿 #0f9d58 / 红 #d9534f,alpha 提到肉眼可辨)
|
|
142
|
+
'diffEditor.insertedLineBackground': '#0f9d5830',
|
|
143
|
+
'diffEditor.removedLineBackground': '#d9534f2e',
|
|
144
|
+
'diffEditor.insertedTextBackground': '#0f9d5833',
|
|
145
|
+
'diffEditor.removedTextBackground': '#d9534f40',
|
|
146
|
+
'diffEditorGutter.insertedLineBackground': '#0f9d5859',
|
|
147
|
+
'diffEditorGutter.removedLineBackground': '#d9534f59',
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
const LIGHT_COLORS = {
|
|
@@ -157,6 +165,13 @@ const LIGHT_COLORS = {
|
|
|
157
165
|
'scrollbarSlider.background': '#64646466',
|
|
158
166
|
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
159
167
|
'scrollbarSlider.activeBackground': '#00000099',
|
|
168
|
+
// 差异视图色块(同上:主题键保证整行背景可见;色相/透明度与暗色一致)
|
|
169
|
+
'diffEditor.insertedLineBackground': '#0f9d5830',
|
|
170
|
+
'diffEditor.removedLineBackground': '#d9534f2e',
|
|
171
|
+
'diffEditor.insertedTextBackground': '#0f9d5833',
|
|
172
|
+
'diffEditor.removedTextBackground': '#d9534f40',
|
|
173
|
+
'diffEditorGutter.insertedLineBackground': '#0f9d5859',
|
|
174
|
+
'diffEditorGutter.removedLineBackground': '#d9534f59',
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
/**
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 「打开方式…」选择弹窗(资源管理器右键、文件目标专属)。
|
|
4
|
+
* 列出 fileOpeners 注册表中当前可用的打开器(本插件/官方侧栏/系统等),
|
|
5
|
+
* 选中即调用该打开器 open();复用 ModalShell 视觉,Esc/遮罩/取消 = 不打开。
|
|
6
|
+
* 作者 ddj 2026年09月22号
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { createRoot } from 'react-dom/client'
|
|
10
|
+
import { availableOpeners, isAvailable } from './fileOpeners.js'
|
|
11
|
+
import { ModalShell } from './ui/ModalShell.js'
|
|
12
|
+
|
|
13
|
+
/** 打开器列表行样式(列表为一次性选择器,不进 editor.css 的常驻样式表)。 */
|
|
14
|
+
const ITEM_STYLE = {
|
|
15
|
+
display: 'flex', flexDirection: 'column', gap: 2, width: '100%', textAlign: 'left',
|
|
16
|
+
padding: '8px 10px', marginBottom: 6, borderRadius: 6, cursor: 'pointer',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 打开器选择卡片(ModalShell 内容)。 */
|
|
20
|
+
function OpenWithCard({ openers, onPick, onCancel }) {
|
|
21
|
+
return React.createElement(React.Fragment, null,
|
|
22
|
+
React.createElement('h3', null, '打开方式…'),
|
|
23
|
+
openers.map((opener) => React.createElement('button', {
|
|
24
|
+
key: opener.id,
|
|
25
|
+
style: ITEM_STYLE,
|
|
26
|
+
title: opener.description || opener.label,
|
|
27
|
+
onClick: () => onPick(opener),
|
|
28
|
+
},
|
|
29
|
+
React.createElement('span', null, opener.label),
|
|
30
|
+
opener.description ? React.createElement('span', { style: { fontSize: 12, opacity: 0.7 } }, opener.description) : null)),
|
|
31
|
+
React.createElement('div', { className: 'vsm-mcp-dialog-actions' },
|
|
32
|
+
React.createElement('button', { onClick: onCancel }, '取消')))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 弹出打开器选择弹窗;选中后立即以该打开器打开目标文件。
|
|
37
|
+
* @author ddj 2026年09月22号
|
|
38
|
+
* @param registry 文件打开器注册表
|
|
39
|
+
* @param context 打开上下文(会话/工作区)
|
|
40
|
+
* @param path 目标文件(工作区相对)
|
|
41
|
+
* @param onDone 结果回调(opened = 实际使用的打开器;null = 取消/未打开),反馈文案由调用方接线
|
|
42
|
+
*/
|
|
43
|
+
export function openWithDialog(registry, context, path, onDone) {
|
|
44
|
+
const openers = availableOpeners(registry)
|
|
45
|
+
if (!openers.length) {
|
|
46
|
+
onDone?.(null)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
const host = document.createElement('div')
|
|
50
|
+
document.body.appendChild(host)
|
|
51
|
+
const root = createRoot(host)
|
|
52
|
+
const close = () => {
|
|
53
|
+
root.unmount()
|
|
54
|
+
host.remove()
|
|
55
|
+
}
|
|
56
|
+
const pick = (opener) => {
|
|
57
|
+
close()
|
|
58
|
+
if (!isAvailable(opener)) {
|
|
59
|
+
onDone?.(null)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
Promise.resolve(opener.open(path, context)).then(() => onDone?.(opener)).catch(() => onDone?.(null))
|
|
63
|
+
}
|
|
64
|
+
root.render(React.createElement(ModalShell, { onClose: () => { close(); onDone?.(null) } },
|
|
65
|
+
React.createElement(OpenWithCard, { openers, onPick: pick, onCancel: () => { close(); onDone?.(null) } })))
|
|
66
|
+
}
|
package/src/client/searchSeed.ts
CHANGED
|
@@ -62,3 +62,39 @@ export function takeSearchSeed(): string {
|
|
|
62
62
|
export function clearSearchSeed(): void {
|
|
63
63
|
pendingSeed = null
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
// --region 目录过滤种子(「在文件夹中查找…」:SearchPanel 消费后置 include 过滤)
|
|
67
|
+
|
|
68
|
+
/** 待消费的目录过滤种子('' = 无)。 */
|
|
69
|
+
let pendingScope: string | null = null
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 写入待消费的目录过滤种子(空目录等价于清除)。
|
|
73
|
+
* @author ddj 2026年09月22号
|
|
74
|
+
* @param dir 目标目录(工作区相对;空 = 根 = 不限定目录)
|
|
75
|
+
*/
|
|
76
|
+
export function setSearchScope(dir: unknown): void {
|
|
77
|
+
const text = String(dir ?? '').trim().replace(/\\/g, '/').replace(/\/+$/, '')
|
|
78
|
+
pendingScope = text ? text : null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 取走待消费的目录过滤种子(取后即清空,与 query 种子同一一次性消费语义)。
|
|
83
|
+
* @author ddj 2026年09月22号
|
|
84
|
+
* @returns 目录相对路径;无待消费种子返回空串
|
|
85
|
+
*/
|
|
86
|
+
export function takeSearchScope(): string {
|
|
87
|
+
const scope = pendingScope
|
|
88
|
+
pendingScope = null
|
|
89
|
+
return scope ?? ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 清空待消费的目录过滤种子(测试隔离用)。
|
|
94
|
+
* @author ddj 2026年09月22号
|
|
95
|
+
*/
|
|
96
|
+
export function clearSearchScope(): void {
|
|
97
|
+
pendingScope = null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --endregion
|
|
@@ -18,14 +18,17 @@ export interface TreeMenuTarget {
|
|
|
18
18
|
/** 一条右键菜单项定义。 */
|
|
19
19
|
export interface TreeMenuItem {
|
|
20
20
|
id: string
|
|
21
|
-
|
|
21
|
+
/** 展示文案;目标相关时可传函数(buildTreeMenu 解析为字符串)。 */
|
|
22
|
+
label: string | ((target: TreeMenuTarget, ctx: SidebarCtx) => string)
|
|
22
23
|
/** 排序:数值越小越靠前(缺省 100)。 */
|
|
23
24
|
order?: number
|
|
25
|
+
/** 分组:同组条目相邻成块,组切换处由 buildTreeMenu 动态置前置分隔线(缺省不参与分组)。 */
|
|
26
|
+
group?: number
|
|
24
27
|
/** 红色警示样式(如删除类操作)。 */
|
|
25
28
|
danger?: boolean
|
|
26
|
-
/**
|
|
27
|
-
disabled?: boolean
|
|
28
|
-
/**
|
|
29
|
+
/** 置灰不可点(如无权限/目标不支持时):布尔或按目标动态求值。 */
|
|
30
|
+
disabled?: boolean | ((target: TreeMenuTarget, ctx: SidebarCtx) => boolean)
|
|
31
|
+
/** 前置分隔线(不带 group 的存量/第三方条目沿用静态语义)。 */
|
|
29
32
|
separator?: boolean
|
|
30
33
|
/** 显隐守卫:返回 false 则不显示。 */
|
|
31
34
|
visible?: (target: TreeMenuTarget, ctx: SidebarCtx) => boolean
|
|
@@ -81,19 +84,57 @@ export function createTreeMenuRegistry(): TreeMenuRegistry {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
87
|
+
* 按目标解析动态字段并计算前置分隔线(纯函数)。
|
|
88
|
+
*
|
|
89
|
+
* 分隔线口径:
|
|
90
|
+
* - 带 group 的条目:与前一条可见条目组号不同即出线(首条除外)——组首条被 visible
|
|
91
|
+
* 隐藏时下一条自动顶上分组头,不丢线也不重复出线;
|
|
92
|
+
* - 不带 group 的存量/第三方条目:沿用其静态 separator;
|
|
93
|
+
* - 统一收尾:相邻分隔线合并为一条(动态组头与静态分隔线相邻时不叠双线)。
|
|
94
|
+
* @author ddj 2026年09月22号
|
|
95
|
+
* @param items 可见条目(已按 visible 过滤、按 order 排序)
|
|
96
|
+
* @param target 右键目标(动态 label/disabled 求值用)
|
|
97
|
+
* @param ctx 面板共享上下文
|
|
98
|
+
* @returns 解析后的条目(label/disabled 已定值、separator 已定值)
|
|
99
|
+
*/
|
|
100
|
+
export function markMenuSeparators(
|
|
101
|
+
items: readonly TreeMenuItem[],
|
|
102
|
+
target: TreeMenuTarget,
|
|
103
|
+
ctx: SidebarCtx,
|
|
104
|
+
): TreeMenuItem[] {
|
|
105
|
+
const out: TreeMenuItem[] = []
|
|
106
|
+
let prevGroup: number | null = null
|
|
107
|
+
let sepEmitted = false
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
const grouped = typeof item.group === 'number'
|
|
110
|
+
let sep = grouped ? (out.length > 0 && item.group !== prevGroup) : (item.separator === true && out.length > 0)
|
|
111
|
+
if (grouped) prevGroup = item.group as number
|
|
112
|
+
if (sep && sepEmitted) sep = false
|
|
113
|
+
sepEmitted = sep
|
|
114
|
+
out.push({
|
|
115
|
+
...item,
|
|
116
|
+
label: typeof item.label === 'function' ? item.label(target, ctx) : item.label,
|
|
117
|
+
disabled: typeof item.disabled === 'function' ? item.disabled(target, ctx) === true : item.disabled === true,
|
|
118
|
+
separator: sep,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 按目标构建当前可见菜单项(visible 过滤 + order 排序 + 动态字段解析 + 分组分隔线)。
|
|
126
|
+
* @author ddj 2026年08月27号 / 2026年09月22号
|
|
86
127
|
* @param registry 菜单项注册表
|
|
87
128
|
* @param target 右键目标
|
|
88
129
|
* @param ctx 面板共享上下文
|
|
89
|
-
* @returns
|
|
130
|
+
* @returns 可见菜单项(已排序、已解析)
|
|
90
131
|
*/
|
|
91
132
|
export function buildTreeMenu(registry: TreeMenuRegistry | undefined, target: TreeMenuTarget, ctx: SidebarCtx): TreeMenuItem[] {
|
|
92
133
|
if (!registry) return []
|
|
93
|
-
const
|
|
134
|
+
const visible: TreeMenuItem[] = []
|
|
94
135
|
for (const item of registry.list()) {
|
|
95
136
|
if (typeof item.visible === 'function' && !item.visible(target, ctx)) continue
|
|
96
|
-
|
|
137
|
+
visible.push(item)
|
|
97
138
|
}
|
|
98
|
-
return
|
|
139
|
+
return markMenuSeparators(visible, target, ctx)
|
|
99
140
|
}
|