dsh-vscode-mode 0.5.2 → 0.6.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 +78 -5
- package/lib/client.js +5579 -236
- package/lib/client.js.map +1 -1
- package/lib/index.js +2358 -126
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +1 -1
- 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/editorLimit.ts +31 -0
- package/src/client/index.ts +13 -0
- package/src/client/markdownPreview.ts +24 -0
- package/src/client/md/componentType.ts +28 -0
- package/src/client/md/mdPanel.ts +98 -0
- 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/searchSeed.ts +64 -0
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SearchPanel.ts +70 -15
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +186 -1
- 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 +54 -0
- package/src/client/ui/EditorView.ts +711 -29
- package/src/client/ui/McpSettings.ts +29 -0
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- 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 +54 -0
- package/src/client/ui/horizontalWheel.ts +85 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/client-primitives.d.ts +34 -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/fileOpenSettings.ts +3 -0
- package/src/index.ts +7 -3
- package/src/lsp/providers.ts +3 -2
- package/src/reveal.ts +31 -20
- package/src/rpc.ts +11 -3
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/editorLimit.ts +46 -0
- package/src/shared/keybindings.ts +10 -0
- package/src/shared/rpc.ts +40 -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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { hasEditorModel, hasOpenTabs } from '../editorModelState.js'
|
|
10
10
|
import { svnCurrentStatus } from '../svnStatus.js'
|
|
11
|
+
import { dapStore } from '../dap/store.js'
|
|
11
12
|
import { svnActionOn, svnActionsFor } from '../../shared/svnActions.js'
|
|
12
13
|
import type { SvnActionDef } from '../../shared/svnActions.js'
|
|
13
14
|
|
|
@@ -160,6 +161,8 @@ function svnEventOf(action: SvnActionDef): string {
|
|
|
160
161
|
add: 'svnAdd',
|
|
161
162
|
revert: 'svnRevertCli',
|
|
162
163
|
log: 'svnLog',
|
|
164
|
+
'create-patch': 'svnCreatePatch',
|
|
165
|
+
'diff-summarize': 'svnDiffSum',
|
|
163
166
|
cleanup: 'svnCleanup',
|
|
164
167
|
'tortoise-commit': 'svnTortoiseCommit',
|
|
165
168
|
'tortoise-log': 'svnTortoiseLog',
|
|
@@ -197,6 +200,11 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
197
200
|
keybinding: 'Ctrl+Shift+F', available: alwaysAvailable,
|
|
198
201
|
run: () => emit('searchInFiles'),
|
|
199
202
|
},
|
|
203
|
+
{
|
|
204
|
+
id: 'edrv.toggleMarkdownPreview', label: '切换 Markdown 预览', category: '视图', order: 25,
|
|
205
|
+
keybinding: 'Ctrl+Shift+V', available: alwaysAvailable,
|
|
206
|
+
run: () => emit('toggleMarkdownPreview'),
|
|
207
|
+
},
|
|
200
208
|
{
|
|
201
209
|
id: 'edrv.showLogs', label: '查看诊断日志', category: '视图', order: 30,
|
|
202
210
|
available: alwaysAvailable,
|
|
@@ -256,6 +264,51 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
256
264
|
...svnPaletteDefs(),
|
|
257
265
|
]
|
|
258
266
|
|
|
267
|
+
/**
|
|
268
|
+
* 调试(DAP)命令组:全部桥接派发,EditorView 统一接线调 dapStore。
|
|
269
|
+
* 可用性:步进类仅在暂停态可用(无编辑器时不吞键);F5 需有配置或已暂停。
|
|
270
|
+
* @author ddj 2026年09月29号
|
|
271
|
+
* @returns 命令定义数组
|
|
272
|
+
*/
|
|
273
|
+
function debugPaletteDefs(): CommandDef[] {
|
|
274
|
+
const phase = () => dapStore.getSnapshot().phase
|
|
275
|
+
const paused = () => phase() === 'paused'
|
|
276
|
+
const canStart = () => paused() || dapStore.getSnapshot().configs.length > 0
|
|
277
|
+
const canStop = () => phase() !== 'idle' && phase() !== 'terminated'
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
id: 'edrv.debugToggleBreakpoint', label: '调试:切换断点(光标行)', category: '调试', order: 10,
|
|
281
|
+
keybinding: 'F9', available: needsModel,
|
|
282
|
+
run: () => emit('debugToggleBreakpoint'),
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'edrv.debugStartContinue', label: '调试:启动 / 继续', category: '调试', order: 20,
|
|
286
|
+
keybinding: 'F5', available: canStart,
|
|
287
|
+
run: () => emit('debugStartContinue'),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'edrv.debugStepOver', label: '调试:单步跳过', category: '调试', order: 30,
|
|
291
|
+
keybinding: 'F10', available: paused,
|
|
292
|
+
run: () => emit('debugStepOver'),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'edrv.debugStepInto', label: '调试:单步步入', category: '调试', order: 40,
|
|
296
|
+
keybinding: 'F11', available: paused,
|
|
297
|
+
run: () => emit('debugStepInto'),
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: 'edrv.debugStepOut', label: '调试:单步步出', category: '调试', order: 50,
|
|
301
|
+
keybinding: 'Shift+F11', available: paused,
|
|
302
|
+
run: () => emit('debugStepOut'),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'edrv.debugStop', label: '调试:停止', category: '调试', order: 60,
|
|
306
|
+
keybinding: 'Shift+F5', available: canStop,
|
|
307
|
+
run: () => emit('debugStop'),
|
|
308
|
+
},
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
|
|
259
312
|
/**
|
|
260
313
|
* 桥接派发指令(无原生监听,键位由 commandBridge 统一 capture 处理)。
|
|
261
314
|
* 新增「只填目录、不写监听」的编辑器指令一律放这里:键位、命令栏、设置页自动可用。
|
|
@@ -266,6 +319,7 @@ export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
|
266
319
|
prevEditorRowDef(),
|
|
267
320
|
addSelectionRefDef(),
|
|
268
321
|
closeTabDef(),
|
|
322
|
+
...debugPaletteDefs(),
|
|
269
323
|
]
|
|
270
324
|
|
|
271
325
|
/**
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 溢出容器的滚轮横向滚动(页签栏用)。
|
|
3
|
+
*
|
|
4
|
+
* 需求 3:页签栏出现水平滚动条时,用鼠标滚轮(垂直滚动轮)也能横向滚动。
|
|
5
|
+
* 浏览器对 `overflow-x: auto` 的容器**不会**把垂直滚轮折算成横向滚动(只有原生横向
|
|
6
|
+
* 滚轮/触控板横滑或 Shift+滚轮才横滚),故需要显式接管。
|
|
7
|
+
*
|
|
8
|
+
* 设计取舍:
|
|
9
|
+
* - **不溢出就不接管**:内容放得下时滚轮必须保持页面垂直滚动语义,绝不能 preventDefault。
|
|
10
|
+
* - **Shift+滚轮不接管**:Chrome/Edge 对横向溢出容器原生支持 Shift+滚轮横滚,
|
|
11
|
+
* 我们再次处理会导致位移翻倍。
|
|
12
|
+
* - 触控板的横向手势(deltaX)与纵向手势(deltaY)都按主分量取用。
|
|
13
|
+
*
|
|
14
|
+
* `wheelScrollStep` 为纯函数(node 可测);`attachHorizontalWheel` 只做 DOM 装配。
|
|
15
|
+
* 作者 ddj 2026年09月18号
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 行模式(deltaMode === 1)下一「行」折算的像素数(主流浏览器约 16px 行高)。 */
|
|
19
|
+
const LINE_HEIGHT_PX = 16
|
|
20
|
+
|
|
21
|
+
/** 滚轮输入(DOM WheelEvent 的相关字段子集,便于单测构造)。 */
|
|
22
|
+
export interface WheelInput {
|
|
23
|
+
deltaX: number
|
|
24
|
+
deltaY: number
|
|
25
|
+
/** 0 = 像素 / 1 = 行 / 2 = 页。 */
|
|
26
|
+
deltaMode: number
|
|
27
|
+
shiftKey: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 容器几何(只需可滚宽度与可视宽度)。 */
|
|
31
|
+
export interface WheelGeometry {
|
|
32
|
+
scrollWidth: number
|
|
33
|
+
clientWidth: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 计算一次滚轮事件应产生的横向位移。
|
|
38
|
+
* @author ddj 2026年09月18号
|
|
39
|
+
* @param input 滚轮事件字段
|
|
40
|
+
* @param geometry 容器几何(未溢出时返回 0,即不接管)
|
|
41
|
+
* @returns 横向位移(px,带符号);0 表示本事件不处理(调用方不得 preventDefault)
|
|
42
|
+
*/
|
|
43
|
+
export function wheelScrollStep(input: WheelInput, geometry: WheelGeometry): number {
|
|
44
|
+
const scrollable = Number(geometry?.scrollWidth) - Number(geometry?.clientWidth)
|
|
45
|
+
if (!Number.isFinite(scrollable) || scrollable <= 0) return 0
|
|
46
|
+
// Shift+滚轮交给浏览器原生横滚(处理两次会翻倍)
|
|
47
|
+
if (input?.shiftKey === true) return 0
|
|
48
|
+
const dx = Number(input?.deltaX) || 0
|
|
49
|
+
const dy = Number(input?.deltaY) || 0
|
|
50
|
+
const raw = Math.abs(dx) >= Math.abs(dy) ? dx : dy
|
|
51
|
+
if (!raw) return 0
|
|
52
|
+
const mode = input?.deltaMode | 0
|
|
53
|
+
if (mode === 1) return Math.round(raw * LINE_HEIGHT_PX)
|
|
54
|
+
if (mode === 2) return Math.round(raw * Number(geometry.clientWidth || 0))
|
|
55
|
+
return Math.round(raw)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 可挂载滚轮的节点最小形状(浏览器元素或测试替身)。 */
|
|
59
|
+
export interface WheelTarget {
|
|
60
|
+
addEventListener: (type: string, listener: (event: unknown) => void, options?: unknown) => void
|
|
61
|
+
removeEventListener: (type: string, listener: (event: unknown) => void, options?: unknown) => void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 给横向溢出容器挂上「垂直滚轮驱动横向滚动」。
|
|
66
|
+
* 监听器为 `passive: false`(需要 preventDefault 阻止页面滚动),仅在需要位移时阻止默认行为。
|
|
67
|
+
* @author ddj 2026年09月18号
|
|
68
|
+
* @param node 目标元素(空则返回空注销器,不做任何事)
|
|
69
|
+
* @returns 注销函数(幂等调用安全)
|
|
70
|
+
*/
|
|
71
|
+
export function attachHorizontalWheel(node: WheelTarget | null | undefined): () => void {
|
|
72
|
+
if (!node || typeof node.addEventListener !== 'function') return () => {}
|
|
73
|
+
const onWheel = (event: unknown): void => {
|
|
74
|
+
const e = event as WheelInput & { preventDefault?: () => void }
|
|
75
|
+
const step = wheelScrollStep(
|
|
76
|
+
{ deltaX: e.deltaX, deltaY: e.deltaY, deltaMode: e.deltaMode, shiftKey: e.shiftKey },
|
|
77
|
+
{ scrollWidth: (node as unknown as { scrollWidth: number }).scrollWidth, clientWidth: (node as unknown as { clientWidth: number }).clientWidth },
|
|
78
|
+
)
|
|
79
|
+
if (!step) return
|
|
80
|
+
e.preventDefault?.()
|
|
81
|
+
;(node as unknown as { scrollLeft: number }).scrollLeft += step
|
|
82
|
+
}
|
|
83
|
+
node.addEventListener('wheel', onWheel, { passive: false })
|
|
84
|
+
return () => node.removeEventListener('wheel', onWheel, { passive: false })
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 前端导出工具(W1-4:变更列表/日志列表 → CSV/HTML 文件下载)。
|
|
3
|
+
*
|
|
4
|
+
* 纯前端拼装:Blob + 隐式 `<a download>` 触发,不经 host、不写工作副本;
|
|
5
|
+
* CSV 前置 UTF-8 BOM 保证 Excel 直开中文不乱码;导出物只在本地,不含凭据、不外发。
|
|
6
|
+
* 下载失败(无 Blob/下载权限等)静默返回 false,由调用方决定是否提示。
|
|
7
|
+
* 作者 ddj 2026年09月20号
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 二维表 → CSV 文本(RFC 4180:含逗号/引号/换行的字段加引号包裹、内部引号翻倍;行尾 CRLF)。
|
|
12
|
+
* @author ddj 2026年09月20号
|
|
13
|
+
* @param rows 二维字符串表(表头行由调用方一并传入)
|
|
14
|
+
* @returns CSV 文本(前置 UTF-8 BOM)
|
|
15
|
+
*/
|
|
16
|
+
export function csvOf(rows: unknown[][]): string {
|
|
17
|
+
const esc = (cell: unknown): string => {
|
|
18
|
+
const text = String(cell ?? '')
|
|
19
|
+
return /[",\r\n]/.test(text) ? '"' + text.replaceAll('"', '""') + '"' : text
|
|
20
|
+
}
|
|
21
|
+
const body = (rows || []).map((row) => (row || []).map(esc).join(',')).join('\r\n')
|
|
22
|
+
return '\uFEFF' + body + '\r\n'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 二维表 → 简单 HTML 报表(W1-4;全部经 HTML 转义防注入,UTF-8 声明 + 内联样式)。
|
|
27
|
+
* @author ddj 2026年09月20号
|
|
28
|
+
* @param headers 表头字符串数组
|
|
29
|
+
* @param rows 二维字符串表
|
|
30
|
+
* @param caption 页面标题(可空)
|
|
31
|
+
* @returns 完整 HTML 文档文本
|
|
32
|
+
*/
|
|
33
|
+
export function htmlTableOf(headers: unknown[], rows: unknown[][], caption?: string): string {
|
|
34
|
+
const esc = (value: unknown): string => String(value ?? '')
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>')
|
|
38
|
+
.replace(/"/g, '"')
|
|
39
|
+
const head = '<tr>' + (headers || []).map((cell) => '<th>' + esc(cell) + '</th>').join('') + '</tr>'
|
|
40
|
+
const body = (rows || []).map((row) => '<tr>' + (row || []).map((cell) => '<td>' + esc(cell) + '</td>').join('') + '</tr>').join('')
|
|
41
|
+
return '<!doctype html><html><head><meta charset="utf-8"><title>' + esc(caption || '导出') + '</title>'
|
|
42
|
+
+ '<style>body{font-family:system-ui,sans-serif;font-size:13px;color:#1f2933;margin:16px}'
|
|
43
|
+
+ 'table{border-collapse:collapse}th,td{border:1px solid #d5dbdb;padding:2px 8px;text-align:left;vertical-align:top}'
|
|
44
|
+
+ 'th{background:#f0f4f4}h3{margin:0 0 10px}</style>'
|
|
45
|
+
+ '</head><body><h3>' + esc(caption || '') + '</h3><table>' + head + body + '</table></body></html>'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 文本下载(Blob + `a[download]` 一次点击;异步 revoke 免内存泄漏)。
|
|
50
|
+
* @author ddj 2026年09月20号
|
|
51
|
+
* @param filename 下载文件名(建议带扩展名)
|
|
52
|
+
* @param text 文件内容(文本直接写入,CSV 已含 BOM 由 csvOf 负责)
|
|
53
|
+
* @param mime MIME 类型(不带 charset,函数内补 utf-8)
|
|
54
|
+
* @returns 是否触发成功(false = 浏览器能力缺失,调用方可提示)
|
|
55
|
+
*/
|
|
56
|
+
export function downloadText(filename: string, text: string, mime: string): boolean {
|
|
57
|
+
try {
|
|
58
|
+
const blob = new Blob([text], { type: mime + ';charset=utf-8' })
|
|
59
|
+
const url = URL.createObjectURL(blob)
|
|
60
|
+
const link = document.createElement('a')
|
|
61
|
+
link.href = url
|
|
62
|
+
link.download = filename
|
|
63
|
+
document.body.appendChild(link)
|
|
64
|
+
link.click()
|
|
65
|
+
link.remove()
|
|
66
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
67
|
+
return true
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -34,4 +34,38 @@ declare module '@deepseek-ai/dsh-client-ui-primitives' {
|
|
|
34
34
|
export const IconListPenOutline16: (props: EdrvIconProps) => unknown
|
|
35
35
|
/** 代码图标(活动栏「大纲」)。 */
|
|
36
36
|
export const IconCodeOutline16: (props: EdrvIconProps) => unknown
|
|
37
|
+
|
|
38
|
+
/** 代码块复制按钮文案(MarkdownText 的 labels.code)。 */
|
|
39
|
+
export interface MarkdownCodeLabels {
|
|
40
|
+
/** 复制按钮空闲态文案。 */
|
|
41
|
+
copyLabel: string
|
|
42
|
+
/** 复制后确认窗口期文案。 */
|
|
43
|
+
copiedLabel: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Markdown 渲染的本地化 chrome(labels 引用须稳定,否则流式渲染缓存失效)。 */
|
|
47
|
+
export interface MarkdownLabels {
|
|
48
|
+
code: MarkdownCodeLabels
|
|
49
|
+
/** 脚注段落标题。 */
|
|
50
|
+
footnotes: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Markdown 渲染原语(GFM + KaTeX;raw HTML 与不安全协议被禁用)。
|
|
55
|
+
* `streaming: false` 走一次性完整解析;`variant: 'body'` 为文档级排版。
|
|
56
|
+
* 旧版 DSH 可能无此导出,调用方须做 typeof 守卫并降级。
|
|
57
|
+
*/
|
|
58
|
+
export const MarkdownText: (props: {
|
|
59
|
+
text: string
|
|
60
|
+
streaming?: boolean
|
|
61
|
+
labels: MarkdownLabels
|
|
62
|
+
variant?: 'body' | 'compact'
|
|
63
|
+
}) => unknown
|
|
64
|
+
|
|
65
|
+
/** Markdown 导航作用域:普通外链与本地文件链接的打开能力(不提供则保持原生/纯文本)。 */
|
|
66
|
+
export function MarkdownDelegateProvider(props: {
|
|
67
|
+
children?: unknown
|
|
68
|
+
openExternalLink?: (href: string) => void
|
|
69
|
+
openFile?: (path: string, options?: { line?: number }) => void
|
|
70
|
+
}): unknown
|
|
37
71
|
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — launch.json「添加配置」片段提取。
|
|
3
|
+
* 扫描 VSCode 扩展清单 contributes.debuggers[]:
|
|
4
|
+
* - configurationSnippets[](label/description/body)优先;缺失时 initialConfigurations[] 兜底
|
|
5
|
+
* (条目本身就是配置体,label 取 body.name → debugger.label → type);
|
|
6
|
+
* - %key% 占位符按 package.nls.json → package.nls.zh-cn.json 合并表解析(zh 覆盖 base),
|
|
7
|
+
* 解析失败剥 %…% 外壳(避免裸占位符上屏);
|
|
8
|
+
* - body 缺 type/name 时补齐(parseLaunchConfigs 要求两者,缺了插进去也不可见)。
|
|
9
|
+
* 与 discovery 各自独立缓存(片段随扩展安装变化;force 重扫,测试隔离)。
|
|
10
|
+
* 作者 ddj 2026年09月22号
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, type Dirent } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { vscodeExtensionsDirs } from '../lsp/providers.js'
|
|
15
|
+
import { dshHome } from '../paths.js'
|
|
16
|
+
import type { DapAdapterDecl, DapConfigSnippet } from '../shared/dap.js'
|
|
17
|
+
|
|
18
|
+
/** 清单 debuggers 条目(片段提取消费的子集)。 */
|
|
19
|
+
interface SnippetDebuggerEntry {
|
|
20
|
+
type?: unknown
|
|
21
|
+
label?: unknown
|
|
22
|
+
configurationSnippets?: unknown
|
|
23
|
+
initialConfigurations?: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 归一后的片段原料(nls 解析前)。 */
|
|
27
|
+
interface RawSnippetItem {
|
|
28
|
+
label?: string
|
|
29
|
+
description?: string
|
|
30
|
+
body: Record<string, unknown>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** nls 键值表(package.nls*.json 扁平结构)。 */
|
|
34
|
+
type NlsTable = Record<string, string>
|
|
35
|
+
|
|
36
|
+
/** 发现缓存(null = 未加载;按 home 隔离,测试用 force/clear 复位)。 */
|
|
37
|
+
let cache: DapConfigSnippet[] | null = null
|
|
38
|
+
let cacheHome = ''
|
|
39
|
+
|
|
40
|
+
// --region 片段发现
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 全部可添加调试配置片段(按 label 字典序稳定输出,type+label 去重)。
|
|
44
|
+
* @author ddj 2026年09月22号
|
|
45
|
+
* @param force 强制重扫(扩展安装后 / 测试隔离用)
|
|
46
|
+
* @param home DSH home(缺省真实;测试可注入)
|
|
47
|
+
* @returns 片段列表
|
|
48
|
+
*/
|
|
49
|
+
export function dapConfigSnippets(force = false, home = dshHome()): DapConfigSnippet[] {
|
|
50
|
+
if (!force && cache !== null && home === cacheHome) return cache
|
|
51
|
+
cache = scanSnippets(home)
|
|
52
|
+
cacheHome = home
|
|
53
|
+
return cache
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 清空片段缓存(扩展安装后 / 测试 afterEach 复位)。 */
|
|
57
|
+
export function clearSnippetCache(): void {
|
|
58
|
+
cache = null
|
|
59
|
+
cacheHome = ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 按适配器可用性标注片段(与调试工具条同源裁决 debuggerDecls):
|
|
64
|
+
* type 命中 decl → 透传 available/reason;未命中(清单无该 type 的可用适配器声明)
|
|
65
|
+
* → available=false +「未发现该类型适配器」。下拉据此过滤「选得进、跑不了」的条目。
|
|
66
|
+
* @author ddj 2026年09月22号
|
|
67
|
+
* @param snippets 片段列表(dapConfigSnippets 产物)
|
|
68
|
+
* @param decls 适配器声明表(debuggerDecls())
|
|
69
|
+
* @returns 带可用性标注的片段列表(新数组,不改入参)
|
|
70
|
+
*/
|
|
71
|
+
export function annotateSnippets(
|
|
72
|
+
snippets: readonly DapConfigSnippet[],
|
|
73
|
+
decls: readonly DapAdapterDecl[],
|
|
74
|
+
): DapConfigSnippet[] {
|
|
75
|
+
const byType = new Map(decls.map((decl) => [decl.type, decl]))
|
|
76
|
+
return snippets.map((snip) => {
|
|
77
|
+
const decl = byType.get(snip.type)
|
|
78
|
+
if (!decl) {
|
|
79
|
+
return { ...snip, available: false, reason: '未发现该类型适配器' }
|
|
80
|
+
}
|
|
81
|
+
const annotated: DapConfigSnippet = { ...snip, available: decl.available }
|
|
82
|
+
if (!decl.available && decl.reason) annotated.reason = decl.reason
|
|
83
|
+
return annotated
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 扫描全部扩展根收集片段(先到先得去重,输出按 label 排序)。 */
|
|
88
|
+
function scanSnippets(home: string): DapConfigSnippet[] {
|
|
89
|
+
const out: DapConfigSnippet[] = []
|
|
90
|
+
const seen = new Set<string>()
|
|
91
|
+
for (const root of vscodeExtensionsDirs(home)) {
|
|
92
|
+
for (const extDir of subDirsOf(root)) {
|
|
93
|
+
collectExt(extDir, out, seen)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out.sort((a, b) => a.label.localeCompare(b.label))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 单扩展处理:有产出原料才读 nls(多数扩展无调试片段,不白读两个小文件)。 */
|
|
100
|
+
function collectExt(extDir: string, out: DapConfigSnippet[], seen: Set<string>): void {
|
|
101
|
+
const entries = debuggersOf(extDir)
|
|
102
|
+
const plans = entries
|
|
103
|
+
.map((entry) => ({ entry, type: strOf(entry.type), items: rawItemsOf(entry) }))
|
|
104
|
+
.filter((plan) => Boolean(plan.type) && plan.items.length > 0)
|
|
105
|
+
if (!plans.length) return
|
|
106
|
+
const nls = readNlsTable(extDir)
|
|
107
|
+
for (const plan of plans) {
|
|
108
|
+
for (const item of plan.items) {
|
|
109
|
+
const snippet = projectSnippet(plan.entry, plan.type, item, nls)
|
|
110
|
+
const key = plan.type + '\u0000' + snippet.label
|
|
111
|
+
if (seen.has(key)) continue
|
|
112
|
+
seen.add(key)
|
|
113
|
+
out.push(snippet)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 清单条目 → 片段投影(nls 解析 + type/name 补齐)。 */
|
|
119
|
+
function projectSnippet(
|
|
120
|
+
entry: SnippetDebuggerEntry,
|
|
121
|
+
type: string,
|
|
122
|
+
item: RawSnippetItem,
|
|
123
|
+
nls: NlsTable,
|
|
124
|
+
): DapConfigSnippet {
|
|
125
|
+
const body = resolveNlsDeep(item.body, nls) as Record<string, unknown>
|
|
126
|
+
if (!strOf(body.type)) body.type = type
|
|
127
|
+
const label = resolveNlsText(item.label ?? '', nls)
|
|
128
|
+
|| strOf(body.name)
|
|
129
|
+
|| resolveNlsText(strOf(entry.label), nls)
|
|
130
|
+
|| type
|
|
131
|
+
if (!strOf(body.name)) body.name = label
|
|
132
|
+
const snippet: DapConfigSnippet = {
|
|
133
|
+
label,
|
|
134
|
+
type: strOf(body.type) || type,
|
|
135
|
+
bodyText: JSON.stringify(body, null, 2),
|
|
136
|
+
}
|
|
137
|
+
const description = resolveNlsText(item.description ?? '', nls)
|
|
138
|
+
if (description) snippet.description = description
|
|
139
|
+
return snippet
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --endregion
|
|
143
|
+
|
|
144
|
+
// --region 清单与原料归一
|
|
145
|
+
|
|
146
|
+
/** 读扩展根下的扩展子目录(根不存在返回空;与 discovery 同口径)。 */
|
|
147
|
+
function subDirsOf(root: string): string[] {
|
|
148
|
+
let entries: Dirent[]
|
|
149
|
+
try {
|
|
150
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
151
|
+
} catch {
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 读某扩展的 contributes.debuggers(无清单/无声明/解析失败返回空)。 */
|
|
158
|
+
function debuggersOf(extDir: string): SnippetDebuggerEntry[] {
|
|
159
|
+
try {
|
|
160
|
+
const text = readFileSync(join(extDir, 'package.json'), 'utf8').replace(/^/, '')
|
|
161
|
+
const manifest = JSON.parse(text) as { contributes?: { debuggers?: unknown } }
|
|
162
|
+
const list = manifest.contributes?.debuggers
|
|
163
|
+
return Array.isArray(list) ? (list as SnippetDebuggerEntry[]) : []
|
|
164
|
+
} catch {
|
|
165
|
+
return []
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 条目 → 原料列表:configurationSnippets 优先,initialConfigurations 兜底(条目即配置体)。 */
|
|
170
|
+
function rawItemsOf(entry: SnippetDebuggerEntry): RawSnippetItem[] {
|
|
171
|
+
const out: RawSnippetItem[] = []
|
|
172
|
+
const snippets = asArray(entry.configurationSnippets)
|
|
173
|
+
for (const raw of snippets) {
|
|
174
|
+
const snippet = asRecord(raw)
|
|
175
|
+
if (!snippet) continue
|
|
176
|
+
const body = asRecord(snippet.body)
|
|
177
|
+
if (!body) continue
|
|
178
|
+
out.push({ label: strOf(snippet.label), description: strOf(snippet.description), body })
|
|
179
|
+
}
|
|
180
|
+
if (out.length) return out
|
|
181
|
+
for (const raw of asArray(entry.initialConfigurations)) {
|
|
182
|
+
const body = asRecord(raw)
|
|
183
|
+
if (body) out.push({ body })
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --endregion
|
|
189
|
+
|
|
190
|
+
// --region nls 占位符解析
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 读扩展 nls 表(base 先入,zh-cn 后入覆盖;文件缺失/解析失败按空跳过)。
|
|
194
|
+
* @author ddj 2026年09月22号
|
|
195
|
+
* @param extDir 扩展根目录
|
|
196
|
+
* @returns %key% → 文本 表
|
|
197
|
+
*/
|
|
198
|
+
function readNlsTable(extDir: string): NlsTable {
|
|
199
|
+
const table: NlsTable = {}
|
|
200
|
+
for (const file of ['package.nls.json', 'package.nls.zh-cn.json']) {
|
|
201
|
+
try {
|
|
202
|
+
const path = join(extDir, file)
|
|
203
|
+
if (!existsSync(path)) continue
|
|
204
|
+
const data = JSON.parse(readFileSync(path, 'utf8').replace(/^/, '')) as Record<string, unknown>
|
|
205
|
+
for (const [key, value] of Object.entries(data)) {
|
|
206
|
+
if (typeof value === 'string') table[key] = value
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
/* 单个 nls 文件损坏按无表处理,不阻断提取 */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return table
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 解析单串 %key% 占位(最多 3 轮:nls 值可再含占位;未命中剥 %…% 外壳)。
|
|
217
|
+
* @author ddj 2026年09月22号
|
|
218
|
+
* @param value 原文
|
|
219
|
+
* @param table nls 表
|
|
220
|
+
* @returns 解析后文本(无占位原样返回)
|
|
221
|
+
*/
|
|
222
|
+
function resolveNlsText(value: string, table: NlsTable): string {
|
|
223
|
+
if (!value.includes('%')) return value
|
|
224
|
+
let out = value
|
|
225
|
+
for (let round = 0; round < 3; round += 1) {
|
|
226
|
+
const next = out.replace(/%([A-Za-z0-9_.-]+)%/g, (_match, key: string) => table[key] ?? key)
|
|
227
|
+
if (next === out) break
|
|
228
|
+
out = next
|
|
229
|
+
}
|
|
230
|
+
return out
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 深度解析任意值内的 %key%(字符串逐个过 resolveNlsText,对象/数组递归)。 */
|
|
234
|
+
function resolveNlsDeep(value: unknown, table: NlsTable): unknown {
|
|
235
|
+
if (typeof value === 'string') return resolveNlsText(value, table)
|
|
236
|
+
if (Array.isArray(value)) return value.map((item) => resolveNlsDeep(item, table))
|
|
237
|
+
if (value && typeof value === 'object') {
|
|
238
|
+
const out: Record<string, unknown> = {}
|
|
239
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
240
|
+
out[key] = resolveNlsDeep(item, table)
|
|
241
|
+
}
|
|
242
|
+
return out
|
|
243
|
+
}
|
|
244
|
+
return value
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --endregion
|
|
248
|
+
|
|
249
|
+
// --region 小工具
|
|
250
|
+
|
|
251
|
+
/** unknown → 非空字符串(其余空串)。 */
|
|
252
|
+
function strOf(value: unknown): string {
|
|
253
|
+
return typeof value === 'string' && value ? value : ''
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** unknown → 普通对象(数组/原始值返回 null)。 */
|
|
257
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
258
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
259
|
+
? (value as Record<string, unknown>)
|
|
260
|
+
: null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** unknown → 数组(单对象包一层,兼容 initialConfigurations 单对象写法)。 */
|
|
264
|
+
function asArray(value: unknown): unknown[] {
|
|
265
|
+
if (Array.isArray(value)) return value
|
|
266
|
+
return value && typeof value === 'object' ? [value] : []
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --endregion
|