dsh-vscode-mode 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -5
- package/lib/client.js +5579 -236
- package/lib/client.js.map +1 -1
- package/lib/index.js +2358 -126
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +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/editorLimit.ts +31 -0
- package/src/client/index.ts +13 -0
- package/src/client/markdownPreview.ts +24 -0
- package/src/client/md/componentType.ts +28 -0
- package/src/client/md/mdPanel.ts +98 -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/searchSeed.ts +64 -0
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SearchPanel.ts +70 -15
- 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 +186 -1
- 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 +54 -0
- package/src/client/ui/EditorView.ts +711 -29
- package/src/client/ui/McpSettings.ts +29 -0
- 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 +54 -0
- package/src/client/ui/horizontalWheel.ts +85 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/client-primitives.d.ts +34 -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/fileOpenSettings.ts +3 -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/editorLimit.ts +46 -0
- package/src/shared/keybindings.ts +10 -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
|
@@ -12,6 +12,8 @@ import { dbg, rpc } from '../rpc.js'
|
|
|
12
12
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
13
13
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
14
14
|
import { dataUrlOf, isImagePath, isSvgPath } from '../imagePreview.js'
|
|
15
|
+
import { isMarkdownPath } from '../markdownPreview.js'
|
|
16
|
+
import { MarkdownPanel } from '../md/mdPanel.js'
|
|
15
17
|
import { base64ToBytes, isPdfPath } from '../pdfPreview.js'
|
|
16
18
|
import {
|
|
17
19
|
clearBaseline,
|
|
@@ -30,6 +32,7 @@ import { createDiffRenderer } from '../monaco/diffRender.js'
|
|
|
30
32
|
import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
|
|
31
33
|
import { diffRegions } from '../state/regions.js'
|
|
32
34
|
import { QuickOpen } from './QuickOpen.js'
|
|
35
|
+
import { attachHorizontalWheel } from './horizontalWheel.js'
|
|
33
36
|
import { CommandPalette } from './CommandPalette.js'
|
|
34
37
|
import { closeCommandPalette } from '../commandPaletteStore.js'
|
|
35
38
|
import { DiffLauncher } from './DiffLauncher.js'
|
|
@@ -47,6 +50,7 @@ import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybi
|
|
|
47
50
|
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
48
51
|
import { navHistoryFor } from '../navHistory.js'
|
|
49
52
|
import { statusOfAdd } from '../addToConversation.js'
|
|
53
|
+
import { setSearchSeed } from '../searchSeed.js'
|
|
50
54
|
import { CACHE_KEY } from '../paths.js'
|
|
51
55
|
import { runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
|
|
52
56
|
import { bindLspUnderline } from '../monaco/lsp/underline.js'
|
|
@@ -54,12 +58,15 @@ import { onLspProgress, refreshStatus, setSession as setLspSession } from '../mo
|
|
|
54
58
|
import { setupAiInline, trackAiEditor, aiInlineEnabled } from '../ai/inlineProvider.js'
|
|
55
59
|
import { SnippetsPicker } from './SnippetsPicker.js'
|
|
56
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'
|
|
57
63
|
import {
|
|
58
64
|
absoluteOf, ancestorDirsOf, applyClose, baseNameOf, closeAll, closeOthers, closeRight, closeSaved,
|
|
59
|
-
insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
|
|
65
|
+
evictPlan, insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
|
|
60
66
|
} from '../tabActions.js'
|
|
67
|
+
import { getMaxOpenEditors } from '../editorLimit.js'
|
|
61
68
|
import { buildTabMenu } from '../tabMenu.js'
|
|
62
|
-
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'
|
|
63
70
|
import { ensureSvnLog, getSvnLog, refreshSvnLog, svnDiffPair, svnDiffRev, svnDiffWorking, svnLogKeyOf, svnLogLoadedLimit, svnLogTruncated, svnWcRev, SVN_LOG_SHOW_ALL_LIMIT } from '../svnLog.js'
|
|
64
71
|
import { runSvnAction } from '../svnActions.js'
|
|
65
72
|
import { dshTrace } from '../svnStore.js'
|
|
@@ -69,7 +76,18 @@ import type { SvnAction } from '../../shared/svn.js'
|
|
|
69
76
|
import { createSaveTimer } from '../saveDebounce.js'
|
|
70
77
|
import { ContextMenu } from './ContextMenu.js'
|
|
71
78
|
import { SvnLogDialog } from './SvnLogDialog.js'
|
|
79
|
+
import { SvnPatchDialog } from './SvnPatchDialog.js'
|
|
80
|
+
import { SvnSumDialog } from './SvnSumDialog.js'
|
|
72
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'
|
|
73
91
|
|
|
74
92
|
/** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
|
|
75
93
|
const SVN_LOG_PAGE = 100
|
|
@@ -85,6 +103,18 @@ function svnLogOptsOf(svnLog) {
|
|
|
85
103
|
return { stopOnCopy: svnLog.soc === true, showMerged: svnLog.merged === true, range: svnLog.range ?? null }
|
|
86
104
|
}
|
|
87
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
|
+
|
|
88
118
|
/** 跳转目标高亮的保留时长(LSP/搜索跳转落地后给用户的位置提示,到期自动清除)。 */
|
|
89
119
|
const NAV_FLASH_MS = 1200
|
|
90
120
|
|
|
@@ -165,6 +195,7 @@ export function EditorView(props) {
|
|
|
165
195
|
const [imgSize, setImgSize] = React.useState(null) // 图片自然尺寸 { w, h }(路径栏 meta)
|
|
166
196
|
const [imgBroken, setImgBroken] = React.useState(false) // 图片解码失败(onError),显示占位与重试
|
|
167
197
|
const svgTextRef = React.useRef(new Set()) // 强制以文本打开的 SVG 路径集合(toggleSvgText 维护)
|
|
198
|
+
const mdPreviewRef = React.useRef(new Set()) // 处于预览态的 Markdown 路径集合(toggleMdPreview 维护;不持久化)
|
|
168
199
|
const [pdfBytes, setPdfBytes] = React.useState(null) // 当前 PDF tab 的原始字节(null=加载中/非 PDF)
|
|
169
200
|
const pdfB64CacheRef = React.useRef(new Map()) // path → base64(tab 切回免重读;FIFO 上限防内存膨胀)
|
|
170
201
|
const pdfCtlRef = React.useRef(new Map()) // path → PDF 面板控制器(mount 产出,关闭/卸载销毁)
|
|
@@ -188,7 +219,9 @@ export function EditorView(props) {
|
|
|
188
219
|
const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
|
|
189
220
|
const [svnStatus, setSvnStatus] = React.useState(null) // SVN 能力状态(SvnStatusPayload;null=未加载,SVN 入口隐藏)
|
|
190
221
|
const [svnChanges, setSvnChanges] = React.useState(null) // SVN 变更清单(null=未加载;徽标/菜单判定)
|
|
191
|
-
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 = 关闭)
|
|
192
225
|
const [svnLog, setSvnLog] = React.useState(null) // 日志弹窗 { target, limit, soc?, range? }(null=关闭;P1-4/P1-6 增选项维度)
|
|
193
226
|
const [svnLogData, setSvnLogData] = React.useState(null) // 日志条目(null=未加载)
|
|
194
227
|
const [svnLogBusy, setSvnLogBusy] = React.useState(false) // 日志在途
|
|
@@ -229,6 +262,7 @@ export function EditorView(props) {
|
|
|
229
262
|
const navBackRef = React.useRef(null) // 后退动作最新闭包(窗口级键盘监听读取)
|
|
230
263
|
const navForwardRef = React.useRef(null) // 前进动作最新闭包(窗口级键盘监听读取)
|
|
231
264
|
const cycleTabRef = React.useRef(null) // 页签循环动作最新闭包(Ctrl+Alt+←/→、Ctrl+PgUp/PgDn)
|
|
265
|
+
const toggleMdPreviewRef = React.useRef(null) // Markdown 预览切换动作最新闭包(窗口键位监听读取,防陈旧闭包)
|
|
232
266
|
const rowNavColRef = React.useRef(null) // 整行上下移动的期望列(连续移动保持列位)
|
|
233
267
|
const rowNavMoveRef = React.useRef(false) // 本次光标变化是否由整行移动触发(否则清空期望列)
|
|
234
268
|
const activeRef = React.useRef(null) // 当前活动文件的最新值(空依赖闭包/指令回调读取)
|
|
@@ -240,6 +274,9 @@ export function EditorView(props) {
|
|
|
240
274
|
const dirtyRef = React.useRef({}) // 脏标记最新值(菜单禁用判定与关闭时落盘读)
|
|
241
275
|
dirtyRef.current = dirtyMap
|
|
242
276
|
const tabsHostRef = React.useRef(null) // 页签栏容器(切换后把当前页签滚入可见区)
|
|
277
|
+
// 页签上限与 LRU 使用序(超限淘汰最久未用者;见下方 tabTouchEffect / tabEvictEffect)
|
|
278
|
+
const [tabLimit, setTabLimit] = React.useState(() => getMaxOpenEditors())
|
|
279
|
+
const tabUseRef = React.useRef({ seq: 0, used: {} })
|
|
243
280
|
const [navTick, setNavTick] = React.useState(0) // 历史可用性版本(按钮 disabled 重渲染)
|
|
244
281
|
const hoverRegionsRef = React.useRef([]) // 当前 pending 区域镜像(稳定回调读取)
|
|
245
282
|
const lineRegionMapRef = React.useRef(new Map()) // 行 → 区域 映射(hover 命中)
|
|
@@ -252,6 +289,82 @@ export function EditorView(props) {
|
|
|
252
289
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
253
290
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
254
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])
|
|
255
368
|
const svnStatusRef = React.useRef(null) // svnStatus 渲染值镜像(ensureEditor 空依赖闭包读取最新状态)
|
|
256
369
|
const [snippetPicker, setSnippetPicker] = React.useState(null) // 'configure' | 'insert' | null(代码片段浮层)
|
|
257
370
|
const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
|
|
@@ -282,6 +395,9 @@ export function EditorView(props) {
|
|
|
282
395
|
const isImageActive = !!active && isImagePath(active) && !svgTextRef.current.has(active)
|
|
283
396
|
// 当前 tab 是否为 PDF 面板(与 loadContent 分派条件同源)
|
|
284
397
|
const isPdfActive = !!active && isPdfPath(active)
|
|
398
|
+
// 当前 tab 是否为可预览的 Markdown,以及是否正处于预览态(预览态替换 Monaco host,只读)
|
|
399
|
+
const isMdActive = !!active && isMarkdownPath(active)
|
|
400
|
+
const mdPreviewing = isMdActive && mdPreviewRef.current.has(active)
|
|
285
401
|
const regions = React.useMemo(() => diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded), [currentRecords, content, contentPath, active])
|
|
286
402
|
// useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
|
|
287
403
|
const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
|
|
@@ -627,8 +743,9 @@ export function EditorView(props) {
|
|
|
627
743
|
setStatus('已加载')
|
|
628
744
|
} else {
|
|
629
745
|
const base = res?.error ? String(res.error) : '读取失败'
|
|
746
|
+
dapTrace('read-failed', { path, error: base, resolvedPath: res?.resolvedPath, cwd })
|
|
630
747
|
// 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
|
|
631
|
-
const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
|
|
748
|
+
const message = res?.resolvedPath ? base + ':' + relativeOf(String(res.resolvedPath), cwd) : base
|
|
632
749
|
setLoadError(message)
|
|
633
750
|
setError(message)
|
|
634
751
|
setStatus('读取失败')
|
|
@@ -972,10 +1089,18 @@ export function EditorView(props) {
|
|
|
972
1089
|
// G9:迁移历史持久化里的绝对路径页签(旧版差异入口写入),并顺带按新形态去重
|
|
973
1090
|
const restored = normalizeTabs(saved?.tabs, cwd)
|
|
974
1091
|
if (restored.length) {
|
|
975
|
-
setTabs(restored)
|
|
976
1092
|
// 活动路径同形态归一,否则恢复后匹配不到任何页签(pickActive 回退首个)
|
|
977
1093
|
const wanted = typeof saved?.active === 'string' ? tabPathOf(saved.active, cwd) : saved?.active
|
|
978
|
-
|
|
1094
|
+
const activePath = pickActive(restored, wanted)
|
|
1095
|
+
// 上限已下调过(或历史存档超限):恢复即收敛。此时页签均未编辑、无脏数据,
|
|
1096
|
+
// 故只做纯计算裁剪,不走 closeTabs(避免无谓的落盘探测与状态栏提示)。
|
|
1097
|
+
// evictPlan 已保护活动页签,无需额外 keep。
|
|
1098
|
+
const limit = getMaxOpenEditors()
|
|
1099
|
+
const plan = evictPlan(restored, limit, { active: activePath, used: {} })
|
|
1100
|
+
const dropped = new Set(plan)
|
|
1101
|
+
const kept = plan.length ? restored.filter((tab) => !dropped.has(tab.path)) : restored
|
|
1102
|
+
setTabs(kept)
|
|
1103
|
+
setActive(kept.some((tab) => tab.path === activePath) ? activePath : (kept[0]?.path ?? null))
|
|
979
1104
|
}
|
|
980
1105
|
}
|
|
981
1106
|
} catch (e) { /* 损坏忽略 */ }
|
|
@@ -999,6 +1124,45 @@ export function EditorView(props) {
|
|
|
999
1124
|
catch (e) { /* 忽略 */ }
|
|
1000
1125
|
}, [tabs, active, sessionId, scope])
|
|
1001
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* 标记页签为「最近使用」(LRU 序号单调递增;编辑动作也算使用,见 onEdit)。
|
|
1129
|
+
* @author ddj 2026年09月18号
|
|
1130
|
+
* @param path 页签路径
|
|
1131
|
+
*/
|
|
1132
|
+
const touchTab = (path) => {
|
|
1133
|
+
if (!path) return
|
|
1134
|
+
const state = tabUseRef.current
|
|
1135
|
+
state.seq += 1
|
|
1136
|
+
state.used[path] = state.seq
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// 页签上限设置变更(edrv:max-open-editors)→ 更新上限态并触发下方淘汰 effect 复算
|
|
1140
|
+
React.useEffect(() => {
|
|
1141
|
+
const onLimit = () => setTabLimit(getMaxOpenEditors())
|
|
1142
|
+
window.addEventListener('edrv:max-open-editors', onLimit)
|
|
1143
|
+
return () => window.removeEventListener('edrv:max-open-editors', onLimit)
|
|
1144
|
+
}, [])
|
|
1145
|
+
|
|
1146
|
+
// LRU 使用序维护:活动页签变化即标记;顺带把 used 表裁剪到当前页签集合,
|
|
1147
|
+
// 否则长会话中已关闭页签的条目会永久堆积(内存无界增长)。
|
|
1148
|
+
// ⚠️ 本 effect 必须声明在淘汰 effect 之前:先标记再淘汰,才能保证「刚打开/刚切到的
|
|
1149
|
+
// 页签」不会在同一次 commit 里被当作最久未用者淘汰。
|
|
1150
|
+
React.useEffect(() => {
|
|
1151
|
+
if (active) touchTab(active)
|
|
1152
|
+
const alive = new Set(tabs.map((t) => t.path))
|
|
1153
|
+
const used = tabUseRef.current.used
|
|
1154
|
+
for (const path of Object.keys(used)) if (!alive.has(path)) delete used[path]
|
|
1155
|
+
}, [tabs, active])
|
|
1156
|
+
|
|
1157
|
+
// 页签超限淘汰:关闭最久未使用的页签(固定页签与活动页签受保护;见 tabActions.evictPlan)。
|
|
1158
|
+
// 复用 closeTabs:它已实现 flushSave + persistDirty(脏页签先静默落盘)+ releaseTab + commitClose,
|
|
1159
|
+
// 故「超限淘汰脏页签」不会丢用户编辑。淘汰后 tabs 变化 → 本 effect 复算 evictPlan 返回空 → 自然收敛。
|
|
1160
|
+
React.useEffect(() => {
|
|
1161
|
+
const plan = evictPlan(tabsRef.current, tabLimit, { active: activeRef.current, used: tabUseRef.current.used })
|
|
1162
|
+
if (!plan.length) return
|
|
1163
|
+
closeTabs(applyClose(tabsRef.current, new Set(plan), activeRef.current), '已关闭最久未使用的页签')
|
|
1164
|
+
}, [tabs, tabLimit, active])
|
|
1165
|
+
|
|
1002
1166
|
// 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
|
|
1003
1167
|
// 宽度下限来自通用设置 sidebarMinWidth(默认 300),恢复值按其重夹
|
|
1004
1168
|
const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(scope)
|
|
@@ -1056,14 +1220,51 @@ export function EditorView(props) {
|
|
|
1056
1220
|
return () => window.removeEventListener('keydown', onKey, true)
|
|
1057
1221
|
}, [])
|
|
1058
1222
|
|
|
1059
|
-
|
|
1223
|
+
/**
|
|
1224
|
+
* 打开搜索面板(Ctrl+Shift+F 与命令栏「在工作区中搜索」的唯一动作)。
|
|
1225
|
+
*
|
|
1226
|
+
* 若编辑器有选区,把选区文本作为一次性种子交给搜索面板自动填入(需求 1);
|
|
1227
|
+
* 无选区/无编辑器时种子为空,行为与改动前一致(仅展开并聚焦)。
|
|
1228
|
+
* 种子经 searchSeed 模块交棒而非事件 detail:侧栏原本收起时搜索面板尚未挂载,
|
|
1229
|
+
* 事件 detail 会丢;面板挂载后自行取走槽内种子。
|
|
1230
|
+
* @author ddj 2026年09月18号
|
|
1231
|
+
*/
|
|
1232
|
+
const openSearchPanel = () => {
|
|
1233
|
+
const ed = editorRef.current
|
|
1234
|
+
const model = ed?.getModel?.()
|
|
1235
|
+
const sel = ed?.getSelection?.()
|
|
1236
|
+
let selected = ''
|
|
1237
|
+
if (model && sel && (sel.startLineNumber !== sel.endLineNumber || sel.startColumn !== sel.endColumn)) {
|
|
1238
|
+
// 选中区间文本(多行时 seedQueryOf 只取首行)
|
|
1239
|
+
selected = model.getValueInRange?.(sel) ?? ''
|
|
1240
|
+
}
|
|
1241
|
+
setSearchSeed(selected)
|
|
1242
|
+
setSidebarOn(true)
|
|
1243
|
+
setActivePanel('search')
|
|
1244
|
+
setTimeout(() => window.dispatchEvent(new CustomEvent('edrv:search-focus')), 0)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Ctrl+Shift+F 全局搜索:展开侧边栏 + 激活搜索页签,随后聚焦搜索输入框(有选中则填入)
|
|
1060
1248
|
React.useEffect(() => {
|
|
1061
1249
|
const onKey = (e) => {
|
|
1062
1250
|
if (!matchEvent(e, bindingsOf('edrv.searchInFiles'))) return
|
|
1063
1251
|
e.preventDefault(); e.stopPropagation()
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1252
|
+
openSearchPanel()
|
|
1253
|
+
}
|
|
1254
|
+
window.addEventListener('keydown', onKey, true)
|
|
1255
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
1256
|
+
}, [])
|
|
1257
|
+
|
|
1258
|
+
// Ctrl+Shift+V 切换 Markdown 预览:仅活动文件是 Markdown 时吞键;
|
|
1259
|
+
// 其余情况直接放行(不 preventDefault),保留浏览器/输入框原生的「无格式粘贴」语义
|
|
1260
|
+
// ——与 closeTab / addSelectionRef「不可用时放行按键」同款约定。
|
|
1261
|
+
React.useEffect(() => {
|
|
1262
|
+
const onKey = (e) => {
|
|
1263
|
+
if (!matchEvent(e, bindingsOf('edrv.toggleMarkdownPreview'))) return
|
|
1264
|
+
const path = activeRef.current
|
|
1265
|
+
if (!isMarkdownPath(path)) return
|
|
1266
|
+
e.preventDefault(); e.stopPropagation()
|
|
1267
|
+
toggleMdPreviewRef.current?.(path)
|
|
1067
1268
|
}
|
|
1068
1269
|
window.addEventListener('keydown', onKey, true)
|
|
1069
1270
|
return () => window.removeEventListener('keydown', onKey, true)
|
|
@@ -1181,10 +1382,11 @@ export function EditorView(props) {
|
|
|
1181
1382
|
['edrv.command.save', () => { if (editorRef.current?.getModel?.()) { flushSave(); doSaveRef.current?.(false) } }],
|
|
1182
1383
|
// 快速打开由 QuickOpen 自己接该事件(它持有搜索框 ref),此处不重复实现
|
|
1183
1384
|
['edrv.command.toggleSidebar', () => setSidebarOn((v) => !v)],
|
|
1184
|
-
['edrv.command.searchInFiles', () =>
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1385
|
+
['edrv.command.searchInFiles', () => openSearchPanel()],
|
|
1386
|
+
['edrv.command.toggleMarkdownPreview', () => {
|
|
1387
|
+
const path = activeRef.current
|
|
1388
|
+
if (!isMarkdownPath(path)) { setStatus('当前文件不是 Markdown'); return }
|
|
1389
|
+
toggleMdPreviewRef.current?.(path)
|
|
1188
1390
|
}],
|
|
1189
1391
|
['edrv.command.navigateBack', () => navBackRef.current?.()],
|
|
1190
1392
|
['edrv.command.navigateForward', () => navForwardRef.current?.()],
|
|
@@ -1226,6 +1428,20 @@ export function EditorView(props) {
|
|
|
1226
1428
|
['edrv.command.svnAdd', () => runSvnAdd(activeRef.current)],
|
|
1227
1429
|
['edrv.command.svnRevertCli', () => runSvnRevertCli(activeRef.current)],
|
|
1228
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
|
+
})],
|
|
1229
1445
|
['edrv.command.svnCleanup', () => runSvnAction(SVN_ACTION_BY_ID.cleanup, {
|
|
1230
1446
|
sessionId,
|
|
1231
1447
|
scope,
|
|
@@ -1265,6 +1481,10 @@ export function EditorView(props) {
|
|
|
1265
1481
|
else if (right > host.scrollLeft + host.clientWidth) host.scrollLeft = right - host.clientWidth
|
|
1266
1482
|
}, [active, tabs.length])
|
|
1267
1483
|
|
|
1484
|
+
// 页签栏滚轮横向滚动(需求 3):页签栏溢出时,鼠标滚轮(含触控板纵向手势)横滚页签栏,
|
|
1485
|
+
// 不溢出时不接管(保持页面垂直滚动);Shift+滚轮交由浏览器原生横滚。详见 horizontalWheel.ts。
|
|
1486
|
+
React.useEffect(() => attachHorizontalWheel(tabsHostRef.current), [])
|
|
1487
|
+
|
|
1268
1488
|
// 鼠标侧键后退/前进:Logitech 官方默认「后退/前进」= XButton 鼠标事件(button 3/4),不产生键盘事件。
|
|
1269
1489
|
// pointerdown 触发导航(preventDefault 后兼容 mousedown 可能不再触发,避免双触发);
|
|
1270
1490
|
// mousedown 兜底仅取消浏览器历史导航默认动作(MDN:preventDefault mousedown/pointerdown 可抑制)。
|
|
@@ -1326,6 +1546,8 @@ export function EditorView(props) {
|
|
|
1326
1546
|
setupAiInline(m)
|
|
1327
1547
|
// 代码片段补全 provider 注册(幂等;条目按会话工作区懒加载)
|
|
1328
1548
|
setupSnippets(m)
|
|
1549
|
+
// launch.json「添加配置」补全 provider 注册(幂等;仅 launch.json configurations 数组内出条目)
|
|
1550
|
+
setupLaunchJson(m)
|
|
1329
1551
|
}).catch((e) => {
|
|
1330
1552
|
if (alive) {
|
|
1331
1553
|
setMonacoErr(String(e?.message ?? e))
|
|
@@ -1359,10 +1581,7 @@ export function EditorView(props) {
|
|
|
1359
1581
|
monacoRef.current = monaco
|
|
1360
1582
|
|
|
1361
1583
|
/** 从编辑器实例 model URI 解析活动文件路径(工作区相对;无模型返回 null)。 */
|
|
1362
|
-
const modelPathOf = (edx) =>
|
|
1363
|
-
const uriPath = edx?.getModel?.()?.uri?.path
|
|
1364
|
-
return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
|
|
1365
|
-
}
|
|
1584
|
+
const modelPathOf = (edx) => editorModelPathOf(edx)
|
|
1366
1585
|
|
|
1367
1586
|
/**
|
|
1368
1587
|
* 同步 Monaco 右键菜单 SVN 组:先注销旧组,再按最新 svn 状态注册(空状态 = 全部移除)。
|
|
@@ -1401,6 +1620,7 @@ export function EditorView(props) {
|
|
|
1401
1620
|
path: def.id === 'update' ? (relPath ?? '') : relPath,
|
|
1402
1621
|
notify: (message) => setStatus(message),
|
|
1403
1622
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
1623
|
+
openSvnLocalPair: runSvnDiffLocalPair,
|
|
1404
1624
|
openSvnLog: (p) => openSvnLog(p ?? relPath),
|
|
1405
1625
|
refreshChanges: () => refreshSvnChanges(sessionId, scope),
|
|
1406
1626
|
})
|
|
@@ -1550,6 +1770,9 @@ export function EditorView(props) {
|
|
|
1550
1770
|
const ed = editorRef.current
|
|
1551
1771
|
if (!ed || !active) return
|
|
1552
1772
|
setDirtyMap((d) => Object.assign({}, d, { [active]: true }))
|
|
1773
|
+
// 编辑即「使用」:需求 2 的淘汰口径是「最近没有改动」,故有改动时刷新 LRU 序号,
|
|
1774
|
+
// 使长时间只在某文件里编辑的页签不会被其它文件的打开动作挤出。
|
|
1775
|
+
touchTab(active)
|
|
1553
1776
|
setStatus('编辑中…')
|
|
1554
1777
|
// 外部改动尚未处理(冲突/文件被删):抑制自动保存,避免用陈旧缓冲反复覆盖磁盘;
|
|
1555
1778
|
// 手工 Ctrl+S 不受抑制(doSave 会走版本守卫并给冲突提示)。
|
|
@@ -1564,6 +1787,9 @@ export function EditorView(props) {
|
|
|
1564
1787
|
// ⚠️ 必须以 contentReady(contentPath === active)为门,不能用 `content !== null`:
|
|
1565
1788
|
// 切页签的那一帧 content 仍是**上一个文件**的内容,据此建 model 会先塞入陈旧文本,
|
|
1566
1789
|
// 待真实内容到达再由 getModel→setValue 覆盖,光标随之被重置(见下方跳转 effect 注释)。
|
|
1790
|
+
// ⚠️ 依赖必须含 mdPreviewing:Markdown 预览态不渲染 Monaco host(见 body 分派的 mdPreviewing 分支),
|
|
1791
|
+
// 切回源码时 ensureEditor 会**新建**编辑器实例,而 active/content 都没变;缺此依赖则本 effect
|
|
1792
|
+
// 不重跑 → 空 model、行内差异标记消失。同款原因见下方差异自绘 effect。
|
|
1567
1793
|
React.useEffect(() => {
|
|
1568
1794
|
if (!monaco || !editorRef.current || !active || !contentReady) return
|
|
1569
1795
|
const ed = editorRef.current
|
|
@@ -1571,7 +1797,7 @@ export function EditorView(props) {
|
|
|
1571
1797
|
if (ed.getModel() !== model) ed.setModel(model)
|
|
1572
1798
|
restoreViewState(active)
|
|
1573
1799
|
setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
|
|
1574
|
-
}, [monaco, active, content, contentReady])
|
|
1800
|
+
}, [monaco, active, content, contentReady, mdPreviewing])
|
|
1575
1801
|
|
|
1576
1802
|
// PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
|
|
1577
1803
|
const ensurePdfHost = (node) => { pdfHostRef.current = node }
|
|
@@ -1598,11 +1824,13 @@ export function EditorView(props) {
|
|
|
1598
1824
|
}, [isPdfActive, pdfBytes, active])
|
|
1599
1825
|
|
|
1600
1826
|
// 行内差异自绘(decorations / view zones / minus overlay)→ diffRenderer
|
|
1827
|
+
// ⚠️ 依赖含 mdPreviewing:切回源码时编辑器实例被重建(见上方 model 同步 effect 的说明),
|
|
1828
|
+
// 不重跑则新实例上没有任何差异装饰,表现为「差异标记全部消失」。
|
|
1601
1829
|
React.useEffect(() => {
|
|
1602
1830
|
if (!monaco || !editorRef.current || !active || content === null) return
|
|
1603
1831
|
diffRendererRef.current.render(monaco, editorRef.current, pendingRegions, sessionId)
|
|
1604
1832
|
setLoadStage({ progress: 100, message: '编辑器已就绪' })
|
|
1605
|
-
}, [monaco, active, content, pendingRegions])
|
|
1833
|
+
}, [monaco, active, content, pendingRegions, mdPreviewing])
|
|
1606
1834
|
|
|
1607
1835
|
React.useEffect(() => () => {
|
|
1608
1836
|
flushSave()
|
|
@@ -1777,9 +2005,105 @@ export function EditorView(props) {
|
|
|
1777
2005
|
if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null }
|
|
1778
2006
|
setHoverAct({ region: hit, top, right })
|
|
1779
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)
|
|
1780
2069
|
editorRef.current = ed
|
|
1781
2070
|
}, [])
|
|
1782
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
|
+
|
|
1783
2107
|
/**
|
|
1784
2108
|
* 跳转落地后给目标区域挂临时高亮(默认 1.2s 后自动清除)。
|
|
1785
2109
|
*
|
|
@@ -1811,6 +2135,14 @@ export function EditorView(props) {
|
|
|
1811
2135
|
|
|
1812
2136
|
// 打开文件后的跳转(导航历史恢复/差异聚焦/搜索与 LSP 行列跳转,目标内容就绪后执行一次)
|
|
1813
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
|
+
//
|
|
1814
2146
|
// ⚠️ 门控必须用 contentReady(contentPath === active),不能用 `content === null`:
|
|
1815
2147
|
// 跨文件跳转时切页签的那一帧 content 仍是**上一个文件**的内容(非 null),据此判定
|
|
1816
2148
|
// 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
|
|
@@ -1852,7 +2184,7 @@ export function EditorView(props) {
|
|
|
1852
2184
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
1853
2185
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
1854
2186
|
ed.focus()
|
|
1855
|
-
}, [active, content, contentReady, pendingRegions, focusRequest])
|
|
2187
|
+
}, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing])
|
|
1856
2188
|
|
|
1857
2189
|
const jumpTo = (region) => {
|
|
1858
2190
|
if (editorRef.current) {
|
|
@@ -1982,6 +2314,32 @@ export function EditorView(props) {
|
|
|
1982
2314
|
loadContent(path, sessionId, true)
|
|
1983
2315
|
}
|
|
1984
2316
|
|
|
2317
|
+
/**
|
|
2318
|
+
* Markdown 在源码编辑与预览之间切换(按路径记忆,不持久化)。
|
|
2319
|
+
*
|
|
2320
|
+
* ⚠️ 进入预览前必须 flushSave():预览态会替换 Monaco host,切回时编辑器实例是新建的;
|
|
2321
|
+
* 若此刻还有未到期的 700ms 防抖保存,定时器到点时 editorRef.current 已被清空
|
|
2322
|
+
* (见 EditorView 卸载清理),doSave 会静默 return —— 用户改动永不落盘。
|
|
2323
|
+
* 同时保存视图状态,保证切回源码后光标/滚动位置不丢。
|
|
2324
|
+
* @author ddj 2026年09月18号
|
|
2325
|
+
* @param path Markdown 文件路径
|
|
2326
|
+
*/
|
|
2327
|
+
const toggleMdPreview = (path) => {
|
|
2328
|
+
if (!path) return
|
|
2329
|
+
const next = new Set(mdPreviewRef.current)
|
|
2330
|
+
const entering = !next.has(path)
|
|
2331
|
+
if (entering) {
|
|
2332
|
+
flushSave()
|
|
2333
|
+
saveViewState(path)
|
|
2334
|
+
next.add(path)
|
|
2335
|
+
} else {
|
|
2336
|
+
next.delete(path)
|
|
2337
|
+
}
|
|
2338
|
+
mdPreviewRef.current = next
|
|
2339
|
+
setStatus(entering ? '已切换为 Markdown 预览' : '已切换为源码编辑')
|
|
2340
|
+
}
|
|
2341
|
+
toggleMdPreviewRef.current = toggleMdPreview
|
|
2342
|
+
|
|
1985
2343
|
/**
|
|
1986
2344
|
* 图片预览面板:工具条(尺寸 meta / SVG 文本切换 / 刷新)+ 棋盘底自适应图片。
|
|
1987
2345
|
* @author ddj 2026年09月08号
|
|
@@ -2243,6 +2601,8 @@ export function EditorView(props) {
|
|
|
2243
2601
|
leftLabel: 'BASE',
|
|
2244
2602
|
rightLabel: '工作区',
|
|
2245
2603
|
scheme: 'edrv-svn-base',
|
|
2604
|
+
binary: outcome.binary === true,
|
|
2605
|
+
encodingHint: outcome.encodingHint === true,
|
|
2246
2606
|
})
|
|
2247
2607
|
setStatus(outcome.message)
|
|
2248
2608
|
})
|
|
@@ -2254,8 +2614,9 @@ export function EditorView(props) {
|
|
|
2254
2614
|
* @author ddj 2026年09月16号
|
|
2255
2615
|
* @param relPath 目标工作区相对路径
|
|
2256
2616
|
* @param revision 目标版本
|
|
2617
|
+
* @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
|
|
2257
2618
|
*/
|
|
2258
|
-
const runSvnDiffRev = (relPath, revision) => {
|
|
2619
|
+
const runSvnDiffRev = (relPath, revision, logTarget) => {
|
|
2259
2620
|
if (!relPath) { setStatus('无目标文件'); return }
|
|
2260
2621
|
setStatus('读取 r' + revision + ' 差异…')
|
|
2261
2622
|
void svnDiffRev(sessionId, relPath, revision).then((outcome) => {
|
|
@@ -2270,6 +2631,9 @@ export function EditorView(props) {
|
|
|
2270
2631
|
leftLabel: 'r' + Math.max(1, revision - 1),
|
|
2271
2632
|
rightLabel: 'r' + revision,
|
|
2272
2633
|
scheme: 'edrv-svn-rev' + revision,
|
|
2634
|
+
binary: outcome.binary === true,
|
|
2635
|
+
encodingHint: outcome.encodingHint === true,
|
|
2636
|
+
logTarget,
|
|
2273
2637
|
})
|
|
2274
2638
|
setStatus('已打开 r' + revision + ' 差异')
|
|
2275
2639
|
})
|
|
@@ -2282,8 +2646,9 @@ export function EditorView(props) {
|
|
|
2282
2646
|
* @param relPath 目标工作区相对路径
|
|
2283
2647
|
* @param revA 左侧版本(选中顺序在前)
|
|
2284
2648
|
* @param revB 右侧版本(选中顺序在后)
|
|
2649
|
+
* @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
|
|
2285
2650
|
*/
|
|
2286
|
-
const runSvnDiffPair = (relPath, revA, revB) => {
|
|
2651
|
+
const runSvnDiffPair = (relPath, revA, revB, logTarget) => {
|
|
2287
2652
|
if (!relPath) { setStatus('仅文件目标支持比较两个修订'); return }
|
|
2288
2653
|
setStatus('读取 r' + revA + ' ↔ r' + revB + ' 差异…')
|
|
2289
2654
|
void svnDiffPair(sessionId, relPath, revA, revB).then((outcome) => {
|
|
@@ -2300,6 +2665,9 @@ export function EditorView(props) {
|
|
|
2300
2665
|
leftLabel: 'r' + revA,
|
|
2301
2666
|
rightLabel: 'r' + revB,
|
|
2302
2667
|
scheme: 'edrv-svn-pair-' + revA + '-' + revB,
|
|
2668
|
+
binary: outcome.binary === true,
|
|
2669
|
+
encodingHint: outcome.encodingHint === true,
|
|
2670
|
+
logTarget,
|
|
2303
2671
|
})
|
|
2304
2672
|
setStatus('已打开 r' + revA + ' ↔ r' + revB + ' 差异')
|
|
2305
2673
|
})
|
|
@@ -2311,8 +2679,9 @@ export function EditorView(props) {
|
|
|
2311
2679
|
* @author ddj 2026年09月17号
|
|
2312
2680
|
* @param relPath 目标工作区相对路径
|
|
2313
2681
|
* @param revision 左侧版本
|
|
2682
|
+
* @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
|
|
2314
2683
|
*/
|
|
2315
|
-
const runSvnDiffWorking = (relPath, revision) => {
|
|
2684
|
+
const runSvnDiffWorking = (relPath, revision, logTarget) => {
|
|
2316
2685
|
if (!relPath) { setStatus('仅文件目标支持与工作副本比较'); return }
|
|
2317
2686
|
setStatus('读取 r' + revision + ' ↔ 工作副本 差异…')
|
|
2318
2687
|
void svnDiffWorking(sessionId, relPath, revision).then((outcome) => {
|
|
@@ -2326,11 +2695,42 @@ export function EditorView(props) {
|
|
|
2326
2695
|
leftLabel: 'r' + revision,
|
|
2327
2696
|
rightLabel: '工作区',
|
|
2328
2697
|
scheme: 'edrv-svn-work-' + revision,
|
|
2698
|
+
binary: outcome.binary === true,
|
|
2699
|
+
encodingHint: outcome.encodingHint === true,
|
|
2700
|
+
logTarget,
|
|
2329
2701
|
})
|
|
2330
2702
|
setStatus('已打开 r' + revision + ' ↔ 工作副本 差异')
|
|
2331
2703
|
})
|
|
2332
2704
|
}
|
|
2333
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
|
+
|
|
2334
2734
|
/**
|
|
2335
2735
|
* 打开日志弹窗(P3):加载该目标的提交历史。
|
|
2336
2736
|
* @author ddj 2026年09月16号
|
|
@@ -2346,6 +2746,17 @@ export function EditorView(props) {
|
|
|
2346
2746
|
ensureSvnLog(sessionId, scope, target, SVN_LOG_PAGE)
|
|
2347
2747
|
}
|
|
2348
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
|
+
|
|
2349
2760
|
/** 加入版本控制(活动文件;未纳入版本控制的文件才有意义,host 侧会再校验)。 */
|
|
2350
2761
|
const runSvnAdd = (relPath) => {
|
|
2351
2762
|
if (!relPath) { setStatus('无活动文件'); return }
|
|
@@ -2448,6 +2859,7 @@ export function EditorView(props) {
|
|
|
2448
2859
|
path,
|
|
2449
2860
|
notify: (message) => setStatus(message),
|
|
2450
2861
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
2862
|
+
openSvnLocalPair: runSvnDiffLocalPair,
|
|
2451
2863
|
openSvnLog: (p) => openSvnLog(p ?? path),
|
|
2452
2864
|
refreshChanges: () => refreshSvnChanges(sessionId, scope),
|
|
2453
2865
|
})
|
|
@@ -2545,6 +2957,8 @@ export function EditorView(props) {
|
|
|
2545
2957
|
*/
|
|
2546
2958
|
const openFileAt = (path, line, column, endLine, endColumn) => {
|
|
2547
2959
|
if (!path) return
|
|
2960
|
+
const tracePath = tabPathOf(path, cwd)
|
|
2961
|
+
dapTrace('open-file-at', { input: path, normalized: tracePath, line, cwd })
|
|
2548
2962
|
recordNav()
|
|
2549
2963
|
// G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
|
|
2550
2964
|
const normalized = addTabNorm(path, true)
|
|
@@ -2553,6 +2967,51 @@ export function EditorView(props) {
|
|
|
2553
2967
|
setFocusRequest((value) => value + 1)
|
|
2554
2968
|
}
|
|
2555
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
|
+
|
|
2556
3015
|
const openPath = () => {
|
|
2557
3016
|
const p = (pathDraft || '').trim()
|
|
2558
3017
|
if (!p) return
|
|
@@ -2598,6 +3057,13 @@ export function EditorView(props) {
|
|
|
2598
3057
|
const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
|
|
2599
3058
|
React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
|
|
2600
3059
|
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开文件'),
|
|
3060
|
+
// Markdown 预览/源码切换(需求 4):仅 .md 文件出现;命令栏与 Ctrl+Shift+V 为同一动作
|
|
3061
|
+
(isMdActive ? React.createElement('button', {
|
|
3062
|
+
className: 'edrv-pill edrv-pill-ghost edrv-pb-mdbtn',
|
|
3063
|
+
title: (mdPreviewing ? '切回源码编辑' : '预览 Markdown') + '(' + (chordOf('edrv.toggleMarkdownPreview') ?? '未绑定') + ')',
|
|
3064
|
+
onClick: () => toggleMdPreview(active),
|
|
3065
|
+
}, mdPreviewing ? '源码' : '预览') : null),
|
|
3066
|
+
(mdPreviewing ? React.createElement('span', { className: 'edrv-pb-meta' }, '预览') : null),
|
|
2601
3067
|
(active && !isImageActive && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
|
|
2602
3068
|
(isImageActive && imgSize ? React.createElement('span', { className: 'edrv-pb-meta' }, imgSize.w + '×' + imgSize.h) : null),
|
|
2603
3069
|
(cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
|
|
@@ -2757,6 +3223,7 @@ export function EditorView(props) {
|
|
|
2757
3223
|
message: svnDiff.message,
|
|
2758
3224
|
leftLabel: svnDiff.leftLabel,
|
|
2759
3225
|
rightLabel: svnDiff.rightLabel,
|
|
3226
|
+
onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
|
|
2760
3227
|
onClose: () => setSvnDiff(null),
|
|
2761
3228
|
})
|
|
2762
3229
|
} else if (!monaco && !monacoErr) {
|
|
@@ -2786,6 +3253,17 @@ export function EditorView(props) {
|
|
|
2786
3253
|
body = React.createElement('div', { className: 'edrv-pdf-host', ref: ensurePdfHost, key: active })
|
|
2787
3254
|
} else if (isPdfActive) {
|
|
2788
3255
|
body = loadingBody(loadStage.message || '读取 PDF…', loadStage.progress)
|
|
3256
|
+
} else if (mdPreviewing && contentReady) {
|
|
3257
|
+
// Markdown 预览(需求 4):复用已加载的文本内容渲染,不额外请求;编辑仍在源码态完成
|
|
3258
|
+
body = React.createElement(MarkdownPanel, {
|
|
3259
|
+
key: 'edrv-md-' + active,
|
|
3260
|
+
text: content,
|
|
3261
|
+
onToggleSource: () => toggleMdPreview(active),
|
|
3262
|
+
onReload: () => reloadFile(),
|
|
3263
|
+
onOpenFile: (path, line) => openFileAt(path, line ?? 1),
|
|
3264
|
+
})
|
|
3265
|
+
} else if (mdPreviewing) {
|
|
3266
|
+
body = loadingBody(loadStage.message || '读取 Markdown…', loadStage.progress)
|
|
2789
3267
|
} else if (content === null) {
|
|
2790
3268
|
body = loadingBody(loadStage.message || '读取文件内容…', loadStage.progress)
|
|
2791
3269
|
} else {
|
|
@@ -2910,14 +3388,125 @@ export function EditorView(props) {
|
|
|
2910
3388
|
svnChanges,
|
|
2911
3389
|
svnChangeMap: svnChangeMapOf(scope),
|
|
2912
3390
|
openSvnDiff: (p) => runSvnDiffBase(p),
|
|
3391
|
+
openSvnLocalPair: runSvnDiffLocalPair,
|
|
2913
3392
|
refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
|
|
2914
3393
|
openSvnLog: (p) => openSvnLog(p),
|
|
2915
3394
|
confirm: (message) => (typeof window === 'undefined' ? false : window.confirm(message)),
|
|
2916
3395
|
notify: (message) => setStatus(message),
|
|
2917
3396
|
}
|
|
2918
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
|
+
|
|
2919
3506
|
// 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
|
|
2920
3507
|
const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
|
|
3508
|
+
dapBar,
|
|
3509
|
+
addCfgBtn,
|
|
2921
3510
|
body,
|
|
2922
3511
|
hoverEl,
|
|
2923
3512
|
overlay)
|
|
@@ -3062,31 +3651,124 @@ export function EditorView(props) {
|
|
|
3062
3651
|
setSvnLog({ ...svnLog, limit: SVN_LOG_SHOW_ALL_LIMIT })
|
|
3063
3652
|
ensureSvnLog(sessionId, scope, svnLog.target || '', SVN_LOG_SHOW_ALL_LIMIT, true, svnLogOptsOf(svnLog))
|
|
3064
3653
|
},
|
|
3065
|
-
onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision) },
|
|
3066
|
-
onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB) },
|
|
3067
|
-
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 || '') },
|
|
3068
3657
|
onOpenFile: (relPath) => { setSvnLog(null); openFile(relPath, false) },
|
|
3069
3658
|
onClose: () => { setSvnLog(null); setSvnLogError('') },
|
|
3070
3659
|
})
|
|
3071
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
|
|
3072
3682
|
// 诊断日志弹窗:portal 到 body,与命令栏/SVN 日志弹窗同一挂载方式
|
|
3073
3683
|
const dlogEl = dlogOpen
|
|
3074
3684
|
? React.createElement(LogDialog, { key: 'edrv-dlog', sessionId, onClose: () => setDlogOpen(false) })
|
|
3075
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
|
|
3076
3750
|
const rootEl = layout === 'side'
|
|
3077
3751
|
? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
|
|
3078
3752
|
editorRow,
|
|
3079
3753
|
tabMenuEl,
|
|
3080
3754
|
paletteEl,
|
|
3081
3755
|
svnLogEl,
|
|
3756
|
+
patchDialogEl,
|
|
3757
|
+
sumDialogEl,
|
|
3082
3758
|
dlogEl,
|
|
3083
|
-
snippetPickerEl
|
|
3759
|
+
snippetPickerEl,
|
|
3760
|
+
dapBpMenuEl,
|
|
3761
|
+
dapBpEditEl)
|
|
3084
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%)' }) },
|
|
3085
3763
|
editorRow,
|
|
3086
3764
|
tabMenuEl,
|
|
3087
3765
|
paletteEl,
|
|
3088
3766
|
svnLogEl,
|
|
3767
|
+
patchDialogEl,
|
|
3768
|
+
sumDialogEl,
|
|
3089
3769
|
dlogEl,
|
|
3090
|
-
snippetPickerEl
|
|
3770
|
+
snippetPickerEl,
|
|
3771
|
+
dapBpMenuEl,
|
|
3772
|
+
dapBpEditEl)
|
|
3091
3773
|
return rootEl
|
|
3092
3774
|
}
|