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.
Files changed (78) hide show
  1. package/README.md +78 -5
  2. package/lib/client.js +5579 -236
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2358 -126
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/compat.ts +1 -1
  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/editorLimit.ts +31 -0
  27. package/src/client/index.ts +13 -0
  28. package/src/client/markdownPreview.ts +24 -0
  29. package/src/client/md/componentType.ts +28 -0
  30. package/src/client/md/mdPanel.ts +98 -0
  31. package/src/client/monaco/lsp/index.ts +15 -0
  32. package/src/client/monaco/lsp/providers.ts +2 -0
  33. package/src/client/monaco/theme.ts +15 -0
  34. package/src/client/searchSeed.ts +64 -0
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +70 -15
  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 +2 -0
  40. package/src/client/styles/editor.css +186 -1
  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 +54 -0
  45. package/src/client/ui/EditorView.ts +711 -29
  46. package/src/client/ui/McpSettings.ts +29 -0
  47. package/src/client/ui/SideBySideDiff.tsx +214 -29
  48. package/src/client/ui/SvnDiffPanel.ts +21 -8
  49. package/src/client/ui/SvnLogDialog.ts +44 -2
  50. package/src/client/ui/SvnPatchDialog.ts +63 -0
  51. package/src/client/ui/SvnSumDialog.ts +77 -0
  52. package/src/client/ui/commandCatalog.ts +54 -0
  53. package/src/client/ui/horizontalWheel.ts +85 -0
  54. package/src/client/ui/svnExport.ts +71 -0
  55. package/src/client-primitives.d.ts +34 -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/fileOpenSettings.ts +3 -0
  66. package/src/index.ts +7 -3
  67. package/src/lsp/providers.ts +3 -2
  68. package/src/reveal.ts +31 -20
  69. package/src/rpc.ts +11 -3
  70. package/src/search/ripgrep.ts +127 -11
  71. package/src/shared/dap.ts +262 -0
  72. package/src/shared/editorLimit.ts +46 -0
  73. package/src/shared/keybindings.ts +10 -0
  74. package/src/shared/rpc.ts +40 -2
  75. package/src/shared/svn.ts +147 -0
  76. package/src/shared/svnActions.ts +20 -0
  77. package/src/svn.ts +320 -11
  78. 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
+ }
@@ -11,6 +11,7 @@ import { createRequire } from 'node:module'
11
11
  import { pathToFileURL } from 'node:url'
12
12
  import type { Ctx } from './store.js'
13
13
  import { KEYBINDING_DEFAULTS } from './shared/keybindings.js'
14
+ import { EDITOR_LIMIT_DEFAULT } from './shared/editorLimit.js'
14
15
  import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
15
16
  import { TORTOISE_DIR_DEFAULT } from './shared/svn.js'
16
17
  import type { AiConfigPatch, AiConfigView } from './shared/ai.js'
@@ -340,6 +341,8 @@ export async function installOpenSettingsSection(
340
341
  fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
341
342
  keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
342
343
  sidebarMinWidth: deps.z.number().default(300),
344
+ // 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
345
+ maxOpenEditors: deps.z.number().default(EDITOR_LIMIT_DEFAULT),
343
346
  integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
344
347
  // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
345
348
  aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
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) }
package/src/rpc.ts CHANGED
@@ -256,6 +256,7 @@ export function buildHandlers(
256
256
  aiHandlers?: Partial<RpcHandlerMap>,
257
257
  fileVersions?: FileVersions,
258
258
  svnHandlers?: Partial<RpcHandlerMap>,
259
+ dapHandlers?: Partial<RpcHandlerMap>,
259
260
  ): RpcHandlerMap {
260
261
  return {
261
262
  // edrv.lsp.* 由 createLspRpc 一次性提供(tracker 跨请求保留),这里并入。
@@ -264,6 +265,8 @@ export function buildHandlers(
264
265
  ...((aiHandlers ?? {}) as RpcHandlerMap),
265
266
  // svn.* 由 createSvnRpc 提供(SVN 检测/更新/Tortoise 发射),这里并入。
266
267
  ...((svnHandlers ?? {}) as RpcHandlerMap),
268
+ // edrv.dap.* 由 createDapRpc 提供(调试会话单例),这里并入。
269
+ ...((dapHandlers ?? {}) as RpcHandlerMap),
267
270
  'edrv.list': async (args) => {
268
271
  const sc = await requireSession(ctx, args.sessionId)
269
272
  if ('err' in sc) return { ok: false, error: sc.err }
@@ -333,6 +336,7 @@ export function buildHandlers(
333
336
  if (!fs) return { ok: false, error: '缺少 fs' }
334
337
  try {
335
338
  const target = await resolveTarget(ctx, sc.session, args.path)
339
+ debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
336
340
  const info = await fs.stat(target)
337
341
  if (!info || info.type !== 'file') {
338
342
  // 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
@@ -366,6 +370,7 @@ export function buildHandlers(
366
370
  if (!fs) return { ok: false, error: '缺少 fs' }
367
371
  try {
368
372
  const target = await resolveTarget(ctx, sc.session, args.path)
373
+ debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
369
374
  const info = await fs.stat(target)
370
375
  if (!info || info.type !== 'file') return { ok: false, error: '文件不存在' }
371
376
  if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持差异重建' }
@@ -399,6 +404,7 @@ export function buildHandlers(
399
404
  if (!fs) return { ok: false, error: '缺少 fs' }
400
405
  try {
401
406
  const target = await resolveTarget(ctx, sc.session, args.path)
407
+ debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
402
408
  // 版本守卫(客户端带回上次读取的版本):文件在读取后被外部改过则拒绝写入,
403
409
  // 避免用陈旧缓冲静默覆盖外部改动;客户端未带 rev(旧版/无版本后端)时行为不变。
404
410
  const rev = typeof args.rev === 'string' && args.rev ? args.rev : null
@@ -421,6 +427,7 @@ export function buildHandlers(
421
427
  if (!fs) return { ok: false, error: '缺少 fs' }
422
428
  try {
423
429
  const target = await resolveTarget(ctx, sc.session, args.path)
430
+ debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
424
431
  const info = await fs.stat(target)
425
432
  if (!info || info.type !== 'file') return { ok: false, error: '文件不存在' }
426
433
  // fs 服务仅提供文本写(writeText/editText),二进制回写经 node:fs 直写解析后的
@@ -540,7 +547,7 @@ export function buildHandlers(
540
547
  },
541
548
  'edrv.dlog.reveal': async () => {
542
549
  // 打开日志根目录(OS 文件管理器;目录形态 → 直接打开)
543
- const revealed = await revealInExplorer(ctx, pluginLogRoot(), true)
550
+ const revealed = await revealInExplorer(pluginLogRoot(), true)
544
551
  if (!revealed.ok) return { ok: false, error: revealed.error }
545
552
  return { ok: true, opened: true }
546
553
  },
@@ -598,7 +605,7 @@ export function buildHandlers(
598
605
  const info = await fs.stat(target)
599
606
  if (!info) return { ok: false, error: '路径不存在' }
600
607
  const abs = fs.processPath(target)
601
- const outcome = await revealInExplorer(ctx, abs, info.type === 'directory')
608
+ const outcome = await revealInExplorer(abs, info.type === 'directory')
602
609
  if (!outcome.ok) return { ok: false, error: outcome.error }
603
610
  return { ok: true, revealed: abs }
604
611
  } catch (error) {
@@ -951,8 +958,9 @@ export async function handleRpc<M extends RpcMethod>(
951
958
  aiHandlers?: Partial<RpcHandlerMap>,
952
959
  fileVersions?: FileVersions,
953
960
  svnHandlers?: Partial<RpcHandlerMap>,
961
+ dapHandlers?: Partial<RpcHandlerMap>,
954
962
  ): Promise<RpcResult<M>> {
955
- const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers)
963
+ const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers)
956
964
  const handler = handlers[method]
957
965
  if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
958
966
  return handler(args)