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
|
@@ -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
|
|
|
@@ -79,6 +80,19 @@ function prevEditorRowDef(): CommandDef {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* 转到行(转发 Monaco 原生 gotoLine widget;补命令栏与快捷键设置页入口)。
|
|
85
|
+
* @author ddj 2026年09月22号
|
|
86
|
+
* @returns 命令定义
|
|
87
|
+
*/
|
|
88
|
+
function gotoLineDef(): CommandDef {
|
|
89
|
+
return {
|
|
90
|
+
id: 'edrv.goToLine', label: '转到行', category: '导航', order: 45,
|
|
91
|
+
keybinding: 'Ctrl+G', available: needsModel,
|
|
92
|
+
run: () => emit('goToLine'),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
/**
|
|
83
97
|
* 添加选中内容为引用(把当前选区追加进对话输入框;无选区则状态栏提示)。
|
|
84
98
|
* 有活动编辑器模型才可用,保证对话框内按 Ctrl+U 不被本命令吞掉。
|
|
@@ -160,6 +174,8 @@ function svnEventOf(action: SvnActionDef): string {
|
|
|
160
174
|
add: 'svnAdd',
|
|
161
175
|
revert: 'svnRevertCli',
|
|
162
176
|
log: 'svnLog',
|
|
177
|
+
'create-patch': 'svnCreatePatch',
|
|
178
|
+
'diff-summarize': 'svnDiffSum',
|
|
163
179
|
cleanup: 'svnCleanup',
|
|
164
180
|
'tortoise-commit': 'svnTortoiseCommit',
|
|
165
181
|
'tortoise-log': 'svnTortoiseLog',
|
|
@@ -261,6 +277,51 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
261
277
|
...svnPaletteDefs(),
|
|
262
278
|
]
|
|
263
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 调试(DAP)命令组:全部桥接派发,EditorView 统一接线调 dapStore。
|
|
282
|
+
* 可用性:步进类仅在暂停态可用(无编辑器时不吞键);F5 需有配置或已暂停。
|
|
283
|
+
* @author ddj 2026年09月29号
|
|
284
|
+
* @returns 命令定义数组
|
|
285
|
+
*/
|
|
286
|
+
function debugPaletteDefs(): CommandDef[] {
|
|
287
|
+
const phase = () => dapStore.getSnapshot().phase
|
|
288
|
+
const paused = () => phase() === 'paused'
|
|
289
|
+
const canStart = () => paused() || dapStore.getSnapshot().configs.length > 0
|
|
290
|
+
const canStop = () => phase() !== 'idle' && phase() !== 'terminated'
|
|
291
|
+
return [
|
|
292
|
+
{
|
|
293
|
+
id: 'edrv.debugToggleBreakpoint', label: '调试:切换断点(光标行)', category: '调试', order: 10,
|
|
294
|
+
keybinding: 'F9', available: needsModel,
|
|
295
|
+
run: () => emit('debugToggleBreakpoint'),
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'edrv.debugStartContinue', label: '调试:启动 / 继续', category: '调试', order: 20,
|
|
299
|
+
keybinding: 'F5', available: canStart,
|
|
300
|
+
run: () => emit('debugStartContinue'),
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'edrv.debugStepOver', label: '调试:单步跳过', category: '调试', order: 30,
|
|
304
|
+
keybinding: 'F10', available: paused,
|
|
305
|
+
run: () => emit('debugStepOver'),
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'edrv.debugStepInto', label: '调试:单步步入', category: '调试', order: 40,
|
|
309
|
+
keybinding: 'F11', available: paused,
|
|
310
|
+
run: () => emit('debugStepInto'),
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: 'edrv.debugStepOut', label: '调试:单步步出', category: '调试', order: 50,
|
|
314
|
+
keybinding: 'Shift+F11', available: paused,
|
|
315
|
+
run: () => emit('debugStepOut'),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'edrv.debugStop', label: '调试:停止', category: '调试', order: 60,
|
|
319
|
+
keybinding: 'Shift+F5', available: canStop,
|
|
320
|
+
run: () => emit('debugStop'),
|
|
321
|
+
},
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
|
|
264
325
|
/**
|
|
265
326
|
* 桥接派发指令(无原生监听,键位由 commandBridge 统一 capture 处理)。
|
|
266
327
|
* 新增「只填目录、不写监听」的编辑器指令一律放这里:键位、命令栏、设置页自动可用。
|
|
@@ -269,8 +330,10 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
269
330
|
export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
270
331
|
nextEditorRowDef(),
|
|
271
332
|
prevEditorRowDef(),
|
|
333
|
+
gotoLineDef(),
|
|
272
334
|
addSelectionRefDef(),
|
|
273
335
|
closeTabDef(),
|
|
336
|
+
...debugPaletteDefs(),
|
|
274
337
|
]
|
|
275
338
|
|
|
276
339
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 调试适配器发现(清单驱动,VSCode contributes.debuggers 同源语义)。
|
|
3
|
+
* 扫描全部扩展根(复用 lsp 侧 vscodeExtensionsDirs:~/.vscode/extensions、
|
|
4
|
+
* ~/.vscode-server/extensions、插件扩展目录),读各扩展 package.json 的
|
|
5
|
+
* contributes.debuggers[] 声明,解析为 DapAdapterDecl:
|
|
6
|
+
* - program 按平台取 windows/linux/osx 覆盖,相对扩展根解析;
|
|
7
|
+
* - runtime='node' → 由 node 跑 js 入口;无 runtime → program 即可执行(args 为清单声明);
|
|
8
|
+
* - 同 type 多扩展并存 → 按「清单版本降序(numeric) → 路径升序」取唯一(与 LSP 候选口径一致);
|
|
9
|
+
* - 入口不存在 / runtime 不支持 → available=false + reason(如 DotRush 按需下载的 clrdbg,
|
|
10
|
+
* 诚实标注不假成功,调用方据此在下拉置灰并提示原因)。
|
|
11
|
+
* 作者 ddj 2026年09月21号
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, type Dirent } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { LSP_LANG_EXT, manifestVersionOf, vscodeExtensionsDirs } from '../lsp/providers.js'
|
|
16
|
+
import { dshHome } from '../paths.js'
|
|
17
|
+
import type { DapAdapterDecl } from '../shared/dap.js'
|
|
18
|
+
|
|
19
|
+
/** package.json contributes.debuggers 条目(本插件消费的子集)。 */
|
|
20
|
+
interface DebuggerEntry {
|
|
21
|
+
type?: unknown
|
|
22
|
+
label?: unknown
|
|
23
|
+
program?: unknown
|
|
24
|
+
args?: unknown
|
|
25
|
+
runtime?: unknown
|
|
26
|
+
languages?: unknown
|
|
27
|
+
windows?: { program?: unknown }
|
|
28
|
+
linux?: { program?: unknown }
|
|
29
|
+
osx?: { program?: unknown }
|
|
30
|
+
darwin?: { program?: unknown }
|
|
31
|
+
configurationAttributes?: {
|
|
32
|
+
attach?: { properties?: { ext?: { default?: unknown } } }
|
|
33
|
+
launch?: { properties?: { ext?: { default?: unknown } } }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 发现缓存(扩展清单会话内基本不变;测试用 force 重扫)。 */
|
|
38
|
+
let cacheReady = false
|
|
39
|
+
let cacheHome = ''
|
|
40
|
+
let cache: DapAdapterDecl[] = []
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 发现全部调试适配器声明(按 type 去重、type 字典序稳定输出)。
|
|
44
|
+
* @author ddj 2026年09月21号
|
|
45
|
+
* @param force 强制重扫(扩展安装后 / 测试隔离用)
|
|
46
|
+
* @param home DSH home(缺省真实;测试可注入)
|
|
47
|
+
*/
|
|
48
|
+
export function debuggerDecls(force = false, home = dshHome()): DapAdapterDecl[] {
|
|
49
|
+
if (cacheReady && !force && home === cacheHome) return cache
|
|
50
|
+
cache = scanDecls(home)
|
|
51
|
+
cacheHome = home
|
|
52
|
+
cacheReady = true
|
|
53
|
+
return cache
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 清空发现缓存(扩展目录变更时;测试 afterEach 复位)。 */
|
|
57
|
+
export function clearDiscoveryCache(): void {
|
|
58
|
+
cacheReady = false
|
|
59
|
+
cacheHome = ''
|
|
60
|
+
cache = []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 扫描全部扩展根并收集声明(同 type 去重后返回)。 */
|
|
64
|
+
function scanDecls(home: string): DapAdapterDecl[] {
|
|
65
|
+
const hits: { decl: DapAdapterDecl; version: string }[] = []
|
|
66
|
+
for (const root of vscodeExtensionsDirs(home)) {
|
|
67
|
+
for (const extDir of subDirsOf(root)) {
|
|
68
|
+
for (const entry of debuggersOf(extDir)) {
|
|
69
|
+
const decl = declOf(extDir, entry)
|
|
70
|
+
if (decl) hits.push({ decl, version: manifestVersionOf(extDir) ?? '' })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return pickPerType(hits)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 同 type 去重:清单版本降序(numeric)→ 路径升序;输出按 type 字典序。 */
|
|
78
|
+
function pickPerType(hits: { decl: DapAdapterDecl; version: string }[]): DapAdapterDecl[] {
|
|
79
|
+
const byType = new Map<string, { decl: DapAdapterDecl; version: string }>()
|
|
80
|
+
for (const hit of hits) {
|
|
81
|
+
const prev = byType.get(hit.decl.type)
|
|
82
|
+
if (!betterOf(hit, prev)) continue
|
|
83
|
+
byType.set(hit.decl.type, hit)
|
|
84
|
+
}
|
|
85
|
+
return [...byType.values()].map((h) => h.decl).sort((a, b) => a.type.localeCompare(b.type))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 候选比较:版本降序优先,平版本路径升序(无前任直接胜出)。 */
|
|
89
|
+
function betterOf(hit: { decl: DapAdapterDecl; version: string }, prev?: { decl: DapAdapterDecl; version: string }): boolean {
|
|
90
|
+
if (!prev) return true
|
|
91
|
+
const byVersion = compareVersion(hit.version, prev.version)
|
|
92
|
+
if (byVersion !== 0) return byVersion > 0
|
|
93
|
+
return hit.decl.extensionPath.localeCompare(prev.decl.extensionPath) < 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 清单版本比较(numeric 感知,与 LSP 候选排序同口径):>0 表示 a 更新。 */
|
|
97
|
+
function compareVersion(a: string, b: string): number {
|
|
98
|
+
return String(a || '').localeCompare(String(b || ''), undefined, { numeric: true })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 枚举扩展根下的扩展子目录(根不存在返回空)。 */
|
|
102
|
+
function subDirsOf(root: string): string[] {
|
|
103
|
+
let entries: Dirent[]
|
|
104
|
+
try {
|
|
105
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
106
|
+
} catch {
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 读某扩展的 contributes.debuggers 声明(无清单/无声明/解析失败返回空)。 */
|
|
113
|
+
function debuggersOf(extDir: string): DebuggerEntry[] {
|
|
114
|
+
try {
|
|
115
|
+
const text = readFileSync(join(extDir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
|
|
116
|
+
const manifest = JSON.parse(text) as { contributes?: { debuggers?: unknown } }
|
|
117
|
+
const list = manifest.contributes?.debuggers
|
|
118
|
+
return Array.isArray(list) ? (list as DebuggerEntry[]) : []
|
|
119
|
+
} catch {
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 清单条目 → 声明(缺 type/program 视为无效条目跳过)。 */
|
|
125
|
+
function declOf(extDir: string, entry: DebuggerEntry): DapAdapterDecl | null {
|
|
126
|
+
const type = typeof entry.type === 'string' ? entry.type : ''
|
|
127
|
+
if (!type || typeof entry.program !== 'string' || !entry.program) return null
|
|
128
|
+
const languages = stringsOf(entry.languages)
|
|
129
|
+
const resolved = resolveProgram(extDir, entry)
|
|
130
|
+
const runtime = typeof entry.runtime === 'string' && entry.runtime ? entry.runtime : undefined
|
|
131
|
+
const runtimeBad = runtime !== undefined && runtime !== 'node'
|
|
132
|
+
const available = resolved.available && !runtimeBad
|
|
133
|
+
const reason = !resolved.available
|
|
134
|
+
? resolved.reason
|
|
135
|
+
: runtimeBad
|
|
136
|
+
? '暂不支持运行时 "' + runtime + '"(仅支持 node 与原生可执行)'
|
|
137
|
+
: undefined
|
|
138
|
+
return {
|
|
139
|
+
type,
|
|
140
|
+
label: typeof entry.label === 'string' && entry.label ? entry.label : type,
|
|
141
|
+
extensionId: extensionIdOf(extDir),
|
|
142
|
+
extensionPath: extDir,
|
|
143
|
+
program: resolved.program,
|
|
144
|
+
args: stringsOf(entry.args),
|
|
145
|
+
runtime,
|
|
146
|
+
languages,
|
|
147
|
+
exts: extsOf(entry, languages),
|
|
148
|
+
available,
|
|
149
|
+
reason,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 平台 program 解析:windows/linux/osx(darwin) 覆盖优先,相对扩展根(剥 ./)。 */
|
|
154
|
+
function resolveProgram(extDir: string, entry: DebuggerEntry): { program: string; available: boolean; reason?: string } {
|
|
155
|
+
const plat = process.platform
|
|
156
|
+
const override = plat === 'win32'
|
|
157
|
+
? entry.windows?.program
|
|
158
|
+
: plat === 'darwin'
|
|
159
|
+
? (entry.osx?.program ?? entry.darwin?.program)
|
|
160
|
+
: entry.linux?.program
|
|
161
|
+
const rel = typeof override === 'string' && override ? override : entry.program
|
|
162
|
+
if (typeof rel !== 'string' || !rel) return { program: '', available: false, reason: '清单未声明本平台 program' }
|
|
163
|
+
const program = join(extDir, ...rel.replace(/^\.\//, '').split(/[\\/]/))
|
|
164
|
+
if (!existsSync(program)) {
|
|
165
|
+
return { program, available: false, reason: '适配器入口不存在(扩展可能需按需下载):' + rel }
|
|
166
|
+
}
|
|
167
|
+
return { program, available: true }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 来源扩展 id(目录名 publisher.name-version 取 version 前段;解析失败回退目录名)。 */
|
|
171
|
+
function extensionIdOf(extDir: string): string {
|
|
172
|
+
const base = extDir.split(/[\\/]/).pop() ?? ''
|
|
173
|
+
const dot = base.lastIndexOf('-')
|
|
174
|
+
return dot > 0 ? base.slice(0, dot) : base
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 可下断点扩展名:优先清单 configurationAttributes 的 ext 缺省(emmylua 的 .lua 家族),
|
|
178
|
+
* 否则按声明 languages 映射(LSP 侧同源);两者皆空 = 不限(不过滤断点)。 */
|
|
179
|
+
function extsOf(entry: DebuggerEntry, languages: string[]): string[] {
|
|
180
|
+
const attrs = entry.configurationAttributes
|
|
181
|
+
const declared = attrs?.attach?.properties?.ext?.default ?? attrs?.launch?.properties?.ext?.default
|
|
182
|
+
const fromManifest = stringsOf(declared)
|
|
183
|
+
if (fromManifest.length) return fromManifest
|
|
184
|
+
const out: string[] = []
|
|
185
|
+
for (const lang of languages) {
|
|
186
|
+
for (const ext of LSP_LANG_EXT[lang] ?? []) {
|
|
187
|
+
const dotted = '.' + ext.replace(/^\./, '')
|
|
188
|
+
if (!out.includes(dotted)) out.push(dotted)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** unknown → 字符串数组(非字符串元素剔除)。 */
|
|
195
|
+
function stringsOf(value: unknown): string[] {
|
|
196
|
+
if (!Array.isArray(value)) return []
|
|
197
|
+
return value.filter((v): v is string => typeof v === 'string' && v.length > 0)
|
|
198
|
+
}
|