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
@@ -15,7 +15,12 @@ const STDOUT_CAP = 4 << 20
15
15
  const STDERR_CAP = 64 << 10
16
16
  const GRACE_MS = 20_000
17
17
 
18
- /** 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。 */
18
+ /**
19
+ * 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。
20
+ * 语义为**大小写敏感**的路径段匹配:`build` 只排除小写构建产物目录,
21
+ * 不得用 `--glob-case-insensitive` 之类的全局开关,「Module/Build」这类
22
+ * 源码目录(如 IslandSplash 的 Module/Build/Logic)必须保持可搜索。
23
+ */
19
24
  export const EXCLUDES = [
20
25
  'node_modules', '.git', '.tmp', '.cache', 'dist', 'build', 'vendor',
21
26
  'coverage', '__pycache__', '.pnpm-store', '.npm-cache', '.codegraph', '.workbuddy',
@@ -52,15 +57,32 @@ interface SearchSubprocess {
52
57
  }
53
58
 
54
59
  /**
55
- * 将 query 转成 rg 的字面 glob。
56
- * @author ddj 2026年08月24号
57
- * @param query 已规范化 query
60
+ * 将查询文本转成大小写不敏感的 rg 正选 glob。
61
+ * 逐字符把有大小写的字母展开成 `[aA]` 字符类,**不用**大小写不敏感开关:
62
+ * ① 全局 `--glob-case-insensitive` 会把 `-g/--glob` 全部当成 `--iglob`,
63
+ * 连带让 `!**\/build/**` 大小写不敏感,从而排除 `Module/Build/` 这类源码目录
64
+ * (Ctrl+P 搜 BuildLogic 恒「无匹配文件」);
65
+ * ② 把查询词改用 `--iglob` 也不行——rg 的正选 glob 会覆盖全部黑名单 glob,
66
+ * node_modules/.git 等排除项集体失效。
67
+ * 字符类方案同时满足「查询不区分大小写」且「排除项保持大小写敏感」。
68
+ * @author ddj 2026年08月24号 / 2026年09月20号
69
+ * @param query 已规范化查询文本
58
70
  * @returns 安全 glob
59
71
  */
60
72
  export function queryGlob(query: string): string {
61
73
  const value = pathText(query)
62
- const escaped = value.replace(/[\*?\[\]{}!]/g, (char) => '\\' + char)
63
- return '**/*' + escaped + '*'
74
+ let body = ''
75
+ for (const char of value) {
76
+ const lower = char.toLowerCase()
77
+ const upper = char.toUpperCase()
78
+ // 有大小写之分的字符(单字符映射,避开 ß→SS 这类长度变化)→ 字符类
79
+ if (lower !== upper && lower.length === 1 && upper.length === 1) {
80
+ body += '[' + lower + upper + ']'
81
+ continue
82
+ }
83
+ body += /[\*?\[\]{}!]/.test(char) ? '\\' + char : char
84
+ }
85
+ return '**/*' + body + '*'
64
86
  }
65
87
 
66
88
  /**
@@ -159,6 +181,24 @@ export function rgExitFailure(code: number | null | undefined, stderrText: strin
159
181
  return { kind: 'hard', message: 'ripgrep 退出码:' + String(code) + (detail ? '(' + detail + ')' : '') }
160
182
  }
161
183
 
184
+ /**
185
+ * 构造文件搜索 rg argv(查询 glob 大小写不敏感,排除 glob 大小写敏感)。
186
+ * 大小写不敏感**只能**靠 queryGlob 的字符类实现:全局 `--glob-case-insensitive`
187
+ * 会波及排除项(误排除 `Module/Build/`),查询词用 `--iglob` 又会覆盖全部黑名单 glob。
188
+ * @author ddj 2026年09月20号
189
+ * @param binary rg 可执行文件路径
190
+ * @param root 搜索根目录
191
+ * @param query 已规范化查询文本
192
+ * @returns 完整 argv(末位为根目录)
193
+ */
194
+ export function filesArgv(binary: string, root: string, query: string): string[] {
195
+ const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob', queryGlob(query)]
196
+ for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
197
+ for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
198
+ argv.push('--', root)
199
+ return argv
200
+ }
201
+
162
202
  /**
163
203
  * 使用打包 ripgrep 搜索工作区文件。
164
204
  * @author ddj 2026年08月24号
@@ -172,12 +212,8 @@ export async function searchRipgrep(input: WorkspaceSearchInput): Promise<Worksp
172
212
  if (!binary) throw new Error('ripgrep 不可用')
173
213
  const root = input.root ?? await searchRoot(input.ctx, input.session)
174
214
  const query = prepareQuery(input.query)
175
- const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob-case-insensitive', '--glob', queryGlob(query.text)]
176
- for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
177
- for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
178
- argv.push('--', root)
179
215
  const handle = sub.spawn({
180
- argv,
216
+ argv: filesArgv(binary, root, query.text),
181
217
  cwd: root,
182
218
  stdio: { stdin: 'ignore', stdout: { maxBytes: STDOUT_CAP }, stderr: { maxBytes: STDERR_CAP } },
183
219
  graceMs: GRACE_MS,
@@ -202,6 +238,86 @@ export async function searchRipgrep(input: WorkspaceSearchInput): Promise<Worksp
202
238
  return { ...parsed, source: 'ripgrep' }
203
239
  }
204
240
 
241
+ /**
242
+ * 构造 chunkname 定向查找的 rg argv。
243
+ * 与 {@link filesArgv} 同源(同样的排除 glob 与大小写口径),差异只在查询 glob 由
244
+ * chunkname 推导:chunkname 可能带 chunk 前缀(`@`)与无扩展名 stem,故先剥 `@`、
245
+ * 取 basename、去掉扩展名后作为片段匹配,仍走 {@link queryGlob} 的字符类方案
246
+ * ——避免 `--glob-case-insensitive` 波及排除项(见 queryGlob 注释)。
247
+ * @author ddj 2026年09月21号
248
+ * @param binary rg 可执行文件路径
249
+ * @param root 搜索根目录
250
+ * @param chunk 适配器上报的 chunkname
251
+ * @returns 完整 argv(末位为根目录);chunk 为空时返回 null
252
+ */
253
+ export function chunkFilesArgv(binary: string, root: string, chunk: string): string[] | null {
254
+ const clean = normalizeChunk(chunk)
255
+ if (!clean) return null
256
+ const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob', queryGlob(clean)]
257
+ for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
258
+ for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
259
+ argv.push('--', root)
260
+ return argv
261
+ }
262
+
263
+ /**
264
+ * chunkname → 用于片段匹配的纯文件名片段。
265
+ * 剥 `@` 前缀、统一分隔符、取 basename、去掉扩展名(xLua chunkname 惯例不含扩展名),
266
+ * 使 `GuideSystemMgr` 能命中 `GuideSystemMgr.lua`。
267
+ * @author ddj 2026年09月21号
268
+ * @param chunk 适配器上报的 chunkname
269
+ * @returns 纯片段(空输入返回空串)
270
+ */
271
+ export function normalizeChunk(chunk: string): string {
272
+ const text = String(chunk ?? '').replace(/^@/, '').replaceAll('\\', '/').trim()
273
+ if (!text) return ''
274
+ const base = text.split('/').pop() ?? ''
275
+ return base.replace(/\.[^.]+$/, '')
276
+ }
277
+
278
+ /**
279
+ * 按 chunkname 定向查找工作区文件(DAP findFile 反向匹配专用)。
280
+ *
281
+ * 为什么不用工作区全量文件索引:22 万文件的 Unity 工程里,全量枚举按到达序截断,
282
+ * `Assets/Scripts/Lua/**` 会被 `GameData`/`Entitas` 等目录挤出配额,导致断点命中后
283
+ * 无法定位源文件(实测前 6000 条中 .lua 数为 0)。此处改为让 rg 在遍历时过滤,
284
+ * 命中规模与工作区总规模无关(实测同一工作区 0.37s 返回正确结果)。
285
+ * @author ddj 2026年09月21号
286
+ * @param ctx DSH 上下文
287
+ * @param root 工作区根(subprocess 可访问路径)
288
+ * @param chunk 适配器上报的 chunkname
289
+ * @param maxResults 候选上限
290
+ * @returns 绝对路径候选(任何失败都返回空数组,不抛错)
291
+ */
292
+ export async function findFilesByChunk(ctx: Ctx, root: string, chunk: string, maxResults = 50): Promise<string[]> {
293
+ const sub = ctx.get('subprocess') as SearchSubprocess | undefined
294
+ const binary = ripgrepPath()
295
+ if (!sub || !binary || !root) return []
296
+ const argv = chunkFilesArgv(binary, root, chunk)
297
+ if (!argv) return []
298
+ let handle: SearchHandle
299
+ try {
300
+ handle = sub.spawn({
301
+ argv,
302
+ cwd: root,
303
+ stdio: { stdin: 'ignore', stdout: { maxBytes: STDOUT_CAP }, stderr: { maxBytes: STDERR_CAP } },
304
+ graceMs: GRACE_MS,
305
+ })
306
+ } catch (error) {
307
+ return []
308
+ }
309
+ let code: number | null | undefined
310
+ try {
311
+ const outcome = await handle.done
312
+ code = outcome.exitCode ?? outcome.code
313
+ } catch (error) {
314
+ return []
315
+ }
316
+ // 0=有命中 1=无命中 2=部分不可访问(已收集结果仍有效):其余视为失败
317
+ if (code !== 0 && code !== 1 && code !== 2) return []
318
+ return parseOutput(handle, maxResults).files
319
+ }
320
+
205
321
  /**
206
322
  * 构造 ripgrep provider。
207
323
  * @author ddj 2026年08月24号
@@ -0,0 +1,262 @@
1
+ /**
2
+ * dsh-vscode-mode — 调试(DAP)双面契约:调试会话状态、事件投影视图、调试配置与
3
+ * edrv.dap.* RPC 载荷。纯类型与纯函数,禁 node/react(同 shared 纯度规则)。
4
+ * 适配器 = 用户已装扩展清单 contributes.debuggers 声明的 DAP 适配器(通用,不限定语言)。
5
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
6
+ */
7
+
8
+ /** 调试会话相位。 */
9
+ export type DapPhase = 'idle' | 'starting' | 'waiting' | 'running' | 'paused' | 'terminated'
10
+
11
+ /** 系统进程条目(进程枚举投影视图)。 */
12
+ export interface DapProcessInfo {
13
+ pid: number
14
+ /** 窗口标题(processName 匹配目标,与 VS Code 扩展语义一致)。 */
15
+ title: string
16
+ /** 可执行文件完整路径。 */
17
+ path: string
18
+ /** 路径 basename(如 Unity.exe)。 */
19
+ name: string
20
+ }
21
+
22
+ /** 调试配置(launch.json configuration 投影视图 + raw 原样透传)。 */
23
+ export interface DapDebugConfig {
24
+ /** 配置名(工具条下拉显示)。 */
25
+ name: string
26
+ /** 适配器类型(= 扩展清单 contributes.debuggers.type,如 emmylua_attach / coreclr / unity)。 */
27
+ type: string
28
+ /** DAP request:attach | launch(缺省 launch;emmylua_attach 旧配置缺 request 时按 attach 兼容)。 */
29
+ request?: string
30
+ /** 进程名(标题或文件名包含匹配;attach 型按它自动选 pid)。 */
31
+ processName?: string
32
+ /** 直接指定 pid(>0 时跳过进程枚举)。 */
33
+ pid?: number
34
+ /** 附加目标 pid(coreclr/unity 等适配器的 launch.json processId 字段)。 */
35
+ processId?: number
36
+ /** emmylua_new 型:目标 emmy_core 监听地址。 */
37
+ host?: string
38
+ port?: number
39
+ /** 源码根目录列表(chunkname → 文件映射辅助)。 */
40
+ sourcePaths?: string[]
41
+ /** 源码扩展名(emmylua 用;通用适配器按声明 languages 推导)。 */
42
+ ext?: string[]
43
+ /** 附加时捕获 Unity 日志(emmy_tool -capture-log)。 */
44
+ captureLog?: boolean
45
+ /** launch.json 原始配置(attach/launch 请求参数原样透传,含全部未识别字段,变量已替换)。 */
46
+ raw?: Record<string, unknown>
47
+ }
48
+
49
+ /** launch.json「添加配置」下拉条目(扩展清单 configurationSnippets / initialConfigurations 投影)。 */
50
+ export interface DapConfigSnippet {
51
+ /** 下拉显示名(%nls% 占位符已解析)。 */
52
+ label: string
53
+ /** 条目说明(%nls% 占位符已解析;无则空)。 */
54
+ description?: string
55
+ /** 适配器类型(= body.type,缺失时由清单条目补齐)。 */
56
+ type: string
57
+ /** 片段主体 JSON 文本(对象;%nls% 已解析,插入方按目标文件缩进重排)。 */
58
+ bodyText: string
59
+ /** 适配器可用性(与工具条同源 debuggerDecls 裁决;false = 下拉过滤不给插入;缺省 = 老 host 未标注,按可用处理)。 */
60
+ available?: boolean
61
+ /** 不可用原因(available=false 时给出,如「适配器入口不存在」)。 */
62
+ reason?: string
63
+ }
64
+
65
+ /** 调试事件投影视图(host 缓冲、客户端 poll 拉增量)。 */
66
+ export interface DapEventView {
67
+ /** 事件游标(单调递增,poll since 用)。 */
68
+ seq: number
69
+ kind: 'output' | 'stopped' | 'continued' | 'terminated'
70
+ /** output 文本 / stopped 原因。 */
71
+ text?: string
72
+ reason?: string
73
+ }
74
+
75
+ /** 调用栈帧视图(file 已归一为工作区相对路径,空串=未能映射)。 */
76
+ export interface DapFrameView {
77
+ id: number
78
+ name: string
79
+ file: string
80
+ rawFile: string
81
+ line: number
82
+ }
83
+
84
+ /** 作用域视图(Variables=局部+upvalue / ENV)。 */
85
+ export interface DapScopeView {
86
+ name: string
87
+ ref: number
88
+ hint?: string
89
+ }
90
+
91
+ /** 变量视图(ref>0 表示可继续展开)。 */
92
+ export interface DapVariableView {
93
+ name: string
94
+ value: string
95
+ type?: string
96
+ ref: number
97
+ }
98
+
99
+ /** 停帧首栈视图(命中断点时驱动文件跳转与行高亮)。 */
100
+ export interface DapTopFrame {
101
+ file: string
102
+ rawFile: string
103
+ line: number
104
+ name: string
105
+ }
106
+
107
+ /** 调试会话状态视图(poll 返回,客户端渲染依据)。 */
108
+ export interface DapStateView {
109
+ phase: DapPhase
110
+ configName?: string
111
+ configType?: string
112
+ pid?: number
113
+ error?: string
114
+ topFrame?: DapTopFrame | null
115
+ }
116
+
117
+ /** poll 增量结果。 */
118
+ export interface DapPollResult {
119
+ state: DapStateView
120
+ events: DapEventView[]
121
+ nextSeq: number
122
+ }
123
+
124
+ /** 断点输入(path 相对工作区根,'/' 分隔;host 负责转绝对路径给适配器)。 */
125
+ export interface DapBreakpointInput {
126
+ path: string
127
+ line: number
128
+ condition?: string
129
+ hitCondition?: string
130
+ logMessage?: string
131
+ }
132
+
133
+ /** 调试控制动作(edrv.dap.command)。 */
134
+ export type DapAction = 'continue' | 'next' | 'stepIn' | 'stepOut' | 'pause' | 'disconnect'
135
+
136
+ /** 全部合法动作(运行时校验用)。 */
137
+ export const DAP_ACTIONS: readonly DapAction[] = ['continue', 'next', 'stepIn', 'stepOut', 'pause', 'disconnect']
138
+
139
+ /** 调试配置文件(工作区相对路径;插件专属,与 VS Code 的 .vscode/launch.json 互不干扰)。 */
140
+ export const DAP_LAUNCH_REL = '.dsh/launch.json'
141
+
142
+ /** 旧共用配置路径(仅首读迁移源:复制到 DAP_LAUNCH_REL 后不再读取,永不回写)。 */
143
+ export const DAP_LEGACY_LAUNCH_REL = '.vscode/launch.json'
144
+
145
+ /** 断点确认结果(适配器真实回执;适配器可能挪行或判不可验证)。 */
146
+ export interface DapBreakpointAck {
147
+ line: number
148
+ verified: boolean
149
+ /** 适配器说明(如「该行无可执行代码」)。 */
150
+ message?: string
151
+ }
152
+
153
+ /** 配置来源。 */
154
+ export type DapConfigSource = 'launchjson' | 'settings' | 'none'
155
+
156
+ /** 调试适配器声明(扩展清单 contributes.debuggers 条目投影视图)。 */
157
+ export interface DapAdapterDecl {
158
+ /** 调试类型(launch.json type 匹配键,= initialize.adapterID)。 */
159
+ type: string
160
+ /** 显示名(下拉与错误提示用)。 */
161
+ label: string
162
+ /** 来源扩展 id(publisher.name)。 */
163
+ extensionId: string
164
+ /** 来源扩展根目录(emmylua 兼容 shim 的 extensionPath 来源)。 */
165
+ extensionPath: string
166
+ /** 适配器入口绝对路径(已按平台解析 program/windows.program)。 */
167
+ program: string
168
+ /** 清单声明的启动参数(如 clrdbg 的 --interpreter=vscode)。 */
169
+ args: string[]
170
+ /** 运行时:'node' = 用 node 跑 js 入口;空 = 直接 spawn program。 */
171
+ runtime?: string
172
+ /** 声明语言(如 ['lua'] / ['csharp'])。 */
173
+ languages: string[]
174
+ /** 该适配器可下断点的文件扩展名(小写、带点;空 = 不限)。 */
175
+ exts: string[]
176
+ /** 入口是否真实存在(DotRush 等按需下载的适配器可能缺失)。 */
177
+ available: boolean
178
+ /** 不可用原因(available=false 时给出)。 */
179
+ reason?: string
180
+ }
181
+
182
+ /** 下发客户端的适配器可用性视图(edrv.dap.configs.adapters 元素)。 */
183
+ export interface DapAdapterInfo {
184
+ type: string
185
+ label: string
186
+ available: boolean
187
+ reason?: string
188
+ }
189
+
190
+ /**
191
+ * chunkname 与候选文件的 basename 匹配(纯函数,可单测)。
192
+ * 适配器 findFileReq 的 file 是 Lua chunkname(可能带 @ 前缀/相对点路径/反斜杠,
193
+ * 且常常不含 .lua 扩展名——xLua 惯例);匹配口径:剥 @、统一分隔符后,
194
+ * 完整相对路径相等 / basename 相等 / basename 去扩展名 stem 相等(大小写不敏感)。
195
+ * @author ddj 2026年09月29号
196
+ * @param chunk 适配器上报的 chunkname
197
+ * @param relPath 候选文件工作区相对路径('/' 分隔)
198
+ * @returns 是否命中
199
+ */
200
+ export function chunkMatchesFile(chunk: string, relPath: string): boolean {
201
+ const clean = String(chunk || '').replace(/^@/, '').replace(/\\/g, '/')
202
+ const rel = String(relPath || '').replace(/\\/g, '/')
203
+ if (!clean || !rel) return false
204
+ if (clean === rel) return true
205
+ const chunkBase = (clean.split('/').pop() ?? '').toLowerCase()
206
+ const fileBase = (rel.split('/').pop() ?? '').toLowerCase()
207
+ if (!chunkBase || !fileBase) return false
208
+ if (chunkBase === fileBase) return true
209
+ return chunkBase === fileBase.replace(/\.[^.]+$/, '')
210
+ }
211
+
212
+ /**
213
+ * 配置 → DAP request 类型:显式 request 优先;emmylua_attach 旧配置缺 request 时按 attach 兼容。
214
+ * @author ddj 2026年09月21号
215
+ * @param config 调试配置(取 type/request 两字段)
216
+ * @returns 'attach' | 'launch'
217
+ */
218
+ export function requestOfConfig(config: Pick<DapDebugConfig, 'type' | 'request'>): 'attach' | 'launch' {
219
+ if (config.request === 'attach' || config.request === 'launch') return config.request
220
+ return config.type === 'emmylua_attach' ? 'attach' : 'launch'
221
+ }
222
+
223
+ /**
224
+ * 是否应发送 configurationDone(DAP 标准握手收尾:断点下发完 → configurationDone → 目标开跑)。
225
+ * 仅当适配器在 initialize 响应声明 supportsConfigurationDoneRequest 时发送;
226
+ * emmylua 适配器未声明该能力(实测响应体仅 5 个 supports* 字段)→ 不发,Lua 行为不变。
227
+ * @author ddj 2026年09月21号
228
+ * @param caps initialize 响应 body(capabilities)
229
+ */
230
+ export function needsConfigurationDone(caps: Record<string, unknown> | null | undefined): boolean {
231
+ return caps?.supportsConfigurationDoneRequest === true
232
+ }
233
+
234
+ /**
235
+ * stopped 事件 body → 当前线程 id:多线程适配器(coreclr 等)取真实 threadId,
236
+ * 缺省/非法回退 1(单线程适配器 emmylua 语义不变)。
237
+ * @author ddj 2026年09月21号
238
+ * @param body stopped 事件 body
239
+ */
240
+ export function threadIdOfStopped(body: Record<string, unknown>): number {
241
+ const id = body.threadId
242
+ return typeof id === 'number' && Number.isInteger(id) && id > 0 ? id : 1
243
+ }
244
+
245
+ /**
246
+ * setBreakpoints 响应 body → 断点回执(DAP 顺序与请求一致;缺行回退请求行)。
247
+ * 适配器会把断点挪到最近可绑定行并给出 verified/message——必须消费而非造假回执。
248
+ * @author ddj 2026年09月21号
249
+ * @param body setBreakpoints 响应 body(breakpoints 数组)
250
+ * @param items 本次请求的断点(回退基准)
251
+ */
252
+ export function bpAcksOfResponse(body: unknown, items: DapBreakpointInput[]): DapBreakpointAck[] {
253
+ const list = (body as { breakpoints?: unknown } | null)?.breakpoints
254
+ if (!Array.isArray(list)) return items.map((it) => ({ line: it.line, verified: true }))
255
+ return list.map((item, i) => {
256
+ const b = (item ?? {}) as Record<string, unknown>
257
+ const line = typeof b.line === 'number' && b.line >= 1 ? b.line : items[i]?.line ?? 0
258
+ const ack: DapBreakpointAck = { line, verified: b.verified === true }
259
+ if (typeof b.message === 'string' && b.message) ack.message = b.message
260
+ return ack
261
+ })
262
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * dsh-vscode-mode shared — 编辑器页签数量上限的常量与归一化(host 与 client 双面契约)。
3
+ *
4
+ * 纯数据模块:host(settings schema 默认值)与 client(淘汰判定)共用,禁止 import
5
+ * 浏览器 API 或 Node API。
6
+ *
7
+ * ⚠️ 与 sidebarMinWidth 的关键差异:**0 是合法值**,语义为「不限制页签数量」,
8
+ * 因此归一化时不能像 normalizeSidebarMinWidth 那样把 0 当作非法输入回退默认值。
9
+ *
10
+ * 作者 ddj 2026年09月18号
11
+ */
12
+
13
+ /** 页签数量上限默认值(对齐 VS Code workbench.editor.limit 默认 10)。 */
14
+ export const EDITOR_LIMIT_DEFAULT = 10
15
+ /** 上限允许下界:0 = 不限制(关闭上限功能)。 */
16
+ export const EDITOR_LIMIT_FLOOR = 0
17
+ /** 上限允许上界(防呆:远超屏幕可容纳数量的上限无意义)。 */
18
+ export const EDITOR_LIMIT_CEIL = 50
19
+
20
+ /**
21
+ * 归一化页签数量上限。
22
+ *
23
+ * 规则:`0` 合法保留(不限制);非数字/非有限/负数 → 回退默认;其余取整后夹到
24
+ * `[EDITOR_LIMIT_FLOOR, EDITOR_LIMIT_CEIL]`。
25
+ * 数字字符串(设置文档/输入框可能给字符串)按数字解析,与 sidebarMin 口径一致。
26
+ *
27
+ * ⚠️ 只接受「真数字」与「非空数字字符串」:不得用裸 `Number(value)` 判值——
28
+ * `Number(null)`、`Number([])`、`Number(false)`、`Number('')` 全是 0,会被误判成
29
+ * 「不限制」;而缺失/损坏的设置必须回退默认(否则一个坏值就永久关掉上限)。
30
+ *
31
+ * @author ddj 2026年09月18号
32
+ * @param value 原始值(设置文档或输入框来的任意 JSON 值)
33
+ * @returns 合法上限(0 = 不限制)
34
+ */
35
+ export function normalizeMaxOpenEditors(value: unknown): number {
36
+ let n: number
37
+ if (typeof value === 'number') {
38
+ n = value
39
+ } else if (typeof value === 'string' && value.trim() !== '') {
40
+ n = Number(value)
41
+ } else {
42
+ return EDITOR_LIMIT_DEFAULT
43
+ }
44
+ if (!Number.isFinite(n) || n < 0) return EDITOR_LIMIT_DEFAULT
45
+ return Math.max(EDITOR_LIMIT_FLOOR, Math.min(EDITOR_LIMIT_CEIL, Math.round(n)))
46
+ }
@@ -14,6 +14,9 @@ export const KEYBINDING_DEFAULTS: Record<string, string> = {
14
14
  'edrv.quickOpen': 'Ctrl+P',
15
15
  'edrv.toggleSidebar': 'Ctrl+B',
16
16
  'edrv.searchInFiles': 'Ctrl+Shift+F',
17
+ // Markdown 预览切换:VS Code 同款 (Ctrl+K V 为分栏,此处取单键 Ctrl+Shift+V)。
18
+ // 仅当活动文件是 .md 时才吞键,其余情况放行给浏览器/输入框(保留「粘贴为纯文本」语义)。
19
+ 'edrv.toggleMarkdownPreview': 'Ctrl+Shift+V',
17
20
  'edrv.navigateBack': 'Alt+ArrowLeft|Ctrl+Alt+-',
18
21
  'edrv.navigateForward': 'Alt+ArrowRight|Ctrl+Shift+-',
19
22
  // 页签循环:主候选避开浏览器保留键(Ctrl+Tab / Ctrl+PgUp/PgDn 会被浏览器截获)
@@ -28,6 +31,13 @@ export const KEYBINDING_DEFAULTS: Record<string, string> = {
28
31
  // 关闭当前页签:VS Code 同款为 Ctrl+W,但浏览器会截获该键(脚本无法 preventDefault),
29
32
  // 故取参考图里的第二候选 Ctrl+F4(Chrome/Edge 默认无行为,可安全拦截)。
30
33
  'edrv.closeTab': 'Ctrl+F4',
34
+ // 调试(VS Code 同款键位;F5 在有调试配置或暂停态才吞键,空闲时放行浏览器刷新)
35
+ 'edrv.debugToggleBreakpoint': 'F9',
36
+ 'edrv.debugStartContinue': 'F5',
37
+ 'edrv.debugStepOver': 'F10',
38
+ 'edrv.debugStepInto': 'F11',
39
+ 'edrv.debugStepOut': 'Shift+F11',
40
+ 'edrv.debugStop': 'Shift+F5',
31
41
  }
32
42
 
33
43
  /**
package/src/shared/rpc.ts CHANGED
@@ -19,7 +19,9 @@ import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules
19
19
  import type { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
20
20
  import type { LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
21
21
  import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
22
- import type { SvnAction, SvnChangeEntry, SvnDiffRevResult, SvnLogEntry, SvnStatusPayload, SvnUpdateResult } from './svn.js'
22
+ import type { SvnAction, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdateResult } from './svn.js'
23
+ import type { DapAction, DapAdapterInfo, DapBreakpointAck, DapBreakpointInput, DapConfigSnippet, DapConfigSource, DapDebugConfig, DapFrameView, DapPhase, DapPollResult, DapProcessInfo, DapScopeView, DapVariableView } from './dap.js'
24
+ import { DAP_ACTIONS } from './dap.js'
23
25
  import type { LoggerLevel } from './logger.js'
24
26
 
25
27
  /** webServer 精确路由。 */
@@ -298,6 +300,24 @@ export interface RpcRequestMap {
298
300
  'svn.cleanup': { sessionId?: string; removeUnversioned?: boolean; removeIgnored?: boolean }
299
301
  'svn.update': { sessionId?: string; path?: string }
300
302
  'svn.tortoise': { sessionId?: string; action: SvnAction; path?: string }
303
+ 'svn.patchText': { sessionId?: string; path: string; whitespace?: 'none' | 'b' | 'w'; ignoreEol?: boolean; unified?: number }
304
+ 'svn.diffSum': { sessionId?: string; path?: string; revA: number; revB: number }
305
+ 'svn.remoteStatus': { sessionId?: string; path?: string }
306
+ 'svn.conflictArtifacts': { sessionId?: string; path: string }
307
+ 'svn.diffLocalPair': { sessionId?: string; left: string; right: string }
308
+ 'svn.fileSizes': { sessionId?: string; paths: string[] }
309
+ 'edrv.dap.configs': { workspacePath: string }
310
+ 'edrv.dap.snippets': {}
311
+ 'edrv.dap.processes': { processName?: string }
312
+ 'edrv.dap.start': { config: DapDebugConfig; workspacePath: string; pid?: number }
313
+ 'edrv.dap.stop': {}
314
+ 'edrv.dap.poll': { since: number }
315
+ 'edrv.dap.setBreakpoints': { workspacePath: string; file: string; points: DapBreakpointInput[] }
316
+ 'edrv.dap.stackTrace': {}
317
+ 'edrv.dap.scopes': { frameId: number }
318
+ 'edrv.dap.variables': { ref: number }
319
+ 'edrv.dap.evaluate': { expression: string; frameId?: number }
320
+ 'edrv.dap.command': { action: DapAction }
301
321
  }
302
322
 
303
323
  export type RpcMethod = keyof RpcRequestMap
@@ -398,7 +418,7 @@ export interface RpcOkMap {
398
418
  'snippets.entries': { entries: SnippetEntry[] }
399
419
  'svn.status': SvnStatusPayload
400
420
  'svn.changes': { wcRoot: string; entries: SvnChangeEntry[]; truncated: boolean }
401
- 'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string }
421
+ 'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string; binary?: boolean; encodingHint?: boolean }
402
422
  'svn.revert': { count: number; summary: string; output: string }
403
423
  'svn.add': { count: number; summary: string; output: string }
404
424
  'svn.log': { entries: SvnLogEntry[]; truncated: boolean; target: string; limit?: number }
@@ -409,6 +429,24 @@ export interface RpcOkMap {
409
429
  'svn.cleanup': { summary: string; output: string }
410
430
  'svn.update': SvnUpdateResult
411
431
  'svn.tortoise': { launched: string }
432
+ 'svn.patchText': { text: string; truncated: boolean; binary: boolean }
433
+ 'svn.diffSum': { entries: SvnSumEntry[]; truncated: boolean }
434
+ 'svn.remoteStatus': { outdated: SvnRemoteOutdatedEntry[]; againstRev: number | null }
435
+ 'svn.conflictArtifacts': { artifacts: SvnConflictArtifact[] }
436
+ 'svn.diffLocalPair': SvnDiffRevResult
437
+ 'svn.fileSizes': { sizes: Array<{ path: string; size: number | null }> }
438
+ 'edrv.dap.configs': { configs: DapDebugConfig[]; adapters: DapAdapterInfo[]; source: DapConfigSource }
439
+ 'edrv.dap.snippets': { snippets: DapConfigSnippet[] }
440
+ 'edrv.dap.processes': { items: DapProcessInfo[] }
441
+ 'edrv.dap.start': { phase: DapPhase }
442
+ 'edrv.dap.stop': object
443
+ 'edrv.dap.poll': DapPollResult
444
+ 'edrv.dap.setBreakpoints': { breakpoints: DapBreakpointAck[] }
445
+ 'edrv.dap.stackTrace': { frames: DapFrameView[] }
446
+ 'edrv.dap.scopes': { scopes: DapScopeView[] }
447
+ 'edrv.dap.variables': { variables: DapVariableView[] }
448
+ 'edrv.dap.evaluate': { result: string; type?: string; ref: number }
449
+ 'edrv.dap.command': object
412
450
  }
413
451
 
414
452
  /** 统一响应:{ok:true, ...payload} 或 {ok:false, error}。 */