dsh-vscode-mode 0.5.3 → 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.
- package/README.md +55 -2
- package/lib/client.js +5173 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +2356 -126
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- 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/index.ts +9 -0
- 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/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +2 -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/ui/EditorView.ts +544 -16
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- 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 +49 -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 +11 -3
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/keybindings.ts +7 -0
- package/src/shared/rpc.ts +40 -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
|
@@ -58,13 +58,15 @@ import { onLspProgress, refreshStatus, setSession as setLspSession } from '../mo
|
|
|
58
58
|
import { setupAiInline, trackAiEditor, aiInlineEnabled } from '../ai/inlineProvider.js'
|
|
59
59
|
import { SnippetsPicker } from './SnippetsPicker.js'
|
|
60
60
|
import { invalidateSnippets, setSnippetsSession, setupSnippets } from '../snippets/provider.js'
|
|
61
|
+
import { setupLaunchJson, prefetchSnippets } from '../dap/launchSnippetProvider.js'
|
|
62
|
+
import { findArrayPos, LAUNCH_JSON_RE } from '../dap/launchInsert.js'
|
|
61
63
|
import {
|
|
62
64
|
absoluteOf, ancestorDirsOf, applyClose, baseNameOf, closeAll, closeOthers, closeRight, closeSaved,
|
|
63
65
|
evictPlan, insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
|
|
64
66
|
} from '../tabActions.js'
|
|
65
67
|
import { getMaxOpenEditors } from '../editorLimit.js'
|
|
66
68
|
import { buildTabMenu } from '../tabMenu.js'
|
|
67
|
-
import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
|
|
69
|
+
import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnDiffLocalPair, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
|
|
68
70
|
import { ensureSvnLog, getSvnLog, refreshSvnLog, svnDiffPair, svnDiffRev, svnDiffWorking, svnLogKeyOf, svnLogLoadedLimit, svnLogTruncated, svnWcRev, SVN_LOG_SHOW_ALL_LIMIT } from '../svnLog.js'
|
|
69
71
|
import { runSvnAction } from '../svnActions.js'
|
|
70
72
|
import { dshTrace } from '../svnStore.js'
|
|
@@ -74,7 +76,18 @@ import type { SvnAction } from '../../shared/svn.js'
|
|
|
74
76
|
import { createSaveTimer } from '../saveDebounce.js'
|
|
75
77
|
import { ContextMenu } from './ContextMenu.js'
|
|
76
78
|
import { SvnLogDialog } from './SvnLogDialog.js'
|
|
79
|
+
import { SvnPatchDialog } from './SvnPatchDialog.js'
|
|
80
|
+
import { SvnSumDialog } from './SvnSumDialog.js'
|
|
77
81
|
import { LogDialog } from './LogDialog.js'
|
|
82
|
+
import { dapStore } from '../dap/store.js'
|
|
83
|
+
import { bpDisabledLinesOf, bpLinesOf } from '../dap/breakpoints.js'
|
|
84
|
+
import { applyBpDecorations, setEditingLine, setHintLine, setPausedLine } from '../dap/decorate.js'
|
|
85
|
+
import { hintLineFor } from '../dap/hintLine.js'
|
|
86
|
+
import { createBpWidget } from '../dap/BpWidget.js'
|
|
87
|
+
import { buildBpMenu } from '../dap/bpMenu.js'
|
|
88
|
+
import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '../dap/toolbarDrag.js'
|
|
89
|
+
import { editorModelPathOf } from '../dap/modelPath.js'
|
|
90
|
+
import { dapTrace } from '../dap/trace.js'
|
|
78
91
|
|
|
79
92
|
/** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
|
|
80
93
|
const SVN_LOG_PAGE = 100
|
|
@@ -90,6 +103,18 @@ function svnLogOptsOf(svnLog) {
|
|
|
90
103
|
return { stopOnCopy: svnLog.soc === true, showMerged: svnLog.merged === true, range: svnLog.range ?? null }
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
/**
|
|
107
|
+
* 工作区相对路径 → 所在目录相对路径(W2-2 目录对比的默认目标;无目录 = 工作副本根)。
|
|
108
|
+
* @author ddj 2026年09月20号
|
|
109
|
+
* @param relPath 工作区相对路径(可空)
|
|
110
|
+
* @returns 目录相对路径('' = 工作副本根)
|
|
111
|
+
*/
|
|
112
|
+
function dirOfRel(relPath) {
|
|
113
|
+
const value = String(relPath ?? '').replace(/\\/g, '/')
|
|
114
|
+
const cut = value.lastIndexOf('/')
|
|
115
|
+
return cut > 0 ? value.slice(0, cut) : ''
|
|
116
|
+
}
|
|
117
|
+
|
|
93
118
|
/** 跳转目标高亮的保留时长(LSP/搜索跳转落地后给用户的位置提示,到期自动清除)。 */
|
|
94
119
|
const NAV_FLASH_MS = 1200
|
|
95
120
|
|
|
@@ -194,7 +219,9 @@ export function EditorView(props) {
|
|
|
194
219
|
const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
|
|
195
220
|
const [svnStatus, setSvnStatus] = React.useState(null) // SVN 能力状态(SvnStatusPayload;null=未加载,SVN 入口隐藏)
|
|
196
221
|
const [svnChanges, setSvnChanges] = React.useState(null) // SVN 变更清单(null=未加载;徽标/菜单判定)
|
|
197
|
-
const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme }(null
|
|
222
|
+
const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme, logTarget?, binary?, encodingHint? }(null=关闭;logTarget 存在 = 来自日志弹窗,差异条可返回日志)
|
|
223
|
+
const [patchDialog, setPatchDialog] = React.useState(null) // W2-1 补丁对话框 { path, text, truncated, binary }(null = 关闭)
|
|
224
|
+
const [sumDialog, setSumDialog] = React.useState(null) // W2-2 目录对比对话框 { path }(null = 关闭)
|
|
198
225
|
const [svnLog, setSvnLog] = React.useState(null) // 日志弹窗 { target, limit, soc?, range? }(null=关闭;P1-4/P1-6 增选项维度)
|
|
199
226
|
const [svnLogData, setSvnLogData] = React.useState(null) // 日志条目(null=未加载)
|
|
200
227
|
const [svnLogBusy, setSvnLogBusy] = React.useState(false) // 日志在途
|
|
@@ -262,6 +289,82 @@ export function EditorView(props) {
|
|
|
262
289
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
263
290
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
264
291
|
const svnMenuDisposersRef = React.useRef([]) // Monaco 右键 SVN 组当前 disposables(同步时先注销旧组)
|
|
292
|
+
// 调试(DAP):store 订阅 tick 驱动重渲 + gutter/F9 切换闭包(ensureEditor 空依赖读取)
|
|
293
|
+
const [dapTick, setDapTick] = React.useState(0)
|
|
294
|
+
React.useEffect(() => dapStore.subscribe(() => setDapTick((t) => t + 1)), [])
|
|
295
|
+
const dapSnap = dapStore.getSnapshot()
|
|
296
|
+
const [dapBarPos, setDapBarPos] = React.useState(() => loadToolbarPosition(scope))
|
|
297
|
+
const [dapBarDragging, setDapBarDragging] = React.useState(false)
|
|
298
|
+
const dapBarRef = React.useRef(null)
|
|
299
|
+
const dapDragRef = React.useRef(null)
|
|
300
|
+
React.useEffect(() => {
|
|
301
|
+
setDapBarPos(loadToolbarPosition(scope))
|
|
302
|
+
}, [scope])
|
|
303
|
+
React.useEffect(() => {
|
|
304
|
+
const onMove = (event) => {
|
|
305
|
+
const drag = dapDragRef.current
|
|
306
|
+
const bar = dapBarRef.current
|
|
307
|
+
const area = bar?.parentElement
|
|
308
|
+
if (!drag || !bar || !area) return
|
|
309
|
+
const areaRect = area.getBoundingClientRect()
|
|
310
|
+
const barRect = bar.getBoundingClientRect()
|
|
311
|
+
const next = clampToolbarPosition({
|
|
312
|
+
left: event.clientX - areaRect.left - drag.offsetX,
|
|
313
|
+
top: event.clientY - areaRect.top - drag.offsetY,
|
|
314
|
+
}, {
|
|
315
|
+
width: areaRect.width,
|
|
316
|
+
height: areaRect.height,
|
|
317
|
+
barWidth: barRect.width,
|
|
318
|
+
barHeight: barRect.height,
|
|
319
|
+
})
|
|
320
|
+
setDapBarPos(next)
|
|
321
|
+
}
|
|
322
|
+
const onUp = () => {
|
|
323
|
+
if (!dapDragRef.current) return
|
|
324
|
+
dapDragRef.current = null
|
|
325
|
+
setDapBarDragging(false)
|
|
326
|
+
const latest = dapBarPosRef.current
|
|
327
|
+
if (latest) saveToolbarPosition(scope, latest)
|
|
328
|
+
}
|
|
329
|
+
document.addEventListener('pointermove', onMove)
|
|
330
|
+
document.addEventListener('pointerup', onUp)
|
|
331
|
+
return () => {
|
|
332
|
+
document.removeEventListener('pointermove', onMove)
|
|
333
|
+
document.removeEventListener('pointerup', onUp)
|
|
334
|
+
}
|
|
335
|
+
}, [scope])
|
|
336
|
+
const dapBarPosRef = React.useRef(dapBarPos)
|
|
337
|
+
dapBarPosRef.current = dapBarPos
|
|
338
|
+
const startDapBarDrag = (event) => {
|
|
339
|
+
if (event.button !== 0 || !dapBarRef.current) return
|
|
340
|
+
const rect = dapBarRef.current.getBoundingClientRect()
|
|
341
|
+
event.preventDefault()
|
|
342
|
+
event.stopPropagation()
|
|
343
|
+
dapDragRef.current = { offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top }
|
|
344
|
+
setDapBarDragging(true)
|
|
345
|
+
event.currentTarget.setPointerCapture?.(event.pointerId)
|
|
346
|
+
}
|
|
347
|
+
// 进程选择器选中项(多候选附加时;候选集变化回落首个)
|
|
348
|
+
const [dapPickPid, setDapPickPid] = React.useState(0)
|
|
349
|
+
React.useEffect(() => { setDapPickPid(dapSnap.candidates?.[0]?.pid ?? 0) }, [dapSnap.candidates])
|
|
350
|
+
// 断点右键菜单(glyph 区右键)与行内断点编辑浮层(Monaco content widget)
|
|
351
|
+
const [dapBpMenu, setDapBpMenu] = React.useState(null)
|
|
352
|
+
const bpWidgetRef = React.useRef(null) // { close, path, line } | null
|
|
353
|
+
const dapScopeRef = React.useRef(scope)
|
|
354
|
+
const dapCwdRef = React.useRef(cwd)
|
|
355
|
+
dapScopeRef.current = scope
|
|
356
|
+
dapCwdRef.current = cwd
|
|
357
|
+
const dapToggleRef = React.useRef(null)
|
|
358
|
+
dapToggleRef.current = (path, line) => {
|
|
359
|
+
if (!cwd || !path || !line) return
|
|
360
|
+
// 严格对齐 CodeBuddy:任意文件都可切换断点(不再限定 .lua 调试目标)
|
|
361
|
+
dapStore.toggleAt(scope, cwd, path, line)
|
|
362
|
+
}
|
|
363
|
+
// 工作区变化同步调试 store(配置清单/启动基准);有 cwd 时预拉 launch.json 配置
|
|
364
|
+
React.useEffect(() => {
|
|
365
|
+
dapStore.setWorkspace(cwd || '')
|
|
366
|
+
if (cwd) void dapStore.ensureConfigs()
|
|
367
|
+
}, [cwd])
|
|
265
368
|
const svnStatusRef = React.useRef(null) // svnStatus 渲染值镜像(ensureEditor 空依赖闭包读取最新状态)
|
|
266
369
|
const [snippetPicker, setSnippetPicker] = React.useState(null) // 'configure' | 'insert' | null(代码片段浮层)
|
|
267
370
|
const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
|
|
@@ -640,8 +743,9 @@ export function EditorView(props) {
|
|
|
640
743
|
setStatus('已加载')
|
|
641
744
|
} else {
|
|
642
745
|
const base = res?.error ? String(res.error) : '读取失败'
|
|
746
|
+
dapTrace('read-failed', { path, error: base, resolvedPath: res?.resolvedPath, cwd })
|
|
643
747
|
// 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
|
|
644
|
-
const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
|
|
748
|
+
const message = res?.resolvedPath ? base + ':' + relativeOf(String(res.resolvedPath), cwd) : base
|
|
645
749
|
setLoadError(message)
|
|
646
750
|
setError(message)
|
|
647
751
|
setStatus('读取失败')
|
|
@@ -1324,6 +1428,20 @@ export function EditorView(props) {
|
|
|
1324
1428
|
['edrv.command.svnAdd', () => runSvnAdd(activeRef.current)],
|
|
1325
1429
|
['edrv.command.svnRevertCli', () => runSvnRevertCli(activeRef.current)],
|
|
1326
1430
|
['edrv.command.svnLog', () => openSvnLog(activeRef.current || undefined)],
|
|
1431
|
+
['edrv.command.svnCreatePatch', () => runSvnAction(SVN_ACTION_BY_ID['create-patch'], {
|
|
1432
|
+
sessionId,
|
|
1433
|
+
scope,
|
|
1434
|
+
path: activeRef.current ?? '',
|
|
1435
|
+
notify: (message) => setStatus(message),
|
|
1436
|
+
openPatchDialog: (payload) => setPatchDialog(payload),
|
|
1437
|
+
})],
|
|
1438
|
+
['edrv.command.svnDiffSum', () => runSvnAction(SVN_ACTION_BY_ID['diff-summarize'], {
|
|
1439
|
+
sessionId,
|
|
1440
|
+
scope,
|
|
1441
|
+
path: dirOfRel(activeRef.current ?? ''),
|
|
1442
|
+
notify: (message) => setStatus(message),
|
|
1443
|
+
openSumDialog: (target) => setSumDialog({ path: target }),
|
|
1444
|
+
})],
|
|
1327
1445
|
['edrv.command.svnCleanup', () => runSvnAction(SVN_ACTION_BY_ID.cleanup, {
|
|
1328
1446
|
sessionId,
|
|
1329
1447
|
scope,
|
|
@@ -1428,6 +1546,8 @@ export function EditorView(props) {
|
|
|
1428
1546
|
setupAiInline(m)
|
|
1429
1547
|
// 代码片段补全 provider 注册(幂等;条目按会话工作区懒加载)
|
|
1430
1548
|
setupSnippets(m)
|
|
1549
|
+
// launch.json「添加配置」补全 provider 注册(幂等;仅 launch.json configurations 数组内出条目)
|
|
1550
|
+
setupLaunchJson(m)
|
|
1431
1551
|
}).catch((e) => {
|
|
1432
1552
|
if (alive) {
|
|
1433
1553
|
setMonacoErr(String(e?.message ?? e))
|
|
@@ -1461,10 +1581,7 @@ export function EditorView(props) {
|
|
|
1461
1581
|
monacoRef.current = monaco
|
|
1462
1582
|
|
|
1463
1583
|
/** 从编辑器实例 model URI 解析活动文件路径(工作区相对;无模型返回 null)。 */
|
|
1464
|
-
const modelPathOf = (edx) =>
|
|
1465
|
-
const uriPath = edx?.getModel?.()?.uri?.path
|
|
1466
|
-
return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
|
|
1467
|
-
}
|
|
1584
|
+
const modelPathOf = (edx) => editorModelPathOf(edx)
|
|
1468
1585
|
|
|
1469
1586
|
/**
|
|
1470
1587
|
* 同步 Monaco 右键菜单 SVN 组:先注销旧组,再按最新 svn 状态注册(空状态 = 全部移除)。
|
|
@@ -1503,6 +1620,7 @@ export function EditorView(props) {
|
|
|
1503
1620
|
path: def.id === 'update' ? (relPath ?? '') : relPath,
|
|
1504
1621
|
notify: (message) => setStatus(message),
|
|
1505
1622
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
1623
|
+
openSvnLocalPair: runSvnDiffLocalPair,
|
|
1506
1624
|
openSvnLog: (p) => openSvnLog(p ?? relPath),
|
|
1507
1625
|
refreshChanges: () => refreshSvnChanges(sessionId, scope),
|
|
1508
1626
|
})
|
|
@@ -1887,9 +2005,105 @@ export function EditorView(props) {
|
|
|
1887
2005
|
if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null }
|
|
1888
2006
|
setHoverAct({ region: hit, top, right })
|
|
1889
2007
|
})
|
|
2008
|
+
// 调试断点:行首 glyph 圆点区**左键**点击切换(右键走下方 contextmenu 菜单,
|
|
2009
|
+
// 必须区分按键——否则右键会既 toggle 断点又弹菜单)。
|
|
2010
|
+
// ⚠️ 枚举在 monaco.editor 子命名空间(monaco.editor.MouseTargetType,值为 2);
|
|
2011
|
+
// 取顶层 m.MouseTargetType 是 undefined,会让点击永远静默返回(实测踩坑)。
|
|
2012
|
+
ed.onMouseDown((e) => {
|
|
2013
|
+
const target = e?.target
|
|
2014
|
+
if (!e?.event?.leftButton) return
|
|
2015
|
+
const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
|
|
2016
|
+
if (!target || target.type !== GUTTER) return
|
|
2017
|
+
const line = target.position?.lineNumber
|
|
2018
|
+
const path = pathOf(ed)
|
|
2019
|
+
if (line && path) dapToggleRef.current?.(path, line)
|
|
2020
|
+
})
|
|
2021
|
+
// 断点 hover 预览(对齐 VS Code `ensureBreakpointHintDecoration`):
|
|
2022
|
+
// 鼠标移到 glyph 区(type=2)或行号区(type=3)的可下断点行 → 半透明红点 + pointer 光标;
|
|
2023
|
+
// 移开/离开编辑器 → 清除。已有断点的行不显示预览(避免与实心红点重叠)。
|
|
2024
|
+
ed.onMouseMove((e) => {
|
|
2025
|
+
const target = e?.target
|
|
2026
|
+
const path = pathOf(ed)
|
|
2027
|
+
const list = path ? (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []) : []
|
|
2028
|
+
const line = hintLineFor(target?.type, target?.position?.lineNumber, list.map((it) => it.line))
|
|
2029
|
+
setHintLine(ed, line || null)
|
|
2030
|
+
})
|
|
2031
|
+
ed.onMouseLeave(() => setHintLine(ed, null))
|
|
2032
|
+
// 断点右键菜单:glyph 区右键**始终**由本插件接管(严格对齐 CodeBuddy:任意文件都出菜单,
|
|
2033
|
+
// 不再限定 .lua)。命中行改用 Monaco 官方 getTargetAtClientPoint——
|
|
2034
|
+
// 手算 floor((relY+scrollTop)/lineHeight)+1 **未扣编辑器 padding-top**(实测 6px),
|
|
2035
|
+
// 点在某行框下半部会被算成下一行 → 对空行显示"添加"菜单(实测踩坑)。
|
|
2036
|
+
const glyphHitLine = (ev) => {
|
|
2037
|
+
const edi = editorRef.current
|
|
2038
|
+
if (!edi?.getTargetAtClientPoint) return null
|
|
2039
|
+
const rect = edi.getDomNode?.()?.getBoundingClientRect?.()
|
|
2040
|
+
if (!rect) return null
|
|
2041
|
+
const layout = edi.getLayoutInfo?.()
|
|
2042
|
+
if (!layout) return null
|
|
2043
|
+
const relX = ev.clientX - rect.left
|
|
2044
|
+
if (relX < layout.glyphMarginLeft || relX > layout.glyphMarginLeft + layout.glyphMarginWidth) return null
|
|
2045
|
+
const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
|
|
2046
|
+
const target = edi.getTargetAtClientPoint(ev.clientX, ev.clientY)
|
|
2047
|
+
if (!target || target.type !== GUTTER) return null
|
|
2048
|
+
return target.position?.lineNumber ?? null
|
|
2049
|
+
}
|
|
2050
|
+
const onBpCtxCapture = (ev) => {
|
|
2051
|
+
const line = glyphHitLine(ev)
|
|
2052
|
+
if (!line) return
|
|
2053
|
+
const path = pathOf(editorRef.current)
|
|
2054
|
+
if (!path) return
|
|
2055
|
+
const entry = (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []).find((it) => it.line === line)
|
|
2056
|
+
ev.preventDefault()
|
|
2057
|
+
ev.stopPropagation()
|
|
2058
|
+
setDapBpMenu({
|
|
2059
|
+
x: ev.clientX,
|
|
2060
|
+
y: ev.clientY,
|
|
2061
|
+
path,
|
|
2062
|
+
line,
|
|
2063
|
+
hasBp: !!entry,
|
|
2064
|
+
enabled: entry ? entry.enabled !== false : true,
|
|
2065
|
+
isLogpoint: !!(entry && entry.logMessage),
|
|
2066
|
+
})
|
|
2067
|
+
}
|
|
2068
|
+
node.addEventListener('contextmenu', onBpCtxCapture, true)
|
|
1890
2069
|
editorRef.current = ed
|
|
1891
2070
|
}, [])
|
|
1892
2071
|
|
|
2072
|
+
// 调试断点装饰:断点表/活动文件变化时全量替换行首圆点(独立 collection,与差异/下划线互不干扰);
|
|
2073
|
+
// 适配器真实回执 verified:false 的行渲染空心红圈(对齐 VS Code 未验证语义)
|
|
2074
|
+
React.useEffect(() => {
|
|
2075
|
+
const ed = editorRef.current
|
|
2076
|
+
if (!ed || !active) return
|
|
2077
|
+
const map = dapStore.bpMapOf(scope)
|
|
2078
|
+
const unverified = bpLinesOf(map, active).filter((line) => dapStore.ackOf(active, line)?.verified === false)
|
|
2079
|
+
applyBpDecorations(ed, bpLinesOf(map, active), bpDisabledLinesOf(map, active), unverified)
|
|
2080
|
+
}, [scope, active, contentPath, dapSnap.bpVersion, dapSnap.bpAckVersion, monaco])
|
|
2081
|
+
|
|
2082
|
+
// 行内断点编辑浮层:切换文件 / 编辑器重建时关闭(浮层锚定旧 model 的行号,留着会错位)
|
|
2083
|
+
React.useEffect(() => {
|
|
2084
|
+
closeBpWidget()
|
|
2085
|
+
}, [active, contentPath])
|
|
2086
|
+
|
|
2087
|
+
// 编辑浮层的目标断点被删除(如 agent 清理埋点/用户在面板删除)→ 浮层自动关闭
|
|
2088
|
+
React.useEffect(() => {
|
|
2089
|
+
const held = bpWidgetRef.current
|
|
2090
|
+
if (!held) return
|
|
2091
|
+
const entry = (dapStore.bpMapOf(scope)[held.path] ?? []).some((it) => it.line === held.line)
|
|
2092
|
+
if (!entry) closeBpWidget()
|
|
2093
|
+
}, [scope, dapSnap.bpVersion])
|
|
2094
|
+
|
|
2095
|
+
// 编辑器卸载时移除浮层(content widget 不随 React 树回收)
|
|
2096
|
+
React.useEffect(() => () => closeBpWidget(), [])
|
|
2097
|
+
|
|
2098
|
+
// 调试停帧行:paused 且首帧属于当前文件时高亮该行;继续/停止/切走自动清除
|
|
2099
|
+
React.useEffect(() => {
|
|
2100
|
+
const ed = editorRef.current
|
|
2101
|
+
if (!ed) return
|
|
2102
|
+
const top = dapSnap.topFrame
|
|
2103
|
+
const hit = top && top.file && sameFile(top.file, active)
|
|
2104
|
+
setPausedLine(ed, hit ? top.line : null)
|
|
2105
|
+
}, [dapSnap.phase, dapSnap.topFrame, active, contentPath, sameFile])
|
|
2106
|
+
|
|
1893
2107
|
/**
|
|
1894
2108
|
* 跳转落地后给目标区域挂临时高亮(默认 1.2s 后自动清除)。
|
|
1895
2109
|
*
|
|
@@ -1921,6 +2135,14 @@ export function EditorView(props) {
|
|
|
1921
2135
|
|
|
1922
2136
|
// 打开文件后的跳转(导航历史恢复/差异聚焦/搜索与 LSP 行列跳转,目标内容就绪后执行一次)
|
|
1923
2137
|
//
|
|
2138
|
+
// ⚠️ 依赖必须含 monaco:contentReady 在**文本到达**时即为真,可能早于 Monaco 就绪;
|
|
2139
|
+
// 那一帧 editorRef.current 仍是 null → 本 effect 提前返回并保留 pendingFocus,随后
|
|
2140
|
+
// Monaco 载入、ensureEditor 建好编辑器,若 effect 不因 monaco 变化重跑,pendingFocus
|
|
2141
|
+
// 就永久滞留 —— 观感是「首次跳转只打开文件不定位,手动再触发一次才落到目标行」
|
|
2142
|
+
// (实测复现:Monaco 未就绪时派发 line=500,首次停在 1:1,二次触发才到 500)。
|
|
2143
|
+
// ⚠️ 依赖也必须含 mdPreviewing:预览态切换会替换 Monaco host 并**重建**编辑器实例,
|
|
2144
|
+
// 而 active/content 都没变;缺此依赖同样不重跑(同下方 model 同步 effect)。
|
|
2145
|
+
//
|
|
1924
2146
|
// ⚠️ 门控必须用 contentReady(contentPath === active),不能用 `content === null`:
|
|
1925
2147
|
// 跨文件跳转时切页签的那一帧 content 仍是**上一个文件**的内容(非 null),据此判定
|
|
1926
2148
|
// 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
|
|
@@ -1962,7 +2184,7 @@ export function EditorView(props) {
|
|
|
1962
2184
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
1963
2185
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
1964
2186
|
ed.focus()
|
|
1965
|
-
}, [active, content, contentReady, pendingRegions, focusRequest])
|
|
2187
|
+
}, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing])
|
|
1966
2188
|
|
|
1967
2189
|
const jumpTo = (region) => {
|
|
1968
2190
|
if (editorRef.current) {
|
|
@@ -2379,6 +2601,8 @@ export function EditorView(props) {
|
|
|
2379
2601
|
leftLabel: 'BASE',
|
|
2380
2602
|
rightLabel: '工作区',
|
|
2381
2603
|
scheme: 'edrv-svn-base',
|
|
2604
|
+
binary: outcome.binary === true,
|
|
2605
|
+
encodingHint: outcome.encodingHint === true,
|
|
2382
2606
|
})
|
|
2383
2607
|
setStatus(outcome.message)
|
|
2384
2608
|
})
|
|
@@ -2390,8 +2614,9 @@ export function EditorView(props) {
|
|
|
2390
2614
|
* @author ddj 2026年09月16号
|
|
2391
2615
|
* @param relPath 目标工作区相对路径
|
|
2392
2616
|
* @param revision 目标版本
|
|
2617
|
+
* @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
|
|
2393
2618
|
*/
|
|
2394
|
-
const runSvnDiffRev = (relPath, revision) => {
|
|
2619
|
+
const runSvnDiffRev = (relPath, revision, logTarget) => {
|
|
2395
2620
|
if (!relPath) { setStatus('无目标文件'); return }
|
|
2396
2621
|
setStatus('读取 r' + revision + ' 差异…')
|
|
2397
2622
|
void svnDiffRev(sessionId, relPath, revision).then((outcome) => {
|
|
@@ -2406,6 +2631,9 @@ export function EditorView(props) {
|
|
|
2406
2631
|
leftLabel: 'r' + Math.max(1, revision - 1),
|
|
2407
2632
|
rightLabel: 'r' + revision,
|
|
2408
2633
|
scheme: 'edrv-svn-rev' + revision,
|
|
2634
|
+
binary: outcome.binary === true,
|
|
2635
|
+
encodingHint: outcome.encodingHint === true,
|
|
2636
|
+
logTarget,
|
|
2409
2637
|
})
|
|
2410
2638
|
setStatus('已打开 r' + revision + ' 差异')
|
|
2411
2639
|
})
|
|
@@ -2418,8 +2646,9 @@ export function EditorView(props) {
|
|
|
2418
2646
|
* @param relPath 目标工作区相对路径
|
|
2419
2647
|
* @param revA 左侧版本(选中顺序在前)
|
|
2420
2648
|
* @param revB 右侧版本(选中顺序在后)
|
|
2649
|
+
* @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
|
|
2421
2650
|
*/
|
|
2422
|
-
const runSvnDiffPair = (relPath, revA, revB) => {
|
|
2651
|
+
const runSvnDiffPair = (relPath, revA, revB, logTarget) => {
|
|
2423
2652
|
if (!relPath) { setStatus('仅文件目标支持比较两个修订'); return }
|
|
2424
2653
|
setStatus('读取 r' + revA + ' ↔ r' + revB + ' 差异…')
|
|
2425
2654
|
void svnDiffPair(sessionId, relPath, revA, revB).then((outcome) => {
|
|
@@ -2436,6 +2665,9 @@ export function EditorView(props) {
|
|
|
2436
2665
|
leftLabel: 'r' + revA,
|
|
2437
2666
|
rightLabel: 'r' + revB,
|
|
2438
2667
|
scheme: 'edrv-svn-pair-' + revA + '-' + revB,
|
|
2668
|
+
binary: outcome.binary === true,
|
|
2669
|
+
encodingHint: outcome.encodingHint === true,
|
|
2670
|
+
logTarget,
|
|
2439
2671
|
})
|
|
2440
2672
|
setStatus('已打开 r' + revA + ' ↔ r' + revB + ' 差异')
|
|
2441
2673
|
})
|
|
@@ -2447,8 +2679,9 @@ export function EditorView(props) {
|
|
|
2447
2679
|
* @author ddj 2026年09月17号
|
|
2448
2680
|
* @param relPath 目标工作区相对路径
|
|
2449
2681
|
* @param revision 左侧版本
|
|
2682
|
+
* @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
|
|
2450
2683
|
*/
|
|
2451
|
-
const runSvnDiffWorking = (relPath, revision) => {
|
|
2684
|
+
const runSvnDiffWorking = (relPath, revision, logTarget) => {
|
|
2452
2685
|
if (!relPath) { setStatus('仅文件目标支持与工作副本比较'); return }
|
|
2453
2686
|
setStatus('读取 r' + revision + ' ↔ 工作副本 差异…')
|
|
2454
2687
|
void svnDiffWorking(sessionId, relPath, revision).then((outcome) => {
|
|
@@ -2462,11 +2695,42 @@ export function EditorView(props) {
|
|
|
2462
2695
|
leftLabel: 'r' + revision,
|
|
2463
2696
|
rightLabel: '工作区',
|
|
2464
2697
|
scheme: 'edrv-svn-work-' + revision,
|
|
2698
|
+
binary: outcome.binary === true,
|
|
2699
|
+
encodingHint: outcome.encodingHint === true,
|
|
2700
|
+
logTarget,
|
|
2465
2701
|
})
|
|
2466
2702
|
setStatus('已打开 r' + revision + ' ↔ 工作副本 差异')
|
|
2467
2703
|
})
|
|
2468
2704
|
}
|
|
2469
2705
|
|
|
2706
|
+
/**
|
|
2707
|
+
* 打开双侧本地文件差异(W2-4 冲突副本 `.rN` ↔ `.mine`,只读并排;resolve 由用户自行执行)。
|
|
2708
|
+
* @author ddj 2026年09月20号
|
|
2709
|
+
* @param left 左侧工作区相对路径
|
|
2710
|
+
* @param right 右侧工作区相对路径
|
|
2711
|
+
* @param leftLabel 左侧标签(如 `.r293524`)
|
|
2712
|
+
* @param rightLabel 右侧标签(如 `.mine`)
|
|
2713
|
+
* @param title 标题(冲突文件路径)
|
|
2714
|
+
*/
|
|
2715
|
+
const runSvnDiffLocalPair = (left, right, leftLabel, rightLabel, title) => {
|
|
2716
|
+
setStatus('读取冲突副本差异…')
|
|
2717
|
+
void svnDiffLocalPair(sessionId, left, right).then((outcome) => {
|
|
2718
|
+
if (!outcome.ok) { setStatus(outcome.message); return }
|
|
2719
|
+
setSvnDiff({
|
|
2720
|
+
path: title,
|
|
2721
|
+
base: outcome.leftText ?? null,
|
|
2722
|
+
working: outcome.rightText ?? '',
|
|
2723
|
+
message: '解决后执行 svn resolve --accept working(本插件不自动执行)',
|
|
2724
|
+
leftLabel,
|
|
2725
|
+
rightLabel,
|
|
2726
|
+
scheme: 'edrv-svn-local',
|
|
2727
|
+
binary: outcome.binary === true,
|
|
2728
|
+
encodingHint: outcome.encodingHint === true,
|
|
2729
|
+
})
|
|
2730
|
+
setStatus('已打开冲突副本对比')
|
|
2731
|
+
})
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2470
2734
|
/**
|
|
2471
2735
|
* 打开日志弹窗(P3):加载该目标的提交历史。
|
|
2472
2736
|
* @author ddj 2026年09月16号
|
|
@@ -2482,6 +2746,17 @@ export function EditorView(props) {
|
|
|
2482
2746
|
ensureSvnLog(sessionId, scope, target, SVN_LOG_PAGE)
|
|
2483
2747
|
}
|
|
2484
2748
|
|
|
2749
|
+
/**
|
|
2750
|
+
* 从日志弹窗打开的差异视图返回日志:按打开时记录的日志目标重开弹窗。
|
|
2751
|
+
* 日志数据走 scope 缓存(openSvnLog → ensureSvnLog 命中即不重查);选中项不复原。
|
|
2752
|
+
* @author ddj 2026年09月18号
|
|
2753
|
+
*/
|
|
2754
|
+
const backToSvnLog = () => {
|
|
2755
|
+
const target = svnDiff?.logTarget ?? ''
|
|
2756
|
+
setSvnDiff(null)
|
|
2757
|
+
openSvnLog(target)
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2485
2760
|
/** 加入版本控制(活动文件;未纳入版本控制的文件才有意义,host 侧会再校验)。 */
|
|
2486
2761
|
const runSvnAdd = (relPath) => {
|
|
2487
2762
|
if (!relPath) { setStatus('无活动文件'); return }
|
|
@@ -2584,6 +2859,7 @@ export function EditorView(props) {
|
|
|
2584
2859
|
path,
|
|
2585
2860
|
notify: (message) => setStatus(message),
|
|
2586
2861
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
2862
|
+
openSvnLocalPair: runSvnDiffLocalPair,
|
|
2587
2863
|
openSvnLog: (p) => openSvnLog(p ?? path),
|
|
2588
2864
|
refreshChanges: () => refreshSvnChanges(sessionId, scope),
|
|
2589
2865
|
})
|
|
@@ -2681,6 +2957,8 @@ export function EditorView(props) {
|
|
|
2681
2957
|
*/
|
|
2682
2958
|
const openFileAt = (path, line, column, endLine, endColumn) => {
|
|
2683
2959
|
if (!path) return
|
|
2960
|
+
const tracePath = tabPathOf(path, cwd)
|
|
2961
|
+
dapTrace('open-file-at', { input: path, normalized: tracePath, line, cwd })
|
|
2684
2962
|
recordNav()
|
|
2685
2963
|
// G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
|
|
2686
2964
|
const normalized = addTabNorm(path, true)
|
|
@@ -2689,6 +2967,51 @@ export function EditorView(props) {
|
|
|
2689
2967
|
setFocusRequest((value) => value + 1)
|
|
2690
2968
|
}
|
|
2691
2969
|
|
|
2970
|
+
// 停帧跳转/调试命令监听(openFileAt 每渲染重建,经 ref 让空依赖监听读最新闭包)
|
|
2971
|
+
const dapOpenRef = React.useRef(null)
|
|
2972
|
+
dapOpenRef.current = openFileAt
|
|
2973
|
+
React.useEffect(() => {
|
|
2974
|
+
const onStopped = (e) => {
|
|
2975
|
+
const detail = e?.detail
|
|
2976
|
+
dapTrace('debug-stopped', detail)
|
|
2977
|
+
if (!detail?.file) {
|
|
2978
|
+
// 适配器只给了 chunkname 且工作区未定位到源文件:明确告知,不留静默无反应
|
|
2979
|
+
setStatus('已暂停:未定位到源文件')
|
|
2980
|
+
return
|
|
2981
|
+
}
|
|
2982
|
+
dapOpenRef.current?.(detail.file, detail.line, 1)
|
|
2983
|
+
}
|
|
2984
|
+
const onToggle = () => {
|
|
2985
|
+
const ed = editorRef.current
|
|
2986
|
+
const path = modelPathOf(ed)
|
|
2987
|
+
const line = ed?.getPosition?.()?.lineNumber
|
|
2988
|
+
if (path && line) dapToggleRef.current?.(path, line)
|
|
2989
|
+
else setStatus('无活动文件,无法切换断点')
|
|
2990
|
+
}
|
|
2991
|
+
const onToggleEvt = () => onToggle()
|
|
2992
|
+
const onStart = () => dapStore.startOrContinue()
|
|
2993
|
+
const onStop = () => { void dapStore.stop() }
|
|
2994
|
+
const onStepOver = () => { void dapStore.action('next') }
|
|
2995
|
+
const onStepIn = () => { void dapStore.action('stepIn') }
|
|
2996
|
+
const onStepOut = () => { void dapStore.action('stepOut') }
|
|
2997
|
+
window.addEventListener('edrv:debug-stopped', onStopped)
|
|
2998
|
+
window.addEventListener('edrv.command.debugToggleBreakpoint', onToggleEvt)
|
|
2999
|
+
window.addEventListener('edrv.command.debugStartContinue', onStart)
|
|
3000
|
+
window.addEventListener('edrv.command.debugStepOver', onStepOver)
|
|
3001
|
+
window.addEventListener('edrv.command.debugStepInto', onStepIn)
|
|
3002
|
+
window.addEventListener('edrv.command.debugStepOut', onStepOut)
|
|
3003
|
+
window.addEventListener('edrv.command.debugStop', onStop)
|
|
3004
|
+
return () => {
|
|
3005
|
+
window.removeEventListener('edrv:debug-stopped', onStopped)
|
|
3006
|
+
window.removeEventListener('edrv.command.debugToggleBreakpoint', onToggleEvt)
|
|
3007
|
+
window.removeEventListener('edrv.command.debugStartContinue', onStart)
|
|
3008
|
+
window.removeEventListener('edrv.command.debugStepOver', onStepOver)
|
|
3009
|
+
window.removeEventListener('edrv.command.debugStepInto', onStepIn)
|
|
3010
|
+
window.removeEventListener('edrv.command.debugStepOut', onStepOut)
|
|
3011
|
+
window.removeEventListener('edrv.command.debugStop', onStop)
|
|
3012
|
+
}
|
|
3013
|
+
}, [])
|
|
3014
|
+
|
|
2692
3015
|
const openPath = () => {
|
|
2693
3016
|
const p = (pathDraft || '').trim()
|
|
2694
3017
|
if (!p) return
|
|
@@ -2900,6 +3223,7 @@ export function EditorView(props) {
|
|
|
2900
3223
|
message: svnDiff.message,
|
|
2901
3224
|
leftLabel: svnDiff.leftLabel,
|
|
2902
3225
|
rightLabel: svnDiff.rightLabel,
|
|
3226
|
+
onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
|
|
2903
3227
|
onClose: () => setSvnDiff(null),
|
|
2904
3228
|
})
|
|
2905
3229
|
} else if (!monaco && !monacoErr) {
|
|
@@ -3064,14 +3388,125 @@ export function EditorView(props) {
|
|
|
3064
3388
|
svnChanges,
|
|
3065
3389
|
svnChangeMap: svnChangeMapOf(scope),
|
|
3066
3390
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
3391
|
+
openSvnLocalPair: runSvnDiffLocalPair,
|
|
3067
3392
|
refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
|
|
3068
3393
|
openSvnLog: (p) => openSvnLog(p),
|
|
3069
3394
|
confirm: (message) => (typeof window === 'undefined' ? false : window.confirm(message)),
|
|
3070
3395
|
notify: (message) => setStatus(message),
|
|
3071
3396
|
}
|
|
3072
3397
|
|
|
3398
|
+
// 调试工具条(VS Code 浮动条紧凑版,图2 形态):配置下拉 + 启动/继续 + 单步组 + 停止 + 相位。
|
|
3399
|
+
// 配置来自工作区 .dsh/launch.json(插件专属,与 VS Code 互不干扰);无配置时下拉提示且按钮禁用。
|
|
3400
|
+
const dapPhaseTextOf = () => ({
|
|
3401
|
+
idle: '', starting: '启动中…', waiting: '等待目标…', running: '运行中', paused: '已暂停', terminated: '已结束',
|
|
3402
|
+
}[dapSnap.phase] || '')
|
|
3403
|
+
const dapActive = dapSnap.phase !== 'idle' && dapSnap.phase !== 'terminated'
|
|
3404
|
+
const dapPaused = dapSnap.phase === 'paused'
|
|
3405
|
+
const dapBtn = (label, title, disabled, onClick) => React.createElement('button', {
|
|
3406
|
+
key: title, className: 'edrv-dapbar-btn', title, disabled: disabled === true, onClick,
|
|
3407
|
+
}, label)
|
|
3408
|
+
const dapBarVisible = dapActive || Boolean(dapSnap.candidates)
|
|
3409
|
+
const dapBarStyle = dapBarPos
|
|
3410
|
+
? { left: dapBarPos.left, top: dapBarPos.top, right: 'auto' }
|
|
3411
|
+
: undefined
|
|
3412
|
+
const dapBar = dapBarVisible ? React.createElement('div', {
|
|
3413
|
+
ref: dapBarRef,
|
|
3414
|
+
className: 'edrv-dapbar' + (dapBarVisible ? ' on' : '') + (dapBarDragging ? ' dragging' : ''),
|
|
3415
|
+
style: dapBarStyle,
|
|
3416
|
+
},
|
|
3417
|
+
React.createElement('span', {
|
|
3418
|
+
className: 'edrv-dapbar-grip',
|
|
3419
|
+
role: 'button',
|
|
3420
|
+
tabIndex: 0,
|
|
3421
|
+
title: '拖动调试工具条',
|
|
3422
|
+
'aria-label': '拖动调试工具条',
|
|
3423
|
+
onPointerDown: startDapBarDrag,
|
|
3424
|
+
}, '⠿'),
|
|
3425
|
+
(dapSnap.candidates
|
|
3426
|
+
? [
|
|
3427
|
+
React.createElement('select', {
|
|
3428
|
+
key: 'pick', className: 'edrv-dapbar-cfg', title: '选择附加目标进程(processName 命中多个候选)',
|
|
3429
|
+
value: String(dapPickPid || dapSnap.candidates[0]?.pid || ''),
|
|
3430
|
+
onChange: (e) => setDapPickPid(Number(e.target.value)),
|
|
3431
|
+
},
|
|
3432
|
+
dapSnap.candidates.map((it) => React.createElement('option', { key: it.pid, value: String(it.pid) },
|
|
3433
|
+
it.pid + ' : ' + it.name + (it.title ? ' — ' + it.title.slice(0, 44) : '')))),
|
|
3434
|
+
dapBtn('✓', '附加到选中进程', false, () => dapStore.confirmPick(dapPickPid || dapSnap.candidates![0]?.pid || 0)),
|
|
3435
|
+
dapBtn('✕', '取消', false, () => dapStore.cancelPick()),
|
|
3436
|
+
]
|
|
3437
|
+
: React.createElement('select', {
|
|
3438
|
+
className: 'edrv-dapbar-cfg',
|
|
3439
|
+
title: '调试配置(读取工作区 .dsh/launch.json;适配器由扩展清单 contributes.debuggers 提供)',
|
|
3440
|
+
value: dapSnap.selectedConfig ?? '',
|
|
3441
|
+
onChange: (e) => dapStore.selectConfig(e.target.value),
|
|
3442
|
+
},
|
|
3443
|
+
dapSnap.configs.length
|
|
3444
|
+
? dapSnap.configs.map((c) => {
|
|
3445
|
+
// 旧 host 无 adapters 数据时视为全部可用(保持兼容);有数据则按可用性置灰并给原因
|
|
3446
|
+
const adapter = dapSnap.adapters.find((a) => a.type === c.type)
|
|
3447
|
+
const ready = dapSnap.adapters.length === 0 || adapter?.available === true
|
|
3448
|
+
const suffix = ready ? '' : '(' + (adapter?.reason || '未发现该类型适配器') + ')'
|
|
3449
|
+
return React.createElement('option', { key: c.name, value: c.name, disabled: !ready }, c.name + suffix)
|
|
3450
|
+
})
|
|
3451
|
+
: React.createElement('option', { value: '' }, '无可用调试配置'))),
|
|
3452
|
+
dapBtn(dapPaused ? '▶' : '▶', '启动 / 继续(F5)', !dapPaused && (!dapSnap.configs.some((c) => dapSnap.adapters.length === 0 || dapSnap.adapters.find((a) => a.type === c.type)?.available === true) || dapActive), () => dapStore.startOrContinue()),
|
|
3453
|
+
dapBtn('⏸', '暂停', !dapActive, () => { void dapStore.action('pause') }),
|
|
3454
|
+
dapBtn('⤵', '单步跳过(F10)', !dapPaused, () => { void dapStore.action('next') }),
|
|
3455
|
+
dapBtn('↓', '单步步入(F11)', !dapPaused, () => { void dapStore.action('stepIn') }),
|
|
3456
|
+
dapBtn('↑', '单步步出(Shift+F11)', !dapPaused, () => { void dapStore.action('stepOut') }),
|
|
3457
|
+
dapBtn('⏹', '停止(Shift+F5)', !dapActive, () => { void dapStore.stop() }),
|
|
3458
|
+
(dapPhaseTextOf()
|
|
3459
|
+
? React.createElement('span', { className: 'edrv-dapbar-phase' + (dapPaused ? ' paused' : '') },
|
|
3460
|
+
dapPhaseTextOf() + (dapSnap.pid ? ' · pid ' + dapSnap.pid : ''))
|
|
3461
|
+
: null),
|
|
3462
|
+
(dapSnap.error
|
|
3463
|
+
? React.createElement('span', { className: 'edrv-dapbar-err', title: dapSnap.error }, '⚠ ' + dapSnap.error.slice(0, 40))
|
|
3464
|
+
: null)) : null
|
|
3465
|
+
|
|
3466
|
+
/**
|
|
3467
|
+
* 「添加配置」按钮点击:定位 configurations 数组插入点(数组开头行首),
|
|
3468
|
+
* 显式拉起 Monaco 补全下拉选择调试配置片段插入。
|
|
3469
|
+
* @author ddj 2026年09月22号
|
|
3470
|
+
*/
|
|
3471
|
+
const addLaunchConfig = async () => {
|
|
3472
|
+
const ed = editorRef.current
|
|
3473
|
+
if (!ed) { setStatus('无活动编辑器'); return }
|
|
3474
|
+
const path = modelPathOf(ed)
|
|
3475
|
+
if (!path || !LAUNCH_JSON_RE.test(String(path).replace(/\\/g, '/'))) {
|
|
3476
|
+
setStatus('请先打开 .dsh/launch.json')
|
|
3477
|
+
return
|
|
3478
|
+
}
|
|
3479
|
+
const model = ed.getModel && ed.getModel()
|
|
3480
|
+
const pos = model ? findArrayPos(model.getValue()) : null
|
|
3481
|
+
if (!pos) { setStatus('未找到 configurations 数组'); return }
|
|
3482
|
+
// 预拉片段并预检可用条数:全不可用时不弹空 suggest(否则按钮点了无反馈)
|
|
3483
|
+
const snippets = await prefetchSnippets().catch(() => [])
|
|
3484
|
+
if (!snippets.some((snip) => snip.available !== false)) {
|
|
3485
|
+
setStatus('无可用适配器的配置模板(适配器入口缺失或未发现该类型适配器)')
|
|
3486
|
+
return
|
|
3487
|
+
}
|
|
3488
|
+
ed.focus()
|
|
3489
|
+
ed.setPosition({ lineNumber: pos.line, column: pos.column })
|
|
3490
|
+
if (typeof ed.revealPositionInCenterIfOutsideViewport === 'function') {
|
|
3491
|
+
ed.revealPositionInCenterIfOutsideViewport({ lineNumber: pos.line, column: pos.column })
|
|
3492
|
+
}
|
|
3493
|
+
ed.trigger('edrv-addcfg', 'editor.action.triggerSuggest', null)
|
|
3494
|
+
setStatus('选择要插入的调试配置')
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// 「添加配置」按钮(VSCode 同款形态,编辑区右下角):仅插件专属 launch.json 打开且非差异审阅态显示
|
|
3498
|
+
const addCfgBtn = (active && !svnDiff && LAUNCH_JSON_RE.test(String(active).replace(/\\/g, '/')))
|
|
3499
|
+
? React.createElement('button', {
|
|
3500
|
+
className: 'edrv-addcfg',
|
|
3501
|
+
title: '添加调试配置(插入到 configurations 数组开头)',
|
|
3502
|
+
onClick: addLaunchConfig,
|
|
3503
|
+
}, '添加配置…')
|
|
3504
|
+
: null
|
|
3505
|
+
|
|
3073
3506
|
// 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
|
|
3074
3507
|
const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
|
|
3508
|
+
dapBar,
|
|
3509
|
+
addCfgBtn,
|
|
3075
3510
|
body,
|
|
3076
3511
|
hoverEl,
|
|
3077
3512
|
overlay)
|
|
@@ -3216,31 +3651,124 @@ export function EditorView(props) {
|
|
|
3216
3651
|
setSvnLog({ ...svnLog, limit: SVN_LOG_SHOW_ALL_LIMIT })
|
|
3217
3652
|
ensureSvnLog(sessionId, scope, svnLog.target || '', SVN_LOG_SHOW_ALL_LIMIT, true, svnLogOptsOf(svnLog))
|
|
3218
3653
|
},
|
|
3219
|
-
onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision) },
|
|
3220
|
-
onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB) },
|
|
3221
|
-
onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision) },
|
|
3654
|
+
onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision, svnLog.target || '') },
|
|
3655
|
+
onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB, svnLog.target || '') },
|
|
3656
|
+
onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision, svnLog.target || '') },
|
|
3222
3657
|
onOpenFile: (relPath) => { setSvnLog(null); openFile(relPath, false) },
|
|
3223
3658
|
onClose: () => { setSvnLog(null); setSvnLogError('') },
|
|
3224
3659
|
})
|
|
3225
3660
|
: null
|
|
3661
|
+
// W2-1 补丁对话框 / W2-2 目录对比对话框:portal 到 body,与日志弹窗同一挂载方式
|
|
3662
|
+
const patchDialogEl = patchDialog
|
|
3663
|
+
? React.createElement(SvnPatchDialog, {
|
|
3664
|
+
key: 'edrv-svn-patch',
|
|
3665
|
+
path: patchDialog.path,
|
|
3666
|
+
text: patchDialog.text,
|
|
3667
|
+
truncated: patchDialog.truncated === true,
|
|
3668
|
+
binary: patchDialog.binary === true,
|
|
3669
|
+
onNote: (message) => setStatus(message),
|
|
3670
|
+
onClose: () => setPatchDialog(null),
|
|
3671
|
+
})
|
|
3672
|
+
: null
|
|
3673
|
+
const sumDialogEl = sumDialog
|
|
3674
|
+
? React.createElement(SvnSumDialog, {
|
|
3675
|
+
key: 'edrv-svn-sum',
|
|
3676
|
+
sessionId,
|
|
3677
|
+
path: sumDialog.path,
|
|
3678
|
+
onNote: (message) => setStatus(message),
|
|
3679
|
+
onClose: () => setSumDialog(null),
|
|
3680
|
+
})
|
|
3681
|
+
: null
|
|
3226
3682
|
// 诊断日志弹窗:portal 到 body,与命令栏/SVN 日志弹窗同一挂载方式
|
|
3227
3683
|
const dlogEl = dlogOpen
|
|
3228
3684
|
? React.createElement(LogDialog, { key: 'edrv-dlog', sessionId, onClose: () => setDlogOpen(false) })
|
|
3229
3685
|
: null
|
|
3686
|
+
// 断点右键菜单(逐条对齐 CodeBuddy:4 项添加类 / 删除·编辑·启停 / 记录点名词 / 暂停追加运行到行)
|
|
3687
|
+
const runBpMenuAction = (action) => {
|
|
3688
|
+
const scope = dapScopeRef.current
|
|
3689
|
+
const cwd = dapCwdRef.current
|
|
3690
|
+
const menu = dapBpMenu
|
|
3691
|
+
if (!menu) return
|
|
3692
|
+
const { path, line } = menu
|
|
3693
|
+
if (action === 'add') { dapStore.toggleAt(scope, cwd, path, line); return }
|
|
3694
|
+
if (action === 'delete') { if (menu.hasBp) dapStore.toggleAt(scope, cwd, path, line); return }
|
|
3695
|
+
if (action === 'disable' || action === 'enable') { dapStore.setBpEnabled(scope, cwd, path, line, action === 'enable'); return }
|
|
3696
|
+
if (action === 'edit') { openBpEditor(path, line); return }
|
|
3697
|
+
if (action === 'run-to-line') { dapStore.runTo(cwd, path, line); return }
|
|
3698
|
+
if (action === 'add-triggered') { setStatus('当前 Lua 调试器不支持触发的断点'); return }
|
|
3699
|
+
// 添加条件断点 / 添加记录点:先建点,再开浮层并选中对应模式(0 表达式 / 2 日志消息)
|
|
3700
|
+
dapStore.toggleAt(scope, cwd, path, line)
|
|
3701
|
+
openBpEditor(path, line, action === 'add-logpoint' ? 2 : 0)
|
|
3702
|
+
}
|
|
3703
|
+
const openBpEditor = (path, line, mode) => {
|
|
3704
|
+
const entry = (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []).find((it) => it.line === line)
|
|
3705
|
+
if (!entry) return
|
|
3706
|
+
const ed = editorRef.current
|
|
3707
|
+
if (!ed?.changeViewZones) return
|
|
3708
|
+
// 行内浮层(View Zone,整编辑区宽 + 推开内容,对齐 VS Code breakpointWidget):同一时刻只挂一个
|
|
3709
|
+
closeBpWidget()
|
|
3710
|
+
bpWidgetRef.current = { ...createBpWidget(ed, {
|
|
3711
|
+
line,
|
|
3712
|
+
entry,
|
|
3713
|
+
mode,
|
|
3714
|
+
onSave: (fields) => {
|
|
3715
|
+
dapStore.applyBpFields(dapScopeRef.current, dapCwdRef.current, path, line, fields)
|
|
3716
|
+
setStatus('断点已更新')
|
|
3717
|
+
closeBpWidget()
|
|
3718
|
+
},
|
|
3719
|
+
onClose: () => closeBpWidget(),
|
|
3720
|
+
}), path, line }
|
|
3721
|
+
}
|
|
3722
|
+
const closeBpWidget = () => {
|
|
3723
|
+
const held = bpWidgetRef.current
|
|
3724
|
+
if (!held) return
|
|
3725
|
+
bpWidgetRef.current = null
|
|
3726
|
+
try { held.close() } catch { /* 编辑器可能已销毁 */ }
|
|
3727
|
+
}
|
|
3728
|
+
const dapBpMenuEl = dapBpMenu
|
|
3729
|
+
? React.createElement(ContextMenu, {
|
|
3730
|
+
x: dapBpMenu.x,
|
|
3731
|
+
y: dapBpMenu.y,
|
|
3732
|
+
onClose: () => setDapBpMenu(null),
|
|
3733
|
+
entries: buildBpMenu({
|
|
3734
|
+
hasBreakpoint: dapBpMenu.hasBp,
|
|
3735
|
+
enabled: dapBpMenu.enabled,
|
|
3736
|
+
isLogpoint: dapBpMenu.isLogpoint,
|
|
3737
|
+
paused: dapSnap.phase === 'paused',
|
|
3738
|
+
canRunTo: true,
|
|
3739
|
+
canTriggered: false,
|
|
3740
|
+
}).map((item) => ({
|
|
3741
|
+
id: 'dap-bp-' + item.id,
|
|
3742
|
+
label: item.label,
|
|
3743
|
+
hint: item.hint,
|
|
3744
|
+
separator: item.separator,
|
|
3745
|
+
onClick: () => runBpMenuAction(item.id),
|
|
3746
|
+
})),
|
|
3747
|
+
})
|
|
3748
|
+
: null
|
|
3749
|
+
const dapBpEditEl = null
|
|
3230
3750
|
const rootEl = layout === 'side'
|
|
3231
3751
|
? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
|
|
3232
3752
|
editorRow,
|
|
3233
3753
|
tabMenuEl,
|
|
3234
3754
|
paletteEl,
|
|
3235
3755
|
svnLogEl,
|
|
3756
|
+
patchDialogEl,
|
|
3757
|
+
sumDialogEl,
|
|
3236
3758
|
dlogEl,
|
|
3237
|
-
snippetPickerEl
|
|
3759
|
+
snippetPickerEl,
|
|
3760
|
+
dapBpMenuEl,
|
|
3761
|
+
dapBpEditEl)
|
|
3238
3762
|
: React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: Object.assign({}, baseStyle, { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)' }) },
|
|
3239
3763
|
editorRow,
|
|
3240
3764
|
tabMenuEl,
|
|
3241
3765
|
paletteEl,
|
|
3242
3766
|
svnLogEl,
|
|
3767
|
+
patchDialogEl,
|
|
3768
|
+
sumDialogEl,
|
|
3243
3769
|
dlogEl,
|
|
3244
|
-
snippetPickerEl
|
|
3770
|
+
snippetPickerEl,
|
|
3771
|
+
dapBpMenuEl,
|
|
3772
|
+
dapBpEditEl)
|
|
3245
3773
|
return rootEl
|
|
3246
3774
|
}
|