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.
- package/README.md +72 -2
- package/lib/client.js +12082 -6420
- package/lib/client.js.map +1 -1
- package/lib/index.js +2750 -147
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/copyText.ts +28 -0
- package/src/client/dap/BpWidget.ts +262 -0
- package/src/client/dap/bpMenu.ts +91 -0
- package/src/client/dap/bpRowMenu.ts +53 -0
- package/src/client/dap/breakpoints.ts +222 -0
- package/src/client/dap/decorate.ts +128 -0
- package/src/client/dap/hintLine.ts +29 -0
- package/src/client/dap/hover.ts +166 -0
- package/src/client/dap/hoverMode.ts +159 -0
- package/src/client/dap/hoverTree.ts +722 -0
- package/src/client/dap/launchInsert.ts +229 -0
- package/src/client/dap/launchSnippetProvider.ts +206 -0
- package/src/client/dap/modelPath.ts +23 -0
- package/src/client/dap/panelSplit.ts +105 -0
- package/src/client/dap/pidPick.ts +24 -0
- package/src/client/dap/store.ts +571 -0
- package/src/client/dap/toolbarDrag.ts +51 -0
- package/src/client/dap/trace.ts +22 -0
- package/src/client/dap/variableTree.ts +62 -0
- package/src/client/fileClipboard.ts +46 -0
- package/src/client/index.ts +12 -3
- package/src/client/monaco/lsp/index.ts +15 -0
- package/src/client/monaco/lsp/providers.ts +2 -0
- package/src/client/monaco/theme.ts +15 -0
- package/src/client/openWithDialog.ts +66 -0
- package/src/client/searchSeed.ts +36 -0
- package/src/client/sidebar/contextMenu.ts +51 -10
- package/src/client/sidebar/menuItems.ts +473 -50
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SearchPanel.ts +24 -3
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +8 -0
- package/src/client/styles/editor.css +176 -0
- package/src/client/svnActions.ts +18 -1
- package/src/client/svnLog.ts +3 -3
- package/src/client/svnStatus.ts +140 -2
- package/src/client/tabActions.ts +22 -10
- package/src/client/ui/EditorView.ts +625 -23
- package/src/client/ui/OfficialSideTab.ts +1 -0
- package/src/client/ui/PromptDialog.ts +63 -0
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/client/ui/SvnDiffPanel.ts +21 -8
- package/src/client/ui/SvnLogDialog.ts +44 -2
- package/src/client/ui/SvnPatchDialog.ts +63 -0
- package/src/client/ui/SvnSumDialog.ts +77 -0
- package/src/client/ui/commandCatalog.ts +63 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/dap/configSnippets.ts +269 -0
- package/src/dap/discovery.ts +198 -0
- package/src/dap/launchConfig.ts +127 -0
- package/src/dap/manager.ts +588 -0
- package/src/dap/processList.ts +76 -0
- package/src/dap/protocol.ts +80 -0
- package/src/dap/provider.ts +49 -0
- package/src/dap/rpc.ts +192 -0
- package/src/dap/sourcePath.ts +82 -0
- package/src/index.ts +7 -3
- package/src/lsp/providers.ts +3 -2
- package/src/reveal.ts +31 -20
- package/src/rpc.ts +209 -5
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/fsNames.ts +115 -0
- package/src/shared/keybindings.ts +9 -0
- package/src/shared/rpc.ts +52 -2
- package/src/shared/svn.ts +147 -0
- package/src/shared/svnActions.ts +20 -0
- package/src/svn.ts +320 -11
- 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
|
+
}
|