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,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,115 @@
1
+ /**
2
+ * dsh-vscode-mode shared — 文件操作名称/路径校验(纯函数,可单测)。
3
+ * 新建/重命名输入护栏(客户端弹窗校验与 host 端二次校验共用一份):
4
+ * 空名、绝对路径、盘符、'.'/'..' 路径段、空路径段、控制字符、超长一律拒绝;
5
+ * 新建允许 a/b.c 嵌套段(对齐 VS Code「新建文件…」输入语义),重命名仅允许单个名称段。
6
+ * 作者 ddj 2026年09月22号
7
+ */
8
+
9
+ /** 单个名称段最大长度(防误贴超长串直接打到文件系统)。 */
10
+ export const NAME_MAX = 255
11
+
12
+ /**
13
+ * 路径归一:去首尾空白 + 反斜杠转 '/'(Windows 输入等价处理)。
14
+ * @author ddj 2026年09月22号
15
+ * @param raw 原始输入
16
+ * @returns 归一后的字符串
17
+ */
18
+ function normPath(raw: unknown): string {
19
+ return String(raw ?? '').trim().replace(/\\/g, '/')
20
+ }
21
+
22
+ /**
23
+ * 单个名称段的通用违规检查(两套校验共用)。
24
+ * @author ddj 2026年09月22号
25
+ * @param seg 名称段
26
+ * @returns 错误文案;null = 通过
27
+ */
28
+ function checkSegment(seg: string): string | null {
29
+ if (!seg) return '名称包含空路径段'
30
+ if (seg === '.' || seg === '..') return '名称不能包含 . 或 .. 路径段'
31
+ if (seg.length > NAME_MAX) return '名称过长(单段超过 ' + NAME_MAX + ' 字符)'
32
+ if (/[\0-\x1f]/.test(seg)) return '名称包含非法控制字符'
33
+ return null
34
+ }
35
+
36
+ /**
37
+ * 校验新建文件/文件夹的相对路径(允许 a/b.c 嵌套段)。
38
+ * @author ddj 2026年09月22号
39
+ * @param raw 用户输入的名称或相对路径
40
+ * @returns 错误文案;null = 校验通过
41
+ */
42
+ export function checkNewName(raw: unknown): string | null {
43
+ const text = normPath(raw)
44
+ if (!text) return '名称不能为空'
45
+ if (text.startsWith('/')) return '名称不能是绝对路径'
46
+ if (/^[a-zA-Z]:/.test(text)) return '名称不能带盘符'
47
+ for (const seg of text.split('/')) {
48
+ const bad = checkSegment(seg)
49
+ if (bad) return bad
50
+ }
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * 校验重命名的新名称(仅单个名称段,不接受路径)。
56
+ * @author ddj 2026年09月22号
57
+ * @param raw 用户输入的新名称
58
+ * @returns 错误文案;null = 校验通过
59
+ */
60
+ export function checkRenameName(raw: unknown): string | null {
61
+ const text = normPath(raw)
62
+ if (!text) return '名称不能为空'
63
+ if (text.includes('/')) return '新名称不能包含路径分隔符'
64
+ return checkSegment(text)
65
+ }
66
+
67
+ /**
68
+ * 拼接工作区相对路径(dir 为空 = 根;name 新建时可含多段)。
69
+ * @author ddj 2026年09月22号
70
+ * @param dir 目标目录(工作区相对)
71
+ * @param name 名称
72
+ * @returns 拼接后的相对路径
73
+ */
74
+ export function joinRelPath(dir: string, name: string): string {
75
+ const base = normPath(dir).replace(/\/+$/, '')
76
+ const add = normPath(name).replace(/^\/+/, '')
77
+ return base ? base + '/' + add : add
78
+ }
79
+
80
+ /**
81
+ * 取相对路径的父目录(顶层条目返回空串 = 根)。
82
+ * @author ddj 2026年09月22号
83
+ * @param path 工作区相对路径
84
+ * @returns 父目录相对路径
85
+ */
86
+ export function parentRelOf(path: string): string {
87
+ const parts = normPath(path).split('/').filter(Boolean)
88
+ parts.pop()
89
+ return parts.join('/')
90
+ }
91
+
92
+ /**
93
+ * 取相对路径的名称段(末段)。
94
+ * @author ddj 2026年09月22号
95
+ * @param path 工作区相对路径
96
+ * @returns 末段名称
97
+ */
98
+ export function baseNameOf(path: string): string {
99
+ const parts = normPath(path).split('/').filter(Boolean)
100
+ return parts.pop() ?? ''
101
+ }
102
+
103
+ /**
104
+ * other 是否为 base 本身或其内部路径(复制/移动「不能以自身或其子目录为目标」护栏)。
105
+ * @author ddj 2026年09月22号
106
+ * @param base 基准相对路径(源)
107
+ * @param other 待判定相对路径(目标目录)
108
+ * @returns 是否为自身或子路径
109
+ */
110
+ export function isSubPath(base: string, other: string): boolean {
111
+ const b = normPath(base).replace(/\/+$/, '')
112
+ const o = normPath(other).replace(/\/+$/, '')
113
+ if (!b) return false
114
+ return o === b || o.startsWith(b + '/')
115
+ }
@@ -22,6 +22,8 @@ export const KEYBINDING_DEFAULTS: Record<string, string> = {
22
22
  // 页签循环:主候选避开浏览器保留键(Ctrl+Tab / Ctrl+PgUp/PgDn 会被浏览器截获)
23
23
  'edrv.nextTab': 'Ctrl+Alt+ArrowRight|Ctrl+PageDown',
24
24
  'edrv.prevTab': 'Ctrl+Alt+ArrowLeft|Ctrl+PageUp',
25
+ // 转到行:插件只补键位与命令栏入口,widget 本体转发 Monaco 原生 editor.action.gotoLine(VS Code 同款 Ctrl+G)
26
+ 'edrv.goToLine': 'Ctrl+G',
25
27
  // 命令栏(Ctrl+Shift+P 主候选;F1 为 VS Code 同款第二候选)与编辑行导航
26
28
  'edrv.showCommands': 'Ctrl+Shift+P|F1',
27
29
  'edrv.nextEditorRow': 'Ctrl+Alt+ArrowDown',
@@ -31,6 +33,13 @@ export const KEYBINDING_DEFAULTS: Record<string, string> = {
31
33
  // 关闭当前页签:VS Code 同款为 Ctrl+W,但浏览器会截获该键(脚本无法 preventDefault),
32
34
  // 故取参考图里的第二候选 Ctrl+F4(Chrome/Edge 默认无行为,可安全拦截)。
33
35
  'edrv.closeTab': 'Ctrl+F4',
36
+ // 调试(VS Code 同款键位;F5 在有调试配置或暂停态才吞键,空闲时放行浏览器刷新)
37
+ 'edrv.debugToggleBreakpoint': 'F9',
38
+ 'edrv.debugStartContinue': 'F5',
39
+ 'edrv.debugStepOver': 'F10',
40
+ 'edrv.debugStepInto': 'F11',
41
+ 'edrv.debugStepOut': 'Shift+F11',
42
+ 'edrv.debugStop': 'Shift+F5',
34
43
  }
35
44
 
36
45
  /**
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 精确路由。 */
@@ -214,6 +216,12 @@ export interface RpcRequestMap {
214
216
  'edrv.searchContent': { sessionId?: string; query: string; matchCase?: boolean; wholeWord?: boolean; regex?: boolean; maxResults?: number; include?: string[]; exclude?: string[] }
215
217
  'edrv.listDir': { sessionId?: string; path: string; force?: boolean }
216
218
  'edrv.revealInExplorer': { sessionId?: string; path: string }
219
+ 'edrv.fsCreateFile': { sessionId?: string; path: string }
220
+ 'edrv.fsCreateDir': { sessionId?: string; path: string }
221
+ 'edrv.fsRename': { sessionId?: string; path: string; newName: string }
222
+ 'edrv.fsDelete': { sessionId?: string; path: string }
223
+ 'edrv.fsCopy': { sessionId?: string; from: string; toDir: string }
224
+ 'edrv.fsMove': { sessionId?: string; from: string; toDir: string }
217
225
  'mcp.list': {}
218
226
  'mcp.save': { config: MpcConfig }
219
227
  'mcp.remove': { id: string }
@@ -298,6 +306,24 @@ export interface RpcRequestMap {
298
306
  'svn.cleanup': { sessionId?: string; removeUnversioned?: boolean; removeIgnored?: boolean }
299
307
  'svn.update': { sessionId?: string; path?: string }
300
308
  'svn.tortoise': { sessionId?: string; action: SvnAction; path?: string }
309
+ 'svn.patchText': { sessionId?: string; path: string; whitespace?: 'none' | 'b' | 'w'; ignoreEol?: boolean; unified?: number }
310
+ 'svn.diffSum': { sessionId?: string; path?: string; revA: number; revB: number }
311
+ 'svn.remoteStatus': { sessionId?: string; path?: string }
312
+ 'svn.conflictArtifacts': { sessionId?: string; path: string }
313
+ 'svn.diffLocalPair': { sessionId?: string; left: string; right: string }
314
+ 'svn.fileSizes': { sessionId?: string; paths: string[] }
315
+ 'edrv.dap.configs': { workspacePath: string }
316
+ 'edrv.dap.snippets': {}
317
+ 'edrv.dap.processes': { processName?: string }
318
+ 'edrv.dap.start': { config: DapDebugConfig; workspacePath: string; pid?: number }
319
+ 'edrv.dap.stop': {}
320
+ 'edrv.dap.poll': { since: number }
321
+ 'edrv.dap.setBreakpoints': { workspacePath: string; file: string; points: DapBreakpointInput[] }
322
+ 'edrv.dap.stackTrace': {}
323
+ 'edrv.dap.scopes': { frameId: number }
324
+ 'edrv.dap.variables': { ref: number }
325
+ 'edrv.dap.evaluate': { expression: string; frameId?: number }
326
+ 'edrv.dap.command': { action: DapAction }
301
327
  }
302
328
 
303
329
  export type RpcMethod = keyof RpcRequestMap
@@ -325,6 +351,12 @@ export interface RpcOkMap {
325
351
  'edrv.searchContent': { matches: SearchContentMatch[]; truncated: boolean; warning?: string }
326
352
  'edrv.listDir': { root: string; path: string; entries: TreeEntry[] }
327
353
  'edrv.revealInExplorer': { revealed: string }
354
+ 'edrv.fsCreateFile': { path: string }
355
+ 'edrv.fsCreateDir': { path: string }
356
+ 'edrv.fsRename': { from: string; to: string }
357
+ 'edrv.fsDelete': { path: string }
358
+ 'edrv.fsCopy': { from: string; to: string }
359
+ 'edrv.fsMove': { from: string; to: string }
328
360
  'mcp.list': { servers: MpcServer[] }
329
361
  'mcp.save': { server: MpcServer }
330
362
  'mcp.remove': object
@@ -398,7 +430,7 @@ export interface RpcOkMap {
398
430
  'snippets.entries': { entries: SnippetEntry[] }
399
431
  'svn.status': SvnStatusPayload
400
432
  'svn.changes': { wcRoot: string; entries: SvnChangeEntry[]; truncated: boolean }
401
- 'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string }
433
+ 'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string; binary?: boolean; encodingHint?: boolean }
402
434
  'svn.revert': { count: number; summary: string; output: string }
403
435
  'svn.add': { count: number; summary: string; output: string }
404
436
  'svn.log': { entries: SvnLogEntry[]; truncated: boolean; target: string; limit?: number }
@@ -409,6 +441,24 @@ export interface RpcOkMap {
409
441
  'svn.cleanup': { summary: string; output: string }
410
442
  'svn.update': SvnUpdateResult
411
443
  'svn.tortoise': { launched: string }
444
+ 'svn.patchText': { text: string; truncated: boolean; binary: boolean }
445
+ 'svn.diffSum': { entries: SvnSumEntry[]; truncated: boolean }
446
+ 'svn.remoteStatus': { outdated: SvnRemoteOutdatedEntry[]; againstRev: number | null }
447
+ 'svn.conflictArtifacts': { artifacts: SvnConflictArtifact[] }
448
+ 'svn.diffLocalPair': SvnDiffRevResult
449
+ 'svn.fileSizes': { sizes: Array<{ path: string; size: number | null }> }
450
+ 'edrv.dap.configs': { configs: DapDebugConfig[]; adapters: DapAdapterInfo[]; source: DapConfigSource }
451
+ 'edrv.dap.snippets': { snippets: DapConfigSnippet[] }
452
+ 'edrv.dap.processes': { items: DapProcessInfo[] }
453
+ 'edrv.dap.start': { phase: DapPhase }
454
+ 'edrv.dap.stop': object
455
+ 'edrv.dap.poll': DapPollResult
456
+ 'edrv.dap.setBreakpoints': { breakpoints: DapBreakpointAck[] }
457
+ 'edrv.dap.stackTrace': { frames: DapFrameView[] }
458
+ 'edrv.dap.scopes': { scopes: DapScopeView[] }
459
+ 'edrv.dap.variables': { variables: DapVariableView[] }
460
+ 'edrv.dap.evaluate': { result: string; type?: string; ref: number }
461
+ 'edrv.dap.command': object
412
462
  }
413
463
 
414
464
  /** 统一响应:{ok:true, ...payload} 或 {ok:false, error}。 */
package/src/shared/svn.ts CHANGED
@@ -189,6 +189,10 @@ export interface SvnDiffRevResult {
189
189
  reason?: string
190
190
  /** 失败提示(诊断用)。 */
191
191
  error?: string
192
+ /** W2-6 护栏:内容含 NUL 字节(二进制嫌疑),客户端不渲染编辑器。 */
193
+ binary?: boolean
194
+ /** W2-6 护栏:嗅探窗含 U+FFFD(可能非 UTF-8),客户端提示乱码风险。 */
195
+ encodingHint?: boolean
192
196
  }
193
197
 
194
198
  // --endregion
@@ -281,6 +285,31 @@ export interface SvnChangeFilter {
281
285
  unversioned?: boolean
282
286
  /** 是否显示已被忽略的条目(默认 false)。 */
283
287
  ignored?: boolean
288
+ /**
289
+ * 名称过滤串(W1-2,纯前端):`*`/`?` 通配或子串包含,忽略大小写;空/空白 = 不过滤。
290
+ * 只影响列表展示;「全部加入/全部还原」等批量动作仍按未过滤集合计算(防误伤)。
291
+ */
292
+ name?: string
293
+ }
294
+
295
+ /**
296
+ * 路径名称过滤匹配(W1-2):`*` 任意串、`?` 单字符(忽略大小写);
297
+ * 无通配符时回落子串包含。非法正则元字符已转义,永不抛错。
298
+ * @author ddj 2026年09月20号
299
+ * @param path 条目路径(`/` 分隔)
300
+ * @param pattern 过滤串(空/空白 = 不过滤)
301
+ * @returns 是否命中
302
+ */
303
+ export function changeNameMatch(path: string, pattern: string | undefined): boolean {
304
+ const query = String(pattern ?? '').trim().toLowerCase()
305
+ if (!query) return true
306
+ const target = String(path ?? '').toLowerCase()
307
+ if (!query.includes('*') && !query.includes('?')) return target.includes(query)
308
+ const source = query
309
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
310
+ .replaceAll('*', '.*')
311
+ .replaceAll('?', '.')
312
+ return new RegExp('^' + source + '$').test(target)
284
313
  }
285
314
 
286
315
  /**
@@ -297,6 +326,7 @@ export function svnVisibleChanges(
297
326
  const showUnversioned = filter.unversioned !== false
298
327
  const showIgnored = filter.ignored === true
299
328
  return entries.filter((entry) => {
329
+ if (!changeNameMatch(entry.path, filter.name)) return false
300
330
  if (entry.status === 'ignored') return showIgnored
301
331
  if (entry.status === 'unversioned') return showUnversioned
302
332
  return isSvnChange(entry)
@@ -375,3 +405,120 @@ export const SVN_TORTOISE_LABELS: Record<SvnAction, string> = {
375
405
  blame: 'TortoiseSVN 追溯',
376
406
  revert: 'TortoiseSVN 还原',
377
407
  }
408
+
409
+ // --region W2 差异管理补全(护栏/目录对比/远端检查/冲突/配对)
410
+
411
+ /** W2-6 内容护栏嗅探窗(字符数;NUL/U+FFFD 只看前 8K,全量扫描大文本得不偿失)。 */
412
+ export const GUARD_SNIFF_CHARS = 8 * 1024
413
+
414
+ /** W2-6 内容级护栏标记(host 嗅探 → client 提示)。 */
415
+ export interface SvnContentGuard {
416
+ /** 嗅探窗含 NUL 字节(二进制嫌疑;M4 实测 svn cat 对二进制原样透传字节)。 */
417
+ binary?: boolean
418
+ /** 嗅探窗含 U+FFFD(UTF-8 解码失败替换符;可能非 UTF-8 编码)。 */
419
+ encodingHint?: boolean
420
+ }
421
+
422
+ /**
423
+ * 文本级内容护栏(W2-6,纯函数):对喂给差异视图/补丁的文本做低成本嗅探。
424
+ * 二进制锚点取 NUL 字节(比 svn:mime-type 更兜底——mime 缺省的「伪文本」二进制也拦得住);
425
+ * 编码提示取 U+FFFD(host fs.readText 按 UTF-8 解码,无效序列会变成替换符)。
426
+ * @author ddj 2026年09月20号
427
+ * @param text 待嗅探文本(null/undefined 按空处理)
428
+ * @returns 护栏标记(干净内容返回空对象)
429
+ */
430
+ export function contentGuardOf(text: string | null | undefined): SvnContentGuard {
431
+ if (typeof text !== 'string' || !text) return {}
432
+ const window = text.slice(0, GUARD_SNIFF_CHARS)
433
+ if (window.includes('\u0000')) return { binary: true }
434
+ if (window.includes('\uFFFD')) return { encodingHint: true }
435
+ return {}
436
+ }
437
+
438
+ /** W2-5 一对「疑似改名」配对(missing `!` ↔ unversioned `?`)。 */
439
+ export interface SvnRenamePair {
440
+ /** 原路径(missing 条目)。 */
441
+ missingPath: string
442
+ /** 新路径(unversioned 条目)。 */
443
+ unversionedPath: string
444
+ }
445
+
446
+ /** 取路径 basename(`/`/`\` 通用;纯函数)。 */
447
+ function baseNameOf(path: string): string {
448
+ return String(path ?? '').split(/[\\/]/).pop() ?? ''
449
+ }
450
+
451
+ /**
452
+ * 疑似改名配对(W2-5,纯函数;M2 定论:status XML 无 copyfrom,只能客户端启发式)。
453
+ * 配对条件从严:basename 相同 **且** 双侧 size 均已知且相等(size 缺失不配对,防误报)。
454
+ * @author ddj 2026年09月20号
455
+ * @param entries 变更清单
456
+ * @param sizes 路径 → 字节大小(未知为 null/缺失)
457
+ * @returns 配对数组(每个 missing 至多配一个,先到先得保持输入顺序)
458
+ */
459
+ export function pairMissingWithUnversioned(
460
+ entries: readonly SvnChangeEntry[],
461
+ sizes: Record<string, number | null>,
462
+ ): SvnRenamePair[] {
463
+ const unversioned = entries.filter((entry) => entry.status === 'unversioned')
464
+ const pairs: SvnRenamePair[] = []
465
+ for (const missing of entries) {
466
+ if (missing.status !== 'missing') continue
467
+ const name = baseNameOf(missing.path)
468
+ for (const candidate of unversioned) {
469
+ if (baseNameOf(candidate.path) !== name) continue
470
+ const a = sizes[missing.path]
471
+ const b = sizes[candidate.path]
472
+ if (typeof a !== 'number' || typeof b !== 'number' || a !== b) continue
473
+ pairs.push({ missingPath: missing.path, unversionedPath: candidate.path })
474
+ break
475
+ }
476
+ }
477
+ return pairs
478
+ }
479
+
480
+ /** W2-2 目录对比单条(`svn diff --summarize --xml` 的 `<path>`;M5 实测形态)。 */
481
+ export interface SvnSumEntry {
482
+ /** 工作副本相对路径(host 已从绝对路径归一)。 */
483
+ path: string
484
+ /** 变更动作(svn 词汇:added/deleted/replaced/modified/normal/none)。 */
485
+ item: string
486
+ /** 节点类型(file/dir;异常输出回 'none')。 */
487
+ kind: string
488
+ /** 属性变更(none/modified)。 */
489
+ props: string
490
+ }
491
+
492
+ /** W2-3 远端落后条目(`svn status -u --xml` 的 repos-status;M3 实测形态)。 */
493
+ export interface SvnRemoteOutdatedEntry {
494
+ /** 工作副本相对路径。 */
495
+ path: string
496
+ /** 远端状态(实测落后文件为 modified;'none' = 远端无变化)。 */
497
+ item: string
498
+ }
499
+
500
+ /** W2-2 summarize 动作 → 展示字母(对齐 svn status 首列字母;normal/none 不出徽标)。 */
501
+ export const SVN_SUM_LETTER: Record<string, string> = {
502
+ added: 'A',
503
+ deleted: 'D',
504
+ replaced: 'R',
505
+ modified: 'M',
506
+ normal: ' ',
507
+ none: ' ',
508
+ 'props-deleted': ' ',
509
+ 'props-modified': ' ',
510
+ }
511
+
512
+ /** W2-4 冲突副本文件(`.mine`/`.working`/`.rN`)。 */
513
+ export interface SvnConflictArtifact {
514
+ /** 工作副本相对路径。 */
515
+ path: string
516
+ /** 文件名(含冲突后缀)。 */
517
+ name: string
518
+ /** 种类:mine = 我的(.mine)/ working = 基础工作版(.working)/ rev = 仓库版本(.rN)。 */
519
+ kind: 'mine' | 'working' | 'rev'
520
+ /** rev 种类时的修订号。 */
521
+ rev?: number
522
+ }
523
+
524
+ // --endregion