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,229 @@
1
+ /**
2
+ * dsh-vscode-mode client — launch.json「添加配置」插入定位与插入文本构造(纯函数,node 可测)。
3
+ * 插入语义对齐 VS Code「添加配置」:光标落在 `"configurations": [` 下一行行首
4
+ * (单行数组落在 `[ 后),新配置插到数组开头;插入文本自带缩进与尾逗号,按纯文本插入
5
+ * (不用 Monaco snippet 语法——${file}/${workspaceFolder} 是 DAP 运行期变量,必须原样落盘)。
6
+ * needComma/缩进从「光标之后的结构」探测:键入过滤词不污染判断(词本身在光标前已被越过)。
7
+ * 作者 ddj 2026年09月22号
8
+ */
9
+ import { DAP_LAUNCH_REL } from '../../shared/dap.js'
10
+
11
+ /** launch.json 键行(容忍单行 `{"configurations": [` 形态;match[0] 恒以 `[ 结尾)。 */
12
+ const CONFIG_KEY_RE = /^\s*\{?\s*"configurations"\s*:\s*\[/
13
+
14
+ /** 调试配置文件路径判定(由共享 DAP_LAUNCH_REL 动态构造防漂移;工作区相对与绝对路径同口径)。 */
15
+ export const LAUNCH_JSON_RE = new RegExp(
16
+ '(^|\\/)' + DAP_LAUNCH_REL.split('/').map((seg) => seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\/') + '$',
17
+ 'i',
18
+ )
19
+
20
+ /** 编辑器光标位置(1-based,Monaco 口径)。 */
21
+ interface CursorPos {
22
+ lineNumber: number
23
+ column: number
24
+ }
25
+
26
+ /** 数组插入定位结果。 */
27
+ export interface ConfigInsertPos {
28
+ /** 插入起始行(1-based;多行数组 = "configurations" 行的下一行)。 */
29
+ line: number
30
+ /** 插入起始列(1-based;多行数组 = 1,单行数组 = `[ 后一列)。 */
31
+ column: number
32
+ /** 新元素行缩进字符串。 */
33
+ indent: string
34
+ /** 缩进单元字符串(' ' / ' ' / '\t',body 重排用)。 */
35
+ unit: string
36
+ /** 插入点后已有元素 → 新元素后需补逗号。 */
37
+ needComma: boolean
38
+ /** 单行数组(光标在 configurations 行内)→ 插入文本首尾自带换行/收尾缩进。 */
39
+ inline: boolean
40
+ /** configurations 行缩进(inline 收尾对齐 `]` 用)。 */
41
+ cfgIndent: string
42
+ }
43
+
44
+ /**
45
+ * 定位 launch.json configurations 数组的插入点。
46
+ * @author ddj 2026年09月22号
47
+ * @param text launch.json 全文(JSONC)
48
+ * @param cursor 可选当前光标(补全触发时传入;needComma/缩进从光标后探测,忽略键入的过滤词)
49
+ * @returns 插入定位;无 configurations 数组返回 null
50
+ */
51
+ export function findArrayPos(text: string, cursor?: CursorPos): ConfigInsertPos | null {
52
+ const lines = text.split('\n')
53
+ const keyLine = lines.findIndex((line) => CONFIG_KEY_RE.test(line))
54
+ if (keyLine < 0) return null
55
+ const cfgLine = lines[keyLine]
56
+ const match = CONFIG_KEY_RE.exec(cfgLine)
57
+ if (!match) return null
58
+ const bracketIdx = match.index + match[0].length - 1
59
+ const cfgIndent = /^[ \t]*/.exec(cfgLine)?.[0] ?? ''
60
+ const lineStarts = lineStartOffsets(lines)
61
+ const eof = keyLine + 1 >= lines.length
62
+ const keyRest = text.slice(lineStarts[keyLine] + bracketIdx + 1, lineStarts[keyLine] + cfgLine.length)
63
+ const inline = eof || firstSignificant(keyRest, 0) !== null
64
+ const scanFrom = resolveScanFrom(lineStarts, lines, cursor, inline, keyLine, bracketIdx)
65
+ const sig = firstSignificant(text, scanFrom)
66
+ const hasContent = sig !== null && sig.char !== ']'
67
+ const elemIndent = sig !== null && !inline ? trailingWsBefore(text, lineStarts, sig.index) : ''
68
+ const unit = unitOf(cfgIndent, inline ? '' : elemIndent)
69
+ const indent = hasContent && elemIndent ? elemIndent : cfgIndent + unit
70
+ if (inline) {
71
+ return {
72
+ line: keyLine + 1,
73
+ column: bracketIdx + 2,
74
+ indent,
75
+ unit,
76
+ needComma: hasContent,
77
+ inline: true,
78
+ cfgIndent,
79
+ }
80
+ }
81
+ return {
82
+ line: keyLine + 2,
83
+ column: 1,
84
+ indent,
85
+ unit,
86
+ needComma: hasContent,
87
+ inline: false,
88
+ cfgIndent,
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 构造插入文本(body 按目标 unit 重排缩进;多行数组首行带 indent、尾随逗号换行;
94
+ * 单行数组首尾补换行并回填 cfgIndent,让原 `] 落到对齐列)。
95
+ * @author ddj 2026年09月22号
96
+ * @param pos 插入定位
97
+ * @param bodyText 片段主体 JSON 文本(host 已 nls 解析,2 空格缩进)
98
+ * @returns 插入到 (line, column) 的完整文本
99
+ */
100
+ export function buildInsertText(pos: ConfigInsertPos, bodyText: string): string {
101
+ const body = reindentBody(bodyText, pos.unit)
102
+ const indented = body.split('\n').map((line) => pos.indent + line).join('\n')
103
+ const head = pos.inline ? '\n' : ''
104
+ const tail = (pos.needComma ? ',' : '') + '\n' + (pos.inline ? pos.cfgIndent : '')
105
+ return head + indented + tail
106
+ }
107
+
108
+ /** 首个有效字符(跳过空白与 // /* 注释;null = 到扫描尾)。 */
109
+ interface SigChar {
110
+ /** 字符在全文(或扫描段)中的偏移。 */
111
+ index: number
112
+ /** 字符本身。 */
113
+ char: string
114
+ }
115
+
116
+ function firstSignificant(text: string, from: number): SigChar | null {
117
+ let i = Math.max(0, from)
118
+ const n = text.length
119
+ while (i < n) {
120
+ const ch = text[i]
121
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
122
+ i += 1
123
+ continue
124
+ }
125
+ if (ch === '/' && text[i + 1] === '/') {
126
+ while (i < n && text[i] !== '\n') i += 1
127
+ continue
128
+ }
129
+ if (ch === '/' && text[i + 1] === '*') {
130
+ i += 2
131
+ while (i + 1 < n && !(text[i] === '*' && text[i + 1] === '/')) i += 1
132
+ i = Math.min(i + 2, n)
133
+ continue
134
+ }
135
+ return { index: i, char: ch }
136
+ }
137
+ return null
138
+ }
139
+
140
+ /** 解析结构扫描起点:有光标(且未越界)从光标起——键入的过滤词在光标前被越过;否则从插入点起。 */
141
+ function resolveScanFrom(
142
+ lineStarts: number[],
143
+ lines: string[],
144
+ cursor: CursorPos | undefined,
145
+ inline: boolean,
146
+ keyLine: number,
147
+ bracketIdx: number,
148
+ ): number {
149
+ const defaultFrom = inline
150
+ ? lineStarts[keyLine] + bracketIdx + 1
151
+ : lineStarts[keyLine + 1] ?? textLen(lineStarts, lines)
152
+ if (!cursor) return defaultFrom
153
+ const lineIdx = cursor.lineNumber - 1
154
+ if (!Number.isInteger(lineIdx) || lineIdx < 0 || lineIdx >= lines.length) return defaultFrom
155
+ const column = Number.isFinite(cursor.column) && cursor.column >= 1 ? Math.floor(cursor.column) : 1
156
+ return lineStarts[lineIdx] + column - 1
157
+ }
158
+
159
+ /** 全文长度(末行起始 + 末行长度;越界兜底 0)。 */
160
+ function textLen(lineStarts: number[], lines: string[]): number {
161
+ const last = lines.length - 1
162
+ return last >= 0 ? (lineStarts[last] ?? 0) + lines[last].length : 0
163
+ }
164
+
165
+ /** 每行起始偏移(含第 0 行 = 0)。 */
166
+ function lineStartOffsets(lines: string[]): number[] {
167
+ const starts: number[] = []
168
+ let offset = 0
169
+ for (const line of lines) {
170
+ starts.push(offset)
171
+ offset += line.length + 1
172
+ }
173
+ return starts
174
+ }
175
+
176
+ /** 偏移 → 行下标(二分;越界取末行)。 */
177
+ function lineIndexOf(lineStarts: number[], offset: number): number {
178
+ let lo = 0
179
+ let hi = lineStarts.length - 1
180
+ while (lo < hi) {
181
+ const mid = Math.ceil((lo + hi) / 2)
182
+ if (lineStarts[mid] <= offset) lo = mid
183
+ else hi = mid - 1
184
+ }
185
+ return lo
186
+ }
187
+
188
+ /** 目标偏移前、其所在行内的末尾空白串(= 该处真实缩进;"Em {" 取 " " 排除过滤词)。 */
189
+ function trailingWsBefore(text: string, lineStarts: number[], offset: number): string {
190
+ const lineStart = lineStarts[lineIndexOf(lineStarts, offset)] ?? 0
191
+ const prefix = text.slice(lineStart, Math.max(lineStart, offset))
192
+ return /[\t ]*$/.exec(prefix)?.[0] ?? ''
193
+ }
194
+
195
+ /**
196
+ * 探测缩进单元:元素行相对 configurations 行的增量优先,回退 configurations 行自身宽度。
197
+ * @author ddj 2026年09月22号
198
+ * @param cfgIndent configurations 行缩进
199
+ * @param elemIndent 首个元素前的真实缩进(无元素线索传空串)
200
+ * @returns 缩进单元字符串(缺省 2 空格)
201
+ */
202
+ function unitOf(cfgIndent: string, elemIndent: string): string {
203
+ if (elemIndent && elemIndent.startsWith(cfgIndent)) {
204
+ const unit = elemIndent.slice(cfgIndent.length)
205
+ if (unit && unit.length <= 8 && /^[\t ]+$/.test(unit)) return unit
206
+ }
207
+ if (cfgIndent.includes('\t')) return '\t'
208
+ if (cfgIndent && /^ +$/.test(cfgIndent) && cfgIndent.length <= 8) return cfgIndent
209
+ return ' '
210
+ }
211
+
212
+ /**
213
+ * body 文本按目标 unit 重排缩进(2 空格基准 → 目标单元)。
214
+ * @author ddj 2026年09月22号
215
+ * @param bodyText JSON.stringify 产物(2 空格缩进)
216
+ * @param unit 目标缩进单元
217
+ * @returns 重排后文本
218
+ */
219
+ function reindentBody(bodyText: string, unit: string): string {
220
+ if (unit === ' ') return bodyText
221
+ return bodyText
222
+ .split('\n')
223
+ .map((line) => {
224
+ const spaces = /^[ ]*/.exec(line)?.[0].length ?? 0
225
+ const depth = Math.floor(spaces / 2)
226
+ return unit.repeat(depth) + line.slice(spaces)
227
+ })
228
+ .join('\n')
229
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * dsh-vscode-mode client — launch.json「添加配置」Monaco 补全 provider。
3
+ * 仅插件专属 `.dsh/launch.json`(DAP_LAUNCH_REL)且光标位于 configurations 数组插入点
4
+ * (或其后键入过滤词)时出条目;
5
+ * insertText = buildInsertText(纯文本插入,不用 InsertAsSnippet——${file}/${workspaceFolder}
6
+ * 是 DAP 运行期变量,snippet 语法会展开吞掉它们)。
7
+ * 数据源 host edrv.dap.snippets,模块级缓存 + TTL,失败静默降级空表;
8
+ * window 挂注销器防热重载重复注册(与 snippets/provider 同模式)。
9
+ * 作者 ddj 2026年09月22号
10
+ */
11
+ import { rpc } from '../rpc.js'
12
+ import { pathOfModel } from '../monaco/lsp/lspClient.js'
13
+ import { buildInsertText, findArrayPos, LAUNCH_JSON_RE } from './launchInsert.js'
14
+ import type { DapConfigSnippet } from '../../shared/dap.js'
15
+
16
+ /** 条目缓存 TTL(毫秒):扩展清单级数据,本地小表。 */
17
+ const CACHE_TTL_MS = 10_000
18
+
19
+ /** provider 注销器全局挂点名(跨 bundle 重载存活,见 snippets/provider 同名说明)。 */
20
+ const PROVIDER_GLOBAL = '__edrvLaunchCfgProvider__'
21
+
22
+ /** 编辑器光标位置(1-based)。 */
23
+ interface SuggestPos {
24
+ lineNumber: number
25
+ column: number
26
+ }
27
+
28
+ /** 最小 model 形状(仅声明用到的链)。 */
29
+ interface CompletionModel {
30
+ getValue(): string
31
+ uri: { scheme?: string; path?: string }
32
+ getWordUntilPosition(position: SuggestPos): { startColumn: number; endColumn: number }
33
+ }
34
+
35
+ /** 补全条目(本模块消费的子集)。 */
36
+ interface CompletionItemLike {
37
+ label: string
38
+ insertText: string
39
+ filterText: string
40
+ detail: string
41
+ sortText: string
42
+ documentation?: string
43
+ kind?: number
44
+ }
45
+
46
+ /** 最小 monaco.languages 形状。 */
47
+ interface MonacoLanguagesLike {
48
+ registerCompletionItemProvider?: (
49
+ selector: string,
50
+ provider: { provideCompletionItems(model: CompletionModel, position: SuggestPos): Promise<{ suggestions: CompletionItemLike[] }> },
51
+ ) => { dispose(): void }
52
+ CompletionItemKind?: { Snippet?: number }
53
+ }
54
+
55
+ /** 最小 monaco 形状。 */
56
+ interface MonacoLike {
57
+ languages?: MonacoLanguagesLike
58
+ }
59
+
60
+ /** 最小 IDisposable 形状。 */
61
+ interface Disposer {
62
+ dispose(): void
63
+ }
64
+
65
+ /** 已加载片段缓存(null = 未加载/已失效)。 */
66
+ let cache: DapConfigSnippet[] | null = null
67
+ let cacheAt = 0
68
+ let pending: Promise<DapConfigSnippet[]> | null = null
69
+ let registered = false
70
+ let disposer: Disposer | null = null
71
+
72
+ // --region 片段数据
73
+
74
+ /**
75
+ * 读取片段(带缓存与在飞去重;失败降级空表,补全静默不打断输入)。
76
+ * @author ddj 2026年09月22号
77
+ * @returns 片段列表
78
+ */
79
+ async function loadSnippets(): Promise<DapConfigSnippet[]> {
80
+ if (cache && Date.now() - cacheAt < CACHE_TTL_MS) return cache
81
+ if (pending) return pending
82
+ pending = rpc('edrv.dap.snippets', {})
83
+ .then((res) => {
84
+ cache = res.ok && Array.isArray(res.snippets) ? res.snippets : []
85
+ cacheAt = Date.now()
86
+ return cache
87
+ })
88
+ .catch(() => {
89
+ cache = []
90
+ cacheAt = Date.now()
91
+ return cache
92
+ })
93
+ .finally(() => {
94
+ pending = null
95
+ })
96
+ return pending
97
+ }
98
+
99
+ /**
100
+ * 预拉片段(按钮点击时提前发起 RPC,suggest 触发即有数据;调用方据此预检可用条数)。
101
+ * @author ddj 2026年09月22号
102
+ * @returns 片段列表(失败空表)
103
+ */
104
+ export function prefetchSnippets(): Promise<DapConfigSnippet[]> {
105
+ return loadSnippets()
106
+ }
107
+
108
+ // --endregion
109
+
110
+ // --region provider 装配
111
+
112
+ /**
113
+ * 注册 launch.json 配置补全 provider(幂等;Monaco 就绪后调用一次)。
114
+ * @author ddj 2026年09月22号
115
+ * @param monaco window.monaco
116
+ */
117
+ export function setupLaunchJson(monaco: MonacoLike): void {
118
+ if (registered || !monaco?.languages?.registerCompletionItemProvider) return
119
+ // 跨重载守卫:上一代 bundle 注册的 provider 仍在存活 window.monaco 上(注销器已落 window)
120
+ const host = typeof window === 'undefined' ? undefined : (window as unknown as Record<string, unknown>)
121
+ if (host && host[PROVIDER_GLOBAL]) {
122
+ registered = true
123
+ return
124
+ }
125
+ const snippetKind = monaco.languages.CompletionItemKind?.Snippet
126
+ const kind = typeof snippetKind === 'number' ? snippetKind : undefined
127
+ registered = true
128
+ disposer = monaco.languages.registerCompletionItemProvider('json', {
129
+ async provideCompletionItems(model, position) {
130
+ return { suggestions: await suggestionsFor(model, position, kind) }
131
+ },
132
+ })
133
+ // 注销器落 window:跨重载认领(见 PROVIDER_GLOBAL 说明)
134
+ if (host) host[PROVIDER_GLOBAL] = disposer
135
+ }
136
+
137
+ /**
138
+ * 卸载:注销 provider 并复位状态(插件热重载/卸载时调用)。
139
+ * 兼容上一代 bundle 遗留的 window 注销器(模块级 disposer 为空时仍能清干净)。
140
+ * @author ddj 2026年09月22号
141
+ */
142
+ export function disposeLaunchJson(): void {
143
+ const host = typeof window === 'undefined' ? undefined : (window as unknown as Record<string, unknown>)
144
+ const target = (disposer && typeof disposer.dispose === 'function' ? disposer : null)
145
+ ?? (host ? (host[PROVIDER_GLOBAL] as Disposer | undefined) ?? null : null)
146
+ if (target && typeof target.dispose === 'function') {
147
+ try {
148
+ target.dispose()
149
+ } catch {
150
+ /* 已注销 */
151
+ }
152
+ }
153
+ if (host) delete host[PROVIDER_GLOBAL]
154
+ disposer = null
155
+ registered = false
156
+ cache = null
157
+ cacheAt = 0
158
+ pending = null
159
+ }
160
+
161
+ // --endregion
162
+
163
+ // --region 补全计算
164
+
165
+ /**
166
+ * 计算当前光标处的配置补全条目(非 launch.json / 非插入点 / 无片段 → 空表)。
167
+ * 光标门槛:行 = 插入行,且过滤词起点恰为插入列(既挡住误触,又允许键入过滤词)。
168
+ * @author ddj 2026年09月22号
169
+ * @param model Monaco 模型
170
+ * @param position 光标位置
171
+ * @param kind Snippet 图标 kind(monaco 枚举缺失时 undefined)
172
+ * @returns 补全条目数组
173
+ */
174
+ async function suggestionsFor(
175
+ model: CompletionModel,
176
+ position: SuggestPos,
177
+ kind: number | undefined,
178
+ ): Promise<CompletionItemLike[]> {
179
+ const path = pathOfModel(model)
180
+ if (!path || !LAUNCH_JSON_RE.test(path)) return []
181
+ const pos = findArrayPos(model.getValue(), position)
182
+ if (!pos || position.lineNumber !== pos.line) return []
183
+ const word = model.getWordUntilPosition(position)
184
+ if (word.startColumn !== pos.column) return []
185
+ const snippets = await loadSnippets()
186
+ // 可用性过滤:不可用适配器的模板不给插入(=== false 严格判断;老 host 缺字段按可用)
187
+ const usable = snippets.filter((snip) => snip.available !== false)
188
+ if (!usable.length) return []
189
+ return usable.map((snip, index) => {
190
+ const item: CompletionItemLike = {
191
+ label: snip.label,
192
+ // 纯文本插入(不设 insertTextRules):DAP 变量 ${file} 等必须原样落盘
193
+ insertText: buildInsertText(pos, snip.bodyText),
194
+ filterText: snip.label + ' ' + snip.type + (snip.description ? ' ' + snip.description : ''),
195
+ detail: snip.description || snip.type,
196
+ // 空格前缀:词补全的 sortText 是词本身('.NET' 的 '.' < '0' 会抢排模板前),
197
+ // 0x20 空格确保配置模板恒排下拉最前并默认高亮
198
+ sortText: ' ' + String(index).padStart(3, '0'),
199
+ documentation: snip.bodyText,
200
+ }
201
+ if (kind !== undefined) item.kind = kind
202
+ return item
203
+ })
204
+ }
205
+
206
+ // --endregion
@@ -0,0 +1,23 @@
1
+ /**
2
+ * dsh-vscode-mode client — Monaco 编辑器 model 路径归一化(纯函数,可单测)。
3
+ * 与 EditorView 原 modelPathOf 逐字同逻辑,提取共享防止"编辑器切换断点"与
4
+ * "调试面板新增断点"两处口径漂移(断点表 key 必须同源)。
5
+ * 作者 ddj 2026年09月21号
6
+ */
7
+
8
+ /** 最小编辑器形状(仅声明用到的 model.uri.path 链)。 */
9
+ export interface EditorPathLike {
10
+ getModel?: () => { uri?: { path?: string } } | null
11
+ }
12
+
13
+ /**
14
+ * 取当前 model 的文件路径(uri.path → 去首斜杠 → decode)。
15
+ * 无 model / 无 uri.path 时返回 null(调用方降级提示,不产生断点)。
16
+ * @author ddj 2026年09月21号
17
+ * @param editor Monaco 编辑器实例(或形状兼容对象)
18
+ * @returns 归一化路径;不可用时 null
19
+ */
20
+ export function editorModelPathOf(editor: unknown): string | null {
21
+ const uriPath = (editor as EditorPathLike | null)?.getModel?.()?.uri?.path
22
+ return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
23
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * dsh-vscode-mode client — 调试面板四段分栏高度(纯函数 + localStorage 持久)。
3
+ * 对齐 VS Code/CodeBuddy splitview 语义:段间 sash 拖动时相邻两段高度重分配
4
+ * (拖下 = 上段增高、下段减矮;任一段触到夹取界时拖动量被"吸收",分隔条停在可行位置)。
5
+ * 存储:`edrv.dap.split.<scope>` → { variables, watch, stack, breakpoints }。
6
+ * 作者 ddj 2026年09月21号
7
+ */
8
+
9
+ /** 分栏段 key(与 DebugPanel section key 一致)。 */
10
+ export const SPLIT_KEYS = ['variables', 'watch', 'stack', 'breakpoints'] as const
11
+ /** 分栏段 key 类型。 */
12
+ export type SplitKey = (typeof SPLIT_KEYS)[number]
13
+
14
+ /** 默认高度(沿用重构前各段 max-height 值,改前改后视觉一致)。 */
15
+ export const DEFAULT_HEIGHTS: Record<SplitKey, number> = { variables: 360, watch: 220, stack: 180, breakpoints: 260 }
16
+ /** 单段最小高度(低于此不可再压缩)。 */
17
+ export const MIN_H = 64
18
+ /** 单段最大高度(防无限拖爆面板)。 */
19
+ export const MAX_H = 1200
20
+
21
+ /** localStorage key 前缀。 */
22
+ const KEY_PREFIX = 'edrv.dap.split.'
23
+
24
+ /**
25
+ * 夹取单段高度到 [MIN_H, MAX_H](非有限数回退最小值)。
26
+ * @author ddj 2026年09月21号
27
+ * @param px 目标高度
28
+ */
29
+ export function clampSectionHeight(px: number): number {
30
+ if (!Number.isFinite(px)) return MIN_H
31
+ return Math.min(MAX_H, Math.max(MIN_H, Math.round(px)))
32
+ }
33
+
34
+ /**
35
+ * 归一化高度表:非对象/缺 key 回退默认,非法值丢弃(损坏安全)。
36
+ * @author ddj 2026年09月21号
37
+ * @param input localStorage 解析结果
38
+ */
39
+ export function sanitizeHeights(input: unknown): Record<SplitKey, number> {
40
+ const out: Record<SplitKey, number> = { ...DEFAULT_HEIGHTS }
41
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return out
42
+ const data = input as Record<string, unknown>
43
+ for (const key of SPLIT_KEYS) {
44
+ const value = data[key]
45
+ if (typeof value === 'number' && Number.isFinite(value)) out[key] = clampSectionHeight(value)
46
+ }
47
+ return out
48
+ }
49
+
50
+ /**
51
+ * 相邻两段高度重分配:above += dy、below −= dy,各自夹取;
52
+ * 一段被夹取吸收的量补给另一段(分隔条停在可行位置,拖动量尽量生效)。
53
+ * @author ddj 2026年09月21号
54
+ * @param heights 拖动起点高度表(不修改入参)
55
+ * @param above 上邻段 key
56
+ * @param below 下邻段 key
57
+ * @param dy 拖动位移(>0 向下拖 = 上段增高)
58
+ * @returns 新高度表
59
+ */
60
+ export function redistribute(
61
+ heights: Record<SplitKey, number>,
62
+ above: SplitKey,
63
+ below: SplitKey,
64
+ dy: number,
65
+ ): Record<SplitKey, number> {
66
+ const clean = Math.round(Number.isFinite(dy) ? dy : 0)
67
+ const next: Record<SplitKey, number> = { ...heights }
68
+ const startAbove = heights[above] ?? MIN_H
69
+ const startBelow = heights[below] ?? MIN_H
70
+ const targetAbove = clampSectionHeight(startAbove + clean)
71
+ const targetBelow = clampSectionHeight(startBelow - clean)
72
+ next[above] = targetAbove
73
+ next[below] = targetBelow
74
+ // 夹取吸收量:>0 表示该段未能吃满 dy,按原方向补给另一段
75
+ const lostAbove = startAbove + clean - targetAbove
76
+ const lostBelow = startBelow - clean - targetBelow
77
+ if (lostAbove > 0) next[below] = clampSectionHeight(targetBelow + lostAbove)
78
+ else if (lostBelow > 0) next[above] = clampSectionHeight(targetAbove + lostBelow)
79
+ return next
80
+ }
81
+
82
+ /**
83
+ * 读高度表(缺失/损坏回退默认值)。
84
+ * @author ddj 2026年09月21号
85
+ * @param scope 状态作用域
86
+ */
87
+ export function loadSectionHeights(scope: string): Record<SplitKey, number> {
88
+ if (!scope) return { ...DEFAULT_HEIGHTS }
89
+ try {
90
+ const raw = localStorage.getItem(KEY_PREFIX + scope)
91
+ return sanitizeHeights(raw ? JSON.parse(raw) : null)
92
+ } catch {
93
+ return { ...DEFAULT_HEIGHTS }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 写高度表(写失败静默;写入前归一化)。
99
+ * @author ddj 2026年09月21号
100
+ * @param scope 状态作用域
101
+ * @param heights 高度表
102
+ */
103
+ export function saveSectionHeights(scope: string, heights: Record<SplitKey, number>): void {
104
+ try { localStorage.setItem(KEY_PREFIX + scope, JSON.stringify(sanitizeHeights(heights))) } catch { /* 忽略 */ }
105
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * dsh-vscode-mode client — 附加目标 pid 决策(纯函数,可单测)。
3
+ * host 侧只对「唯一命中」自动附加;多候选由客户端弹进程选择器(VS Code QuickPick 等价物)。
4
+ * 作者 ddj 2026年09月29号
5
+ */
6
+ import type { DapProcessInfo } from '../../shared/dap.js'
7
+
8
+ /** pid 决策结果。 */
9
+ export type PidPlan =
10
+ | { kind: 'none'; message: string }
11
+ | { kind: 'auto'; pid: number }
12
+ | { kind: 'picker'; items: DapProcessInfo[] }
13
+
14
+ /**
15
+ * 由进程列表决策附加方式:0 命中报错;唯一命中直用;多候选交选择器。
16
+ * @author ddj 2026年09月29号
17
+ * @param items 已按 processName 过滤的进程列表
18
+ * @param processName 过滤词(错误文案用)
19
+ */
20
+ export function resolvePidPlan(items: DapProcessInfo[], processName?: string): PidPlan {
21
+ if (!items.length) return { kind: 'none', message: '未找到匹配进程:' + (processName || '(空)') }
22
+ if (items.length === 1) return { kind: 'auto', pid: items[0].pid }
23
+ return { kind: 'picker', items }
24
+ }