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.
Files changed (77) hide show
  1. package/README.md +72 -2
  2. package/lib/client.js +12082 -6420
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2750 -147
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/copyText.ts +28 -0
  8. package/src/client/dap/BpWidget.ts +262 -0
  9. package/src/client/dap/bpMenu.ts +91 -0
  10. package/src/client/dap/bpRowMenu.ts +53 -0
  11. package/src/client/dap/breakpoints.ts +222 -0
  12. package/src/client/dap/decorate.ts +128 -0
  13. package/src/client/dap/hintLine.ts +29 -0
  14. package/src/client/dap/hover.ts +166 -0
  15. package/src/client/dap/hoverMode.ts +159 -0
  16. package/src/client/dap/hoverTree.ts +722 -0
  17. package/src/client/dap/launchInsert.ts +229 -0
  18. package/src/client/dap/launchSnippetProvider.ts +206 -0
  19. package/src/client/dap/modelPath.ts +23 -0
  20. package/src/client/dap/panelSplit.ts +105 -0
  21. package/src/client/dap/pidPick.ts +24 -0
  22. package/src/client/dap/store.ts +571 -0
  23. package/src/client/dap/toolbarDrag.ts +51 -0
  24. package/src/client/dap/trace.ts +22 -0
  25. package/src/client/dap/variableTree.ts +62 -0
  26. package/src/client/fileClipboard.ts +46 -0
  27. package/src/client/index.ts +12 -3
  28. package/src/client/monaco/lsp/index.ts +15 -0
  29. package/src/client/monaco/lsp/providers.ts +2 -0
  30. package/src/client/monaco/theme.ts +15 -0
  31. package/src/client/openWithDialog.ts +66 -0
  32. package/src/client/searchSeed.ts +36 -0
  33. package/src/client/sidebar/contextMenu.ts +51 -10
  34. package/src/client/sidebar/menuItems.ts +473 -50
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +24 -3
  37. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  38. package/src/client/sidebar/panels/index.ts +19 -0
  39. package/src/client/sidebar/types.ts +8 -0
  40. package/src/client/styles/editor.css +176 -0
  41. package/src/client/svnActions.ts +18 -1
  42. package/src/client/svnLog.ts +3 -3
  43. package/src/client/svnStatus.ts +140 -2
  44. package/src/client/tabActions.ts +22 -10
  45. package/src/client/ui/EditorView.ts +625 -23
  46. package/src/client/ui/OfficialSideTab.ts +1 -0
  47. package/src/client/ui/PromptDialog.ts +63 -0
  48. package/src/client/ui/SideBySideDiff.tsx +214 -29
  49. package/src/client/ui/SideEditorTab.ts +1 -0
  50. package/src/client/ui/SvnDiffPanel.ts +21 -8
  51. package/src/client/ui/SvnLogDialog.ts +44 -2
  52. package/src/client/ui/SvnPatchDialog.ts +63 -0
  53. package/src/client/ui/SvnSumDialog.ts +77 -0
  54. package/src/client/ui/commandCatalog.ts +63 -0
  55. package/src/client/ui/svnExport.ts +71 -0
  56. package/src/dap/configSnippets.ts +269 -0
  57. package/src/dap/discovery.ts +198 -0
  58. package/src/dap/launchConfig.ts +127 -0
  59. package/src/dap/manager.ts +588 -0
  60. package/src/dap/processList.ts +76 -0
  61. package/src/dap/protocol.ts +80 -0
  62. package/src/dap/provider.ts +49 -0
  63. package/src/dap/rpc.ts +192 -0
  64. package/src/dap/sourcePath.ts +82 -0
  65. package/src/index.ts +7 -3
  66. package/src/lsp/providers.ts +3 -2
  67. package/src/reveal.ts +31 -20
  68. package/src/rpc.ts +209 -5
  69. package/src/search/ripgrep.ts +127 -11
  70. package/src/shared/dap.ts +262 -0
  71. package/src/shared/fsNames.ts +115 -0
  72. package/src/shared/keybindings.ts +9 -0
  73. package/src/shared/rpc.ts +52 -2
  74. package/src/shared/svn.ts +147 -0
  75. package/src/shared/svnActions.ts +20 -0
  76. package/src/svn.ts +320 -11
  77. package/src/workspace.ts +0 -75
@@ -0,0 +1,80 @@
1
+ /**
2
+ * dsh-vscode-mode host — DAP(Debug Adapter Protocol)消息纯类型与守卫。
3
+ * 帧格式与 LSP 相同(Content-Length 头),编解码直接复用 lsp/jsonrpc.ts。
4
+ * 消息形状只覆盖本插件消费的子集(DAP 3.x 常用面 + emmylua 适配器扩展)。
5
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
6
+ */
7
+ /** DAP 消息基型(请求/响应/事件的公共头)。 */
8
+ export interface DapMessage {
9
+ seq?: number
10
+ type: 'request' | 'response' | 'event'
11
+ }
12
+
13
+ /** DAP 响应(按 request_seq 关联)。 */
14
+ export interface DapResponse extends DapMessage {
15
+ type: 'response'
16
+ request_seq: number
17
+ success: boolean
18
+ command: string
19
+ message?: string
20
+ body?: unknown
21
+ }
22
+
23
+ /** DAP 事件。 */
24
+ export interface DapEvent extends DapMessage {
25
+ type: 'event'
26
+ event: string
27
+ body?: unknown
28
+ }
29
+
30
+ /** DAP 请求(客户端 → 适配器)。 */
31
+ export interface DapRequest extends DapMessage {
32
+ type: 'request'
33
+ command: string
34
+ arguments?: unknown
35
+ }
36
+
37
+ /** 客户端 → 适配器请求帧构造。 */
38
+ export function dapRequest(seq: number, command: string, args?: unknown): DapRequest {
39
+ return args === undefined
40
+ ? { seq, type: 'request', command }
41
+ : { seq, type: 'request', command, arguments: args }
42
+ }
43
+
44
+ /** 响应守卫。 */
45
+ export function isDapResponse(msg: DapMessage): msg is DapResponse {
46
+ return msg.type === 'response' && typeof (msg as Partial<DapResponse>).request_seq === 'number'
47
+ }
48
+
49
+ /** 事件守卫。 */
50
+ export function isDapEvent(msg: DapMessage): msg is DapEvent {
51
+ return msg.type === 'event' && typeof (msg as Partial<DapEvent>).event === 'string'
52
+ }
53
+
54
+ /** 事件体安全取值。 */
55
+ export function eventBody(event: DapEvent): Record<string, unknown> {
56
+ return event.body && typeof event.body === 'object' ? event.body as Record<string, unknown> : {}
57
+ }
58
+
59
+ /** 响应体安全取值。 */
60
+ export function responseBody(response: DapResponse): Record<string, unknown> {
61
+ return response.body && typeof response.body === 'object' ? response.body as Record<string, unknown> : {}
62
+ }
63
+
64
+ /**
65
+ * initialize 请求参数(适配器协商口径:path 格式、行列 1 基)。
66
+ * @author ddj 2026年09月29号
67
+ * @param adapterId 适配器 id
68
+ */
69
+ export function initializeArgs(adapterId: string): Record<string, unknown> {
70
+ return {
71
+ adapterID: adapterId,
72
+ locale: 'zh-cn',
73
+ linesStartAt1: true,
74
+ columnsStartAt1: true,
75
+ pathFormat: 'path',
76
+ supportsVariableType: true,
77
+ supportsVariablePaging: false,
78
+ supportsRunInTerminalRequest: false,
79
+ }
80
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * dsh-vscode-mode host — 调试适配器规格解析:按调试类型从扩展清单声明(discovery)
3
+ * 解析 spawn 规格,通用不限定语言:
4
+ * - runtime='node' → node 跑 js 入口(emmylua 形态,等价旧 process.execPath 硬编码);
5
+ * - 无 runtime → program 即原生可执行(clrdbg/monodbg 形态,args 为清单声明如 --interpreter=vscode);
6
+ * - 其它 runtime 明确不支持(decl.available=false 带原因,不做假启动)。
7
+ * 旧版仅探测 tangzx.emmylua 两型(attach/new),由清单驱动后自然覆盖并扩展到任意适配器。
8
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
9
+ */
10
+ import { debuggerDecls } from './discovery.js'
11
+
12
+ /** 适配器启动规格(spawn 参数)。 */
13
+ export interface DapAdapterSpec {
14
+ /** 调试类型(= initialize.adapterID)。 */
15
+ type: string
16
+ /** 可执行文件(node 型 = node 本体;直启型 = 适配器本体)。 */
17
+ command: string
18
+ /** 启动参数(node 型 = [program];直启型 = 清单声明 args)。 */
19
+ args: string[]
20
+ /** 扩展根目录(emmylua 兼容 shim 注入 extensionPath 用)。 */
21
+ extensionPath: string
22
+ /** 来源扩展 id(publisher.name;emmylua 兼容 shim 判定)。 */
23
+ extensionId: string
24
+ /** 可下断点扩展名(断点下发语言过滤;空 = 不限)。 */
25
+ exts: string[]
26
+ /** 探测描述(错误提示用)。 */
27
+ detail: string
28
+ }
29
+
30
+ /**
31
+ * 解析某调试类型的适配器启动规格。
32
+ * @author ddj 2026年09月29号 / 2026年09月21号
33
+ * @param type 调试类型(launch.json type = 清单 contributes.debuggers.type)
34
+ * @param force 强制重扫扩展清单(测试隔离用)
35
+ * @param home DSH home(缺省真实;测试可注入)
36
+ * @returns 启动规格;类型未发现 / 不可用 / runtime 不支持返回 null(原因见 discovery 声明)
37
+ */
38
+ export function resolveAdapterSpec(type: string, force = false, home?: string): DapAdapterSpec | null {
39
+ const decl = debuggerDecls(force, home).find((d) => d.type === type)
40
+ if (!decl || !decl.available) return null
41
+ const base = { type, extensionPath: decl.extensionPath, extensionId: decl.extensionId, exts: decl.exts, detail: decl.extensionPath }
42
+ if (decl.runtime === 'node') {
43
+ return { ...base, command: process.execPath, args: [decl.program, ...decl.args] }
44
+ }
45
+ if (!decl.runtime) {
46
+ return { ...base, command: decl.program, args: [...decl.args] }
47
+ }
48
+ return null
49
+ }
package/src/dap/rpc.ts ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * dsh-vscode-mode host — 调试 RPC(edrv.dap.*)装配。
3
+ * 单例 DapSession + 适配器发现(扩展清单驱动,通用不限定语言)+ 进程枚举 +
4
+ * launch.json 读取(全量透传)+ findFile 反向匹配(chunkname → ripgrep 定向查找源文件)。
5
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
6
+ */
7
+ import { join } from 'node:path'
8
+ import { mkdir, writeFile } from 'node:fs/promises'
9
+ import type { Ctx } from '../store.js'
10
+ import type { RpcHandlerMap } from '../shared/rpc.js'
11
+ import { DAP_ACTIONS, DAP_LAUNCH_REL, DAP_LEGACY_LAUNCH_REL, requestOfConfig, type DapAction, type DapDebugConfig } from '../shared/dap.js'
12
+ import { normalizeSourcePath, rankSourceCandidates } from './sourcePath.js'
13
+ import { findFilesByChunk } from '../search/ripgrep.js'
14
+ import { hasCmdPlaceholder, parseLaunchConfigs } from './launchConfig.js'
15
+ import { dapConfigSnippets, annotateSnippets } from './configSnippets.js'
16
+ import { filterProcesses, listProcesses } from './processList.js'
17
+ import { resolveAdapterSpec } from './provider.js'
18
+ import { debuggerDecls } from './discovery.js'
19
+ import { DapSession } from './manager.js'
20
+ import { debugRecord } from '../debugLog.js'
21
+ import { log } from '../log.js'
22
+
23
+ /**
24
+ * 解析工作区根为 subprocess 执行世界可见的真实路径。
25
+ * DAP 会话无 DSH Session 对象,不能让 fs.resolve('.') 落回 host 进程目录,
26
+ * 因此显式以 cwd 为基准解析 `.`(= cwd 本身)。
27
+ * @author ddj 2026年09月21号
28
+ * @param ctx DSH 上下文
29
+ * @param workspacePath 工作区绝对路径
30
+ * @returns 可交给 rg 的根路径;fs 不可用或解析失败返回空串
31
+ */
32
+ async function rootPathOf(ctx: Ctx, workspacePath: string): Promise<string> {
33
+ const fs = ctx.get('fs')
34
+ if (!fs || !workspacePath) return ''
35
+ try {
36
+ const target = await fs.resolve('.', { cwd: workspacePath })
37
+ return fs.processPath(target) || ''
38
+ } catch (error) {
39
+ return ''
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 创建调试 RPC(单例会话随插件装配创建,ctx.effect 里调 disposeDap 清理)。
45
+ * @author ddj 2026年09月29号
46
+ * @param ctx DSH 上下文
47
+ */
48
+ export function createDapRpc(ctx: Ctx): { handlers: Partial<RpcHandlerMap>; dispose: () => void } {
49
+ const session = new DapSession({
50
+ findFiles: async (workspacePath, chunk) => {
51
+ const clean = normalizeSourcePath(chunk)
52
+ if (!clean) return []
53
+ // 工作区根需转换成 subprocess 执行世界可见的真实路径,再交给 rg 定向查找
54
+ const root = await rootPathOf(ctx, workspacePath)
55
+ if (!root) {
56
+ debugRecord(ctx, workspacePath, '[DEBUG findFiles] chunk=' + clean + ' root=(无法解析) candidates=0', 'debug')
57
+ return []
58
+ }
59
+ const candidates = await findFilesByChunk(ctx, root, clean)
60
+ const ranked = rankSourceCandidates(clean, candidates, workspacePath)
61
+ log.debug('[dap-trace] findFiles chunk=' + clean + ' cwd=' + workspacePath + ' root=' + root + ' candidates=' + ranked.length + ' first=' + (ranked[0] ?? ''))
62
+ debugRecord(ctx, workspacePath, '[DEBUG findFiles] chunk=' + clean + ' root=' + root + ' candidates=' + ranked.length + ' first=' + (ranked[0] ?? ''), 'debug')
63
+ return ranked
64
+ },
65
+ trace: (workspacePath, message) => debugRecord(ctx, workspacePath, message, 'debug'),
66
+ })
67
+
68
+ const handlers: Partial<RpcHandlerMap> = {
69
+ 'edrv.dap.configs': async (args) => {
70
+ const configs = await readLaunchConfigs(ctx, args.workspacePath)
71
+ const adapters = debuggerDecls().map((d) => ({ type: d.type, label: d.label, available: d.available, reason: d.reason }))
72
+ return { ok: true, configs, adapters, source: configs.length ? 'launchjson' : 'none' }
73
+ },
74
+ 'edrv.dap.snippets': async () => ({
75
+ ok: true,
76
+ // 与工具条同源裁决:不可用适配器的模板带 reason,client 下拉过滤
77
+ snippets: annotateSnippets(dapConfigSnippets(), debuggerDecls()),
78
+ }),
79
+ 'edrv.dap.processes': async (args) => {
80
+ const items = filterProcesses(await listProcesses(), args.processName)
81
+ return { ok: true, items }
82
+ },
83
+ 'edrv.dap.start': async (args) => {
84
+ const config = args.config
85
+ if (hasCmdPlaceholder(config)) {
86
+ return { ok: false, error: '配置含 ${command:...} 占位(VS Code 命令),本插件无法解析,请在 launch.json 中改为具体值' }
87
+ }
88
+ const spec = resolveAdapterSpec(config.type)
89
+ if (!spec) return { ok: false, error: unknownTypeMessage(config.type) }
90
+ let pid = args.pid && args.pid > 0 ? args.pid : undefined
91
+ if (requestOfConfig(config) === 'attach' && !pid && !config.processId) {
92
+ const resolved = await resolvePid(config.processName)
93
+ if (typeof resolved === 'string') return { ok: false, error: resolved }
94
+ pid = resolved
95
+ }
96
+ session.start(spec, config, args.workspacePath, pid)
97
+ return { ok: true, phase: 'starting' }
98
+ },
99
+ 'edrv.dap.stop': async () => {
100
+ session.stop()
101
+ return { ok: true }
102
+ },
103
+ 'edrv.dap.poll': async (args) => ({ ok: true, ...session.poll(args.since) }),
104
+ 'edrv.dap.setBreakpoints': async (args) => {
105
+ const abs = join(args.workspacePath, args.file.replace(/\\/g, '/'))
106
+ return { ok: true, breakpoints: await session.setBreakpoints(abs, args.points) }
107
+ },
108
+ 'edrv.dap.stackTrace': async () => ({ ok: true, frames: await session.stackTrace() }),
109
+ 'edrv.dap.scopes': async (args) => ({ ok: true, scopes: await session.scopes(args.frameId) }),
110
+ 'edrv.dap.variables': async (args) => ({ ok: true, variables: await session.variables(args.ref) }),
111
+ 'edrv.dap.evaluate': async (args) => ({ ok: true, ...(await session.evaluate(args.expression, args.frameId)) }),
112
+ 'edrv.dap.command': async (args) => {
113
+ if (!DAP_ACTIONS.includes(args.action as DapAction)) return { ok: false, error: '未知动作:' + args.action }
114
+ if (args.action === 'disconnect') session.stop()
115
+ else session.action(args.action as DapAction)
116
+ return { ok: true }
117
+ },
118
+ }
119
+
120
+ return { handlers, dispose: () => session.dispose() }
121
+ }
122
+
123
+ /**
124
+ * processName → pid:唯一命中直用;0 命中或多命中返回错误文案(客户端引导用 processes 列表选择)。
125
+ * @author ddj 2026年09月29号
126
+ * @param processName 进程名(标题/文件名包含匹配)
127
+ */
128
+ async function resolvePid(processName?: string): Promise<number | string> {
129
+ const items = filterProcesses(await listProcesses(), processName)
130
+ if (items.length === 1) return items[0].pid
131
+ if (items.length === 0) return '未找到匹配进程:' + (processName || '(空)')
132
+ return '匹配到 ' + items.length + ' 个进程,请先在进程列表中选择(edrv.dap.processes)'
133
+ }
134
+
135
+ /**
136
+ * 读工作区调试配置:优先 `.dsh/launch.json`(插件专属);缺失时从旧共用
137
+ * `.vscode/launch.json` 一次性迁移(只读 legacy 源、永不回写;复制失败当次
138
+ * 仍用 legacy 内容响应不丢配置,下次读重试);都缺失 → 空表。
139
+ * @author ddj 2026年09月22号
140
+ * @param ctx DSH 上下文
141
+ * @param workspacePath 工作区绝对路径
142
+ * @returns 解析后的配置列表
143
+ */
144
+ async function readLaunchConfigs(ctx: Ctx, workspacePath: string): Promise<DapDebugConfig[]> {
145
+ const fs = ctx.get('fs')
146
+ if (!fs || !workspacePath) return []
147
+ try {
148
+ const target = await fs.resolve(DAP_LAUNCH_REL, { cwd: workspacePath })
149
+ let text = ''
150
+ let exists = true
151
+ try {
152
+ text = String(await fs.readText(target) ?? '')
153
+ } catch {
154
+ exists = false
155
+ }
156
+ if (exists) return parseLaunchConfigs(text, workspacePath)
157
+ // 新文件缺失 → 读旧共用源:命中则本次以内存内容响应(写入失败不丢配置,下次读重试)
158
+ let legacy = ''
159
+ let legacyExists = true
160
+ try {
161
+ const source = await fs.resolve(DAP_LEGACY_LAUNCH_REL, { cwd: workspacePath })
162
+ legacy = String(await fs.readText(source) ?? '')
163
+ } catch {
164
+ legacyExists = false
165
+ }
166
+ if (!legacyExists) return []
167
+ try {
168
+ const realPath = fs.processPath(target)
169
+ await mkdir(join(realPath, '..'), { recursive: true })
170
+ await writeFile(realPath, legacy, 'utf8')
171
+ log.debug('[dap] 已迁移 ' + DAP_LEGACY_LAUNCH_REL + ' → ' + DAP_LAUNCH_REL + '(' + workspacePath + ')')
172
+ } catch (error) {
173
+ log.debug('[dap] launch.json 迁移失败(下次读重试,本次以旧源响应):' + String(error))
174
+ }
175
+ return parseLaunchConfigs(legacy, workspacePath)
176
+ } catch (error) {
177
+ log.debug('[dap] launch.json 读取失败(视为无配置):' + String(error))
178
+ return []
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 未知调试类型的错误文案:列出已识别类型(含不可用原因),引导用户安装/排查。
184
+ * @author ddj 2026年09月21号
185
+ * @param type launch.json 中的调试类型
186
+ */
187
+ function unknownTypeMessage(type: string): string {
188
+ const known = debuggerDecls()
189
+ .map((d) => d.type + (d.available ? '' : '(' + (d.reason ?? '不可用') + ')'))
190
+ .join(' / ')
191
+ return '未发现调试类型「' + type + '」的适配器(已识别:' + (known || '无,请安装带调试器的扩展') + ')'
192
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * dsh-vscode-mode host — DAP source 路径归一化与候选排序。
3
+ * 适配器可能上报绝对路径、工作区相对路径、file:// URI 或 Lua chunkname;
4
+ * 这里统一转换,避免调用栈只显示 rawFile 但无法跳转。
5
+ * 作者 ddj 2026年09月29号
6
+ */
7
+
8
+ /** 统一路径分隔符并去掉 chunkname 的 @ 前缀。 */
9
+ export function normalizeSourcePath(value: unknown): string {
10
+ let text = String(value ?? '').trim()
11
+ if (!text) return ''
12
+ if (/^file:\/\//i.test(text)) text = fileUriPath(text)
13
+ return text.replace(/^@/, '').replace(/\\/g, '/')
14
+ }
15
+
16
+ /** 将 file:// URI 转成本地路径(兼容 Windows 盘符与 UNC)。 */
17
+ function fileUriPath(value: string): string {
18
+ try {
19
+ const url = new URL(value)
20
+ let path = decodeURIComponent(url.pathname)
21
+ if (url.hostname && url.hostname !== 'localhost') path = '//' + url.hostname + path
22
+ if (/^\/[A-Za-z]:/.test(path)) path = path.slice(1)
23
+ return path
24
+ } catch {
25
+ return value.replace(/^file:\/\//i, '')
26
+ }
27
+ }
28
+
29
+ /** 取路径 basename(大小写保持原样)。 */
30
+ export function sourceBaseOf(value: unknown): string {
31
+ const path = normalizeSourcePath(value)
32
+ return path.split('/').pop() ?? ''
33
+ }
34
+
35
+ /** 规范化路径键(大小写不敏感,供候选缓存使用)。 */
36
+ export function sourceKeyOf(value: unknown): string {
37
+ return normalizeSourcePath(value).toLowerCase()
38
+ }
39
+
40
+ /**
41
+ * 把 source 路径转换为工作区相对路径;工作区外路径保留规范化绝对路径,
42
+ * 不再返回空串,调用方仍可交给现有 tabPathOf/absoluteOf 继续处理。
43
+ */
44
+ export function sourcePathOf(value: unknown, workspacePath: string): string {
45
+ const source = normalizeSourcePath(value)
46
+ if (!source) return ''
47
+ const root = normalizeSourcePath(workspacePath).replace(/\/+$/, '')
48
+ if (root && source.toLowerCase().startsWith((root + '/').toLowerCase())) {
49
+ return source.slice(root.length + 1)
50
+ }
51
+ return source
52
+ }
53
+
54
+ /**
55
+ * 对 findFile 候选按 chunkname 相关度排序:完整路径后缀 > 路径片段 > basename/stem,
56
+ * 同分时保持确定性字典序,避免同名 Lua 文件随机取第一项。
57
+ */
58
+ export function rankSourceCandidates(chunk: string, files: string[], workspacePath: string): string[] {
59
+ const needle = normalizeSourcePath(chunk).toLowerCase()
60
+ const needleParts = needle.split('/').filter(Boolean)
61
+ const needleBase = sourceBaseOf(needle).toLowerCase()
62
+ const needleStem = needleBase.replace(/\.[^.]+$/, '')
63
+ return [...files].sort((a, b) => scoreSource(b, needle, needleParts, needleBase, needleStem, workspacePath) - scoreSource(a, needle, needleParts, needleBase, needleStem, workspacePath) || a.localeCompare(b))
64
+ }
65
+
66
+ /** 计算单个候选相关度。 */
67
+ function scoreSource(file: string, needle: string, parts: string[], base: string, stem: string, workspacePath: string): number {
68
+ const rel = sourcePathOf(file, workspacePath).toLowerCase()
69
+ const fileBase = sourceBaseOf(rel).toLowerCase()
70
+ const fileStem = fileBase.replace(/\.[^.]+$/, '')
71
+ let score = 0
72
+ if (needle && (rel === needle || rel.endsWith('/' + needle) || needle.endsWith('/' + rel))) score += 100000
73
+ if (base && fileBase === base) score += 10000
74
+ if (stem && fileStem === stem) score += 5000
75
+ const relParts = rel.split('/').filter(Boolean)
76
+ let suffix = 0
77
+ for (let i = 1; i <= Math.min(parts.length, relParts.length); i++) {
78
+ if (parts[parts.length - i] !== relParts[relParts.length - i]) break
79
+ suffix += 1
80
+ }
81
+ return score + suffix * 100
82
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,6 @@ import { handleRpc } from './rpc.js'
13
13
  import { newSearcher } from './search/orchestrator.js'
14
14
  import { newContentSearcher } from './search/content.js'
15
15
  import { installIsolation } from './mcpIsolation.js'
16
- import { dropFileIndex } from './workspace.js'
17
16
  import { cwdOf } from './registry.js'
18
17
  import { setupOpenSettings } from './fileOpenSettings.js'
19
18
  import { shellMenuLifecycle } from './integrate.js'
@@ -27,6 +26,7 @@ import { disposeAllServers, hookExitReclaim } from './lsp/transport.js'
27
26
  import { createAiRpc } from './ai/rpc.js'
28
27
  import { createFileVersions } from './fileVersions.js'
29
28
  import { createSvnRpc } from './svn.js'
29
+ import { createDapRpc } from './dap/rpc.js'
30
30
  import { installRulesSection } from './rules.js'
31
31
  import { installSkillGroup } from './skills.js'
32
32
  import type { RpcHandlerMap } from './shared/rpc.js'
@@ -70,6 +70,9 @@ export function apply(ctx: Ctx, config?: unknown): void {
70
70
  /** SVN RPC(检测/更新/Tortoise 发射;settings 提供 svnPath/tortoisePath)。 */
71
71
  const svnRpc = createSvnRpc({ ctx, settings: openSettings })
72
72
  const svnHandlers: Partial<RpcHandlerMap> = svnRpc.handlers as Partial<RpcHandlerMap>
73
+ /** 调试 RPC(DAP 桥单例:spawn 扩展适配器 + 事件缓冲 + findFile 反向匹配)。 */
74
+ const dapRpc = createDapRpc(ctx)
75
+ const dapHandlers: Partial<RpcHandlerMap> = dapRpc.handlers
73
76
  /** 文件磁盘新鲜度观察器(客户端轮询 edrv.versions;变化时顺带失效目录树缓存)。 */
74
77
  const fileVersions = createFileVersions(ctx)
75
78
 
@@ -81,7 +84,6 @@ export function apply(ctx: Ctx, config?: unknown): void {
81
84
  const cwd = cwdOf(session as never)
82
85
  if (cwd) {
83
86
  registry.delete(cwd)
84
- dropFileIndex(cwd)
85
87
  searcher.dispose(cwd)
86
88
  contentSearcher.dispose(cwd)
87
89
  disposeIndex(cwd)
@@ -90,7 +92,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
90
92
  if (typeof sid === 'string') lspRpc.disposeSession(sid)
91
93
  })
92
94
 
93
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers), (warning) => warnings.push(warning))
95
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers), (warning) => warnings.push(warning))
94
96
  installIsolation(ctx)
95
97
  // 系统集成生命周期:启动自动恢复右键菜单注册(marker 存在时);插件卸载/reload 清理注册痕迹
96
98
  ctx.effect(() => shellMenuLifecycle(ctx))
@@ -104,6 +106,8 @@ export function apply(ctx: Ctx, config?: unknown): void {
104
106
  })
105
107
  // 卸载时清空文件版本基准表(观察器为模块内单例,不清会跨装配残留陈旧版本)
106
108
  ctx.effect(() => () => fileVersions.dispose())
109
+ // 卸载/重启时结束调试会话并强杀适配器子进程(防残留注入器/适配器孤儿)
110
+ ctx.effect(() => () => dapRpc.dispose())
107
111
  // 宿主进程退出回收:ctx.effect 清理不覆盖进程退出,缺此注册会留下跨重启的孤儿服务器
108
112
  hookExitReclaim()
109
113
 
@@ -214,8 +214,9 @@ export function sdkEnvOf(sdkRoot: string): Record<string, string> {
214
214
  return env
215
215
  }
216
216
 
217
- /** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。 */
218
- function manifestVersionOf(extDir: string): string | undefined {
217
+ /** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。
218
+ * 导出供 dap/discovery 复用(扩展清单版本排序同口径,避免第二份实现)。 */
219
+ export function manifestVersionOf(extDir: string): string | undefined {
219
220
  try {
220
221
  const manifest = JSON.parse(readFileSync(join(extDir, 'package.json'), 'utf8')) as { version?: unknown }
221
222
  return typeof manifest.version === 'string' ? manifest.version : undefined
package/src/reveal.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * dsh-vscode-mode host — 在 OS 文件浏览器中打开/定位路径(reveal 能力)。
3
- * 纯函数 revealCommand 平台分发 + revealInExplorer 经 ctx.subprocess.spawn 发射
4
- * (argv 数组、无 shell 插值,沿 workspace/revert 的 subprocess 契约)。
3
+ * 纯函数 revealCommand 平台分发 + revealSpawnOpts 启动选项 + revealInExplorer 发射。
5
4
  * 文件 → 资源管理器选中定位;目录 → 打开目录;Linux 无通用定位协议 → 打开所在目录。
6
- * 作者 ddj 2026-08-27
5
+ *
6
+ * 刻意**不经** ctx.subprocess.spawn:DSH 的 Windows Job runner 硬编码
7
+ * `windowsHide: true`(dsh-subprocess-local 的 launchWindowsJob 与 runner 内部 spawn),
8
+ * 连 explorer.exe 的 GUI 窗口一并隐藏——RPC 回 ok:true 但窗口根本不出现。
9
+ * GUI 拉起属 fire-and-forget,用 node child_process + detached + unref 才贴合语义。
10
+ * 作者 ddj 2026年08月27号 / 2026年09月20号
7
11
  */
12
+ import { spawn } from 'node:child_process'
8
13
  import { dirname } from 'node:path'
9
- import type { Ctx } from './store.js'
10
14
 
11
15
  /** reveal 结果(沿 revert.ts 的 Result 风格)。 */
12
16
  export type RevealResult = { ok: true } | { ok: false; error: string }
@@ -35,28 +39,35 @@ export function revealCommand(absPath: string, isDir: boolean, platform: NodeJS.
35
39
  }
36
40
 
37
41
  /**
38
- * 经 host subprocess 服务发射 opener(fire-and-forget 语义)。
39
- * Explorer 为 GUI 分离进程,非零退出码不视为失败;仅 spawn 级错误回失败。
40
- * @author ddj 2026年08月27号
41
- * @param ctx DSH host 上下文
42
+ * 构造 opener 子进程启动选项。
43
+ * `windowsHide` 必须为 **false**:置 true 会把 Explorer 等 GUI 窗口一起隐藏,
44
+ * 症状即「RPC 回 ok:true 但窗口不出现」(本文件头注释记录的回归)。
45
+ * @author ddj 2026年09月20号
46
+ * @param cwd 子进程工作目录(打开文件时用其父目录)
47
+ * @returns child_process.spawn 选项
48
+ */
49
+ export function revealSpawnOpts(cwd: string): { cwd: string; stdio: 'ignore'; windowsHide: false; detached: true } {
50
+ return { cwd, stdio: 'ignore', windowsHide: false, detached: true }
51
+ }
52
+
53
+ /**
54
+ * 发射 opener 拉起 OS 文件浏览器(fire-and-forget 语义)。
55
+ * Explorer 为 GUI 分离进程,非零退出码不视为失败,故只等 spawn 事件即返回;
56
+ * 子进程脱离父进程组并 unref,避免阻塞宿主退出。
57
+ * @author ddj 2026年08月27号 / 2026年09月20号
42
58
  * @param absPath 绝对路径
43
59
  * @param isDir 是否为目录
44
60
  * @returns 成功或失败原因
45
61
  */
46
- export async function revealInExplorer(ctx: Ctx, absPath: string, isDir: boolean): Promise<RevealResult> {
47
- const sub = ctx.get('subprocess')
48
- if (!sub || typeof sub.spawn !== 'function') {
49
- return { ok: false, error: '打开文件浏览器不可用:缺少 subprocess 服务' }
50
- }
51
- const { argv } = revealCommand(absPath, isDir)
62
+ export async function revealInExplorer(absPath: string, isDir: boolean): Promise<RevealResult> {
63
+ const [program, ...args] = revealCommand(absPath, isDir).argv
64
+ if (!program) return { ok: false, error: '打开失败:opener 命令为空' }
52
65
  try {
53
- const handle = sub.spawn({
54
- argv,
55
- cwd: dirname(absPath),
56
- stdio: { stdout: { maxBytes: 1 << 16 }, stderr: { maxBytes: 1 << 16 }, stdin: 'ignore' },
57
- graceMs: 10000,
66
+ await new Promise<void>((resolve, reject) => {
67
+ const child = spawn(program, args, revealSpawnOpts(dirname(absPath)))
68
+ child.once('spawn', () => { child.unref(); resolve() })
69
+ child.once('error', (error) => { reject(error) })
58
70
  })
59
- await handle.done
60
71
  return { ok: true }
61
72
  } catch (error) {
62
73
  return { ok: false, error: '打开失败:' + String(error) }