dsh-vscode-mode 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +62 -11
  2. package/lib/client.js +2879 -1030
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2227 -1056
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/activePathStore.ts +28 -0
  8. package/src/client/commandBridge.ts +9 -41
  9. package/src/client/dap/hover.ts +2 -7
  10. package/src/client/index.ts +116 -14
  11. package/src/client/keybindings.ts +19 -73
  12. package/src/client/luaReservedWords.ts +10 -0
  13. package/src/client/monaco/lsp/keywords.ts +121 -0
  14. package/src/client/monaco/lsp/lspClient.ts +68 -6
  15. package/src/client/monaco/lsp/memberScope.ts +90 -0
  16. package/src/client/monaco/lsp/providers.ts +14 -4
  17. package/src/client/settingsDocRouter.ts +52 -0
  18. package/src/client/shortcutsOfficial.ts +451 -0
  19. package/src/client/sidebar/menuItems.ts +1 -0
  20. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  21. package/src/client/sidebar/types.ts +2 -0
  22. package/src/client/styles/editor.css +143 -37
  23. package/src/client/svnActions.ts +4 -4
  24. package/src/client/ui/ContextMenu.ts +4 -2
  25. package/src/client/ui/EditorView.ts +142 -135
  26. package/src/client/ui/McpSettings.ts +4 -5
  27. package/src/client/ui/QuickOpen.ts +10 -11
  28. package/src/client/ui/ShortcutSettings.ts +294 -0
  29. package/src/client/ui/SvnLogDialog.ts +200 -69
  30. package/src/client/ui/SvnUpdateDialog.ts +472 -0
  31. package/src/client/ui/commandCatalog.ts +36 -41
  32. package/src/client/ui/svnUpdateModel.ts +135 -0
  33. package/src/fileOpenSettings.ts +3 -0
  34. package/src/index.ts +2 -0
  35. package/src/lsp/completionCap.ts +80 -0
  36. package/src/lsp/manager.ts +42 -10
  37. package/src/lsp/rpc.ts +8 -5
  38. package/src/lsp/server.ts +18 -4
  39. package/src/rpc.ts +74 -2
  40. package/src/rules.ts +100 -44
  41. package/src/settingsDocPath.ts +30 -0
  42. package/src/shared/keybindings.ts +253 -0
  43. package/src/shared/rpc.ts +12 -2
  44. package/src/shared/rules.ts +9 -1
  45. package/src/shared/svn.ts +38 -0
  46. package/src/svn.ts +102 -7
  47. package/src/svnText.ts +22 -13
  48. package/src/svnUpdateJob.ts +588 -0
  49. package/src/client/ui/KeybindingsPanel.ts +0 -190
@@ -13,6 +13,7 @@ import { emitFileChanged, emitRefresh } from '../events.js'
13
13
  import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
14
14
  import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
15
15
  import { isMarkdownPath } from '../markdownPreview.js'
16
+ import { setActiveEditorPath } from '../activePathStore.js'
16
17
  import { MarkdownPanel } from '../md/mdPanel.js'
17
18
  import { base64ToBytes, bytesToBase64, isPdfPath } from '../pdfPreview.js'
18
19
  import { inNativePath, tryNativeOpen } from '../nativeOpenStore.js'
@@ -49,13 +50,14 @@ import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewSt
49
50
  import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
50
51
  import { modelsForScope, rememberModel } from '../state/modelCache.js'
51
52
  import { navFlashRangeOf } from '../navHighlight.js'
52
- import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
53
+ import { chordOf, useKeybindingsVersion } from '../keybindings.js'
53
54
  import { getSidebarMinWidth } from '../sidebarMin.js'
54
55
  import { navHistoryFor } from '../navHistory.js'
55
56
  import { statusOfAdd } from '../addToConversation.js'
56
57
  import { setSearchScope, setSearchSeed } from '../searchSeed.js'
57
58
  import { CACHE_KEY } from '../paths.js'
58
59
  import { runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
60
+ import { isLuaTypeSlot } from '../monaco/lsp/keywords.js'
59
61
  import { bindLspUnderline } from '../monaco/lsp/underline.js'
60
62
  import { onLspProgress, refreshStatus, setSession as setLspSession } from '../monaco/lsp/index.js'
61
63
  import { setupAiInline, trackAiEditor, aiInlineEnabled } from '../ai/inlineProvider.js'
@@ -69,7 +71,7 @@ import {
69
71
  } from '../tabActions.js'
70
72
  import { getMaxOpenEditors } from '../editorLimit.js'
71
73
  import { buildTabMenu } from '../tabMenu.js'
72
- import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnDiffLocalPair, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
74
+ import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnDiffLocalPair, svnEditorActions, svnRevert, svnTortoise } from '../svnStatus.js'
73
75
  import { ensureSvnLog, getSvnLog, refreshSvnLog, svnDiffPair, svnDiffRev, svnDiffWorking, svnLogKeyOf, svnLogLoadedLimit, svnLogTruncated, svnWcRev, SVN_LOG_SHOW_ALL_LIMIT } from '../svnLog.js'
74
76
  import { runSvnAction } from '../svnActions.js'
75
77
  import { dshTrace } from '../svnStore.js'
@@ -82,6 +84,9 @@ import { promptName } from './PromptDialog.js'
82
84
  import { openWithDialog } from '../openWithDialog.js'
83
85
  import { copyText as copyClipText } from '../copyText.js'
84
86
  import { SvnLogDialog } from './SvnLogDialog.js'
87
+ import { SvnUpdateDialog } from './SvnUpdateDialog.js'
88
+ import { sameUpdateOwner, updateDone } from './svnUpdateModel.js'
89
+ import { IconRefreshOutline16 } from './icons.js'
85
90
  import { SvnPatchDialog } from './SvnPatchDialog.js'
86
91
  import { SvnSumDialog } from './SvnSumDialog.js'
87
92
  import { LogDialog } from './LogDialog.js'
@@ -95,6 +100,23 @@ import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '
95
100
  import { editorModelPathOf } from '../dap/modelPath.js'
96
101
  import { dapTrace } from '../dap/trace.js'
97
102
 
103
+ /**
104
+ * 只在 Lua 注解类型槽开启注释自动建议;跨文件/离开类型槽立即恢复默认。
105
+ * @author ddj 2026年09月24号
106
+ * @param ed 当前 Monaco 编辑器
107
+ * @param monaco Monaco API(读取 quickSuggestions 枚举)
108
+ */
109
+ function syncTypeSuggest(ed, monaco) {
110
+ const model = ed.getModel()
111
+ const position = ed.getPosition()
112
+ const typeSlot = model?.getLanguageId() === 'lua' && position
113
+ && isLuaTypeSlot(model, position, model.getWordUntilPosition(position))
114
+ const options = ed.getOption(monaco.editor.EditorOption.quickSuggestions)
115
+ const comments = typeSlot ? 'on' : 'off'
116
+ if (options.comments === comments) return
117
+ ed.updateOptions({ quickSuggestions: { ...options, comments } })
118
+ }
119
+
98
120
  /** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
99
121
  const SVN_LOG_PAGE = 100
100
122
 
@@ -252,7 +274,30 @@ export function EditorView(props) {
252
274
  const [svnLogError, setSvnLogError] = React.useState('') // 日志错误文案
253
275
  const [svnLogWcRev, setSvnLogWcRev] = React.useState(null) // P1-9:文件目标的工作副本版号(null = 不加粗)
254
276
  const [dlogOpen, setDlogOpen] = React.useState(false) // 诊断日志弹窗(自取数据,null 语义无需状态层)
255
- const [svnUpdateResult, setSvnUpdateResult] = React.useState(null) // update 条目级结果条(null=不显示)
277
+ const [svnUpdateDialog, setSvnUpdateDialog] = React.useState(null) // {path,sessionId,scope,resumeJobId?};窗口内自取增量
278
+ const [svnUpdateReturn, setSvnUpdateReturn] = React.useState(null) // 从日志/差异返回时恢复同一 job
279
+ const [recentUpdate, setRecentUpdate] = React.useState(null) // {jobId,path,sessionId,scope};只读恢复入口
280
+ const [updateOpenWith, setUpdateOpenWith] = React.useState(false) // 二级弹窗 Esc 不联动关闭父窗口
281
+ const svnUpdateDoneRef = React.useRef(new Set()) // job 终态刷新一次,往返日志/差异不重复刷新
282
+ // @author ddj 2026年09月24号:切换作用域只关闭窗口,不取消旧工作副本中已启动的任务
283
+ React.useEffect(() => {
284
+ setSvnUpdateDialog(null)
285
+ setSvnUpdateReturn(null)
286
+ setRecentUpdate(null)
287
+ setUpdateOpenWith(false)
288
+ }, [sessionId, scope])
289
+ // @author ddj 2026年09月24号:只读恢复原工作区的活动更新,不调用 updateStart
290
+ React.useEffect(() => {
291
+ if (!sessionId || svnStatus?.managed !== true) return undefined
292
+ let alive = true
293
+ void rpc('svn.updateActive', { sessionId }).then((outcome) => {
294
+ if (!alive || !outcome.ok || !outcome.job) return
295
+ const recovered = { path: outcome.job.target, sessionId, scope, resumeJobId: outcome.job.jobId }
296
+ if (updateDone(outcome.job.phase)) setRecentUpdate(recovered)
297
+ else setSvnUpdateDialog((current) => current ?? recovered)
298
+ }).catch(() => {})
299
+ return () => { alive = false }
300
+ }, [sessionId, scope, svnStatus?.managed])
256
301
  const [sidebarOn, setSidebarOn] = React.useState(layout !== 'side') // 侧边栏显隐(侧栏形态默认收起)
257
302
  const [sidebarW, setSidebarW] = React.useState(() => getSidebarMinWidth()) // 侧边栏宽度(初值 = 最小宽度,默认 300)
258
303
  const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
@@ -291,6 +336,9 @@ export function EditorView(props) {
291
336
  const rowNavMoveRef = React.useRef(false) // 本次光标变化是否由整行移动触发(否则清空期望列)
292
337
  const activeRef = React.useRef(null) // 当前活动文件的最新值(空依赖闭包/指令回调读取)
293
338
  activeRef.current = active
339
+ // 活动文件路径镜像到模块级 store:命令目录可用性判定读取(如 Markdown 预览切换仅 .md 吞键)
340
+ setActiveEditorPath(active)
341
+ React.useEffect(() => () => setActiveEditorPath(null), [])
294
342
  const tabsRef = React.useRef([]) // 当前页签表的最新值(菜单动作按 id 分派时读,防陈旧闭包)
295
343
  tabsRef.current = tabs
296
344
  const tabPathsRef = React.useRef([]) // 页签路径镜像(外部改动轮询读取)
@@ -1296,29 +1344,10 @@ export function EditorView(props) {
1296
1344
  try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
1297
1345
  catch (e) { /* 忽略 */ }
1298
1346
  }, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
1299
- // 切换侧边栏(capture 抢占,避免与 DSH 全局冲突;键位随快捷键配置)
1300
- React.useEffect(() => {
1301
- const onKey = (e) => {
1302
- if (!matchEvent(e, bindingsOf('edrv.toggleSidebar'))) return
1303
- e.preventDefault(); e.stopPropagation()
1304
- setSidebarOn((v) => !v)
1305
- }
1306
- window.addEventListener('keydown', onKey, true)
1307
- return () => window.removeEventListener('keydown', onKey, true)
1308
- }, [])
1309
-
1310
- // 保存文件(窗口级):编辑器有活动模型才拦截执行,无文件时放行(浏览器默认行为)
1311
- React.useEffect(() => {
1312
- const onKey = (e) => {
1313
- if (!matchEvent(e, bindingsOf('edrv.save'))) return
1314
- if (!editorRef.current?.getModel?.()) return
1315
- e.preventDefault(); e.stopPropagation()
1316
- flushSave()
1317
- doSaveRef.current(false)
1318
- }
1319
- window.addEventListener('keydown', onKey, true)
1320
- return () => window.removeEventListener('keydown', onKey, true)
1321
- }, [])
1347
+ // 编辑器键位(侧栏/保存/搜索/Markdown 预览/导航/页签循环)已归官方 shortcuts 机制派发:
1348
+ // 官方 resolve → 指令注册表 run → `edrv.command.*` 事件 → 下方「指令系统接线」表执行。
1349
+ // 历史 capture 监听移除(与官方派发并存会双执行);Markdown 预览的「非 .md 不吞键」
1350
+ // 语义由命令 available()(activePathStore + isMarkdownPath)承载,可用性为假时官方派发放行。
1322
1351
 
1323
1352
  /**
1324
1353
  * 打开搜索面板(Ctrl+Shift+F 与命令栏「在工作区中搜索」的唯一动作)。
@@ -1344,65 +1373,7 @@ export function EditorView(props) {
1344
1373
  setTimeout(() => window.dispatchEvent(new CustomEvent('edrv:search-focus')), 0)
1345
1374
  }
1346
1375
 
1347
- // Ctrl+Shift+F 全局搜索:展开侧边栏 + 激活搜索页签,随后聚焦搜索输入框(有选中则填入)
1348
- React.useEffect(() => {
1349
- const onKey = (e) => {
1350
- if (!matchEvent(e, bindingsOf('edrv.searchInFiles'))) return
1351
- e.preventDefault(); e.stopPropagation()
1352
- openSearchPanel()
1353
- }
1354
- window.addEventListener('keydown', onKey, true)
1355
- return () => window.removeEventListener('keydown', onKey, true)
1356
- }, [])
1357
-
1358
- // Ctrl+Shift+V 切换 Markdown 预览:仅活动文件是 Markdown 时吞键;
1359
- // 其余情况直接放行(不 preventDefault),保留浏览器/输入框原生的「无格式粘贴」语义
1360
- // ——与 closeTab / addSelectionRef「不可用时放行按键」同款约定。
1361
- React.useEffect(() => {
1362
- const onKey = (e) => {
1363
- if (!matchEvent(e, bindingsOf('edrv.toggleMarkdownPreview'))) return
1364
- const path = activeRef.current
1365
- if (!isMarkdownPath(path)) return
1366
- e.preventDefault(); e.stopPropagation()
1367
- toggleMdPreviewRef.current?.(path)
1368
- }
1369
- window.addEventListener('keydown', onKey, true)
1370
- return () => window.removeEventListener('keydown', onKey, true)
1371
- }, [])
1372
-
1373
- // 后退/前进:导航历史(跨文件焦点位置;经 ref 读最新闭包,避免空依赖过期)
1374
- React.useEffect(() => {
1375
- const onKey = (e) => {
1376
- if (!matchEvent(e, bindingsOf('edrv.navigateBack'))) return
1377
- e.preventDefault(); e.stopPropagation()
1378
- if (navBackRef.current) navBackRef.current()
1379
- }
1380
- window.addEventListener('keydown', onKey, true)
1381
- return () => window.removeEventListener('keydown', onKey, true)
1382
- }, [])
1383
- React.useEffect(() => {
1384
- const onKey = (e) => {
1385
- if (!matchEvent(e, bindingsOf('edrv.navigateForward'))) return
1386
- e.preventDefault(); e.stopPropagation()
1387
- if (navForwardRef.current) navForwardRef.current()
1388
- }
1389
- window.addEventListener('keydown', onKey, true)
1390
- return () => window.removeEventListener('keydown', onKey, true)
1391
- }, [])
1392
-
1393
- // 页签循环切换:Ctrl+Alt+←/→(主候选,浏览器不占用)或 Ctrl+PgUp/PgDn(部分宿主可用)
1394
- React.useEffect(() => {
1395
- const onKey = (e) => {
1396
- const step = matchEvent(e, bindingsOf('edrv.nextTab')) ? 1
1397
- : matchEvent(e, bindingsOf('edrv.prevTab')) ? -1
1398
- : 0
1399
- if (step === 0) return
1400
- e.preventDefault(); e.stopPropagation()
1401
- if (cycleTabRef.current) cycleTabRef.current(step)
1402
- }
1403
- window.addEventListener('keydown', onKey, true)
1404
- return () => window.removeEventListener('keydown', onKey, true)
1405
- }, [])
1376
+ // Ctrl+Shift+F / Ctrl+Shift+V / Alt+←→ / Ctrl+Alt+←→ 的 capture 监听已随官方派发改造移除(见上)。
1406
1377
 
1407
1378
  // 整行上下移动的实现见下方「指令系统接线」effect(moveRow 单点定义,命令栏与键位共用)。
1408
1379
 
@@ -1727,6 +1698,7 @@ export function EditorView(props) {
1727
1698
  sessionId,
1728
1699
  scope,
1729
1700
  path: def.id === 'update' ? (relPath ?? '') : relPath,
1701
+ openSvnUpdate: (p) => runSvnUpdate(p),
1730
1702
  notify: (message) => setStatus(message),
1731
1703
  openSvnDiff: (p) => runSvnDiffBase(p),
1732
1704
  openSvnLocalPair: runSvnDiffLocalPair,
@@ -1904,6 +1876,7 @@ export function EditorView(props) {
1904
1876
  const ed = editorRef.current
1905
1877
  const model = getModel(active, content)
1906
1878
  if (ed.getModel() !== model) ed.setModel(model)
1879
+ syncTypeSuggest(ed, monaco)
1907
1880
  restoreViewState(active)
1908
1881
  setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
1909
1882
  }, [monaco, active, content, contentReady, mdPreviewing])
@@ -1997,9 +1970,11 @@ export function EditorView(props) {
1997
1970
  // 保存改由窗口级快捷键监听执行(键位可配置;见上方 edrv.save 监听)
1998
1971
  ed.onDidChangeModelContent(() => {
1999
1972
  if (!ed.getModel() || programmaticRef.current) return
1973
+ syncTypeSuggest(ed, m)
2000
1974
  onEditRef.current?.()
2001
1975
  })
2002
1976
  ed.onDidChangeCursorPosition((e) => {
1977
+ syncTypeSuggest(ed, m)
2003
1978
  setCursor('Ln ' + e.position.lineNumber + ', Col ' + e.position.column)
2004
1979
  // 整行移动(↓↑)刚定位时保留期望列;其余光标移动(点击/打字/方向键)清空期望列
2005
1980
  if (rowNavMoveRef.current) rowNavMoveRef.current = false
@@ -2740,17 +2715,15 @@ export function EditorView(props) {
2740
2715
 
2741
2716
  // 供 Monaco 原生右键菜单 addAction 读取的最新动作闭包(空依赖回调不随渲染重建)
2742
2717
  /**
2743
- * SVN 更新(CLI,全平台):页签菜单/命令面板/编辑区右键共用;path 缺省 = 工作区根。
2744
- * P3:结果含条目级明细,成功后展示可关闭的结果条(冲突红字高亮)。
2718
+ * 四入口共用的更新窗口:仅显式点击触发,不在背景静默更新。
2719
+ * @author ddj 2026年09月24号
2720
+ * @param relPath 工作区相对目标;空串代表根
2745
2721
  */
2746
2722
  const runSvnUpdate = (relPath) => {
2747
- void svnUpdate(sessionId, relPath ?? '').then((o) => {
2748
- setStatus(o.message)
2749
- // 更新会改工作副本状态:成功后重查变更清单(徽标/面板跟随)
2750
- if (o.ok) refreshSvnChanges(sessionId, scope)
2751
- // P3:有条目级明细时展示结果条(无变化则清空,不留旧结果)
2752
- setSvnUpdateResult(o.ok ? { summary: o.message, entries: o.entries ?? [], conflicts: o.conflicts ?? [] } : null)
2753
- })
2723
+ closeCommandPalette()
2724
+ setSvnUpdateReturn(null)
2725
+ setRecentUpdate(null)
2726
+ setSvnUpdateDialog({ path: relPath ?? '', sessionId, scope })
2754
2727
  }
2755
2728
  /** 强制重查 SVN 变更清单(命令栏「SVN 刷新变更」)。 */
2756
2729
  const runSvnReload = () => {
@@ -2857,8 +2830,9 @@ export function EditorView(props) {
2857
2830
  * @param relPath 目标工作区相对路径
2858
2831
  * @param revision 左侧版本
2859
2832
  * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2833
+ * @param onOpened 差异视图成功打开后触发(更新结果返回用)
2860
2834
  */
2861
- const runSvnDiffWorking = (relPath, revision, logTarget) => {
2835
+ const runSvnDiffWorking = (relPath, revision, logTarget, onOpened) => {
2862
2836
  if (!relPath) { setStatus('仅文件目标支持与工作副本比较'); return }
2863
2837
  setStatus('读取 r' + revision + ' ↔ 工作副本 差异…')
2864
2838
  void svnDiffWorking(sessionId, relPath, revision).then((outcome) => {
@@ -2877,6 +2851,7 @@ export function EditorView(props) {
2877
2851
  logTarget,
2878
2852
  })
2879
2853
  setStatus('已打开 r' + revision + ' ↔ 工作副本 差异')
2854
+ onOpened?.()
2880
2855
  })
2881
2856
  }
2882
2857
 
@@ -3032,6 +3007,7 @@ export function EditorView(props) {
3032
3007
  sessionId,
3033
3008
  scope,
3034
3009
  path,
3010
+ openSvnUpdate: (p) => runSvnUpdate(p),
3035
3011
  notify: (message) => setStatus(message),
3036
3012
  openSvnDiff: (p) => runSvnDiffBase(p),
3037
3013
  openSvnLocalPair: runSvnDiffLocalPair,
@@ -3239,6 +3215,12 @@ export function EditorView(props) {
3239
3215
  const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
3240
3216
  React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
3241
3217
  React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开文件'),
3218
+ (recentUpdate && !svnUpdateDialog && sameUpdateOwner(recentUpdate, sessionId, scope)
3219
+ ? React.createElement('button', {
3220
+ className: 'edrv-pb-update', type: 'button', title: '查看最近 SVN 更新结果(不重新更新)',
3221
+ onClick: () => setSvnUpdateDialog(recentUpdate),
3222
+ }, React.createElement(IconRefreshOutline16, { size: 14 }), '最近更新')
3223
+ : null),
3242
3224
  // Markdown 预览/源码切换(需求 4):仅 .md 文件出现;命令栏与 Ctrl+Shift+V 为同一动作
3243
3225
  (isMdActive ? React.createElement('button', {
3244
3226
  className: 'edrv-pill edrv-pill-ghost edrv-pb-mdbtn',
@@ -3406,7 +3388,13 @@ export function EditorView(props) {
3406
3388
  leftLabel: svnDiff.leftLabel,
3407
3389
  rightLabel: svnDiff.rightLabel,
3408
3390
  onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
3409
- onClose: () => setSvnDiff(null),
3391
+ onClose: () => {
3392
+ setSvnDiff(null)
3393
+ if (svnUpdateReturn?.from === 'diff') {
3394
+ setSvnUpdateDialog(svnUpdateReturn.dialog)
3395
+ setSvnUpdateReturn(null)
3396
+ }
3397
+ },
3410
3398
  })
3411
3399
  } else if (!monaco && !monacoErr) {
3412
3400
  body = loadingBody(loadStage.message, loadStage.progress)
@@ -3574,6 +3562,7 @@ export function EditorView(props) {
3574
3562
  svnChanges,
3575
3563
  svnChangeMap: svnChangeMapMemo,
3576
3564
  openSvnDiff: (p) => runSvnDiffBase(p),
3565
+ openSvnUpdate: (p) => runSvnUpdate(p),
3577
3566
  openSvnLocalPair: runSvnDiffLocalPair,
3578
3567
  refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
3579
3568
  openSvnLog: (p) => openSvnLog(p),
@@ -3726,49 +3715,11 @@ export function EditorView(props) {
3726
3715
  React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
3727
3716
  React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
3728
3717
  : null
3729
- /**
3730
- * SVN 更新结果条(P3):逐条展示 update 的条目级状态,冲突红字高亮,可关闭。
3731
- * 只在本次 update 有条目明细时渲染(无明细/失败时不占用编辑区高度)。
3732
- * @author ddj 2026年09月16号
3733
- * @returns 结果条元素或 null
3734
- */
3735
- const svnResultBar = () => {
3736
- const result = svnUpdateResult
3737
- if (!result || !result.entries.length) return null
3738
- const conflicts = new Set(result.conflicts ?? [])
3739
- return React.createElement('div', { className: 'edrv-svnres' },
3740
- React.createElement('span', { className: 'edrv-svnres-head' }, 'SVN 更新'),
3741
- React.createElement('div', { className: 'edrv-svnres-list' },
3742
- result.entries.map((entry, idx) => React.createElement('span', {
3743
- key: entry.path + ':' + idx,
3744
- className: 'edrv-svnres-item' + (conflicts.has(entry.path) ? ' edrv-svnres-conflict' : ''),
3745
- title: entry.label + ' ' + entry.path,
3746
- }, entry.action + ' ' + entry.path))),
3747
- React.createElement('span', { style: { flex: 1 } }),
3748
- (conflicts.size
3749
- ? React.createElement('button', {
3750
- className: 'edrv-svn-act',
3751
- title: '在工作区中显示第一个冲突文件',
3752
- onClick: () => {
3753
- const first = [...conflicts][0]
3754
- const hit = result.entries.find((entry) => entry.path === first)
3755
- if (hit?.path) openFile(hit.path, false)
3756
- },
3757
- }, '查看冲突文件')
3758
- : null),
3759
- React.createElement('button', {
3760
- className: 'edrv-svn-act',
3761
- title: '关闭结果条',
3762
- onClick: () => setSvnUpdateResult(null),
3763
- }, '✕'))
3764
- }
3765
-
3766
3718
  const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
3767
3719
  pathBar,
3768
3720
  tabRow,
3769
3721
  sideHintEl,
3770
3722
  diskBanner(),
3771
- svnResultBar(),
3772
3723
  editorArea,
3773
3724
  statusBar)
3774
3725
  // 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
@@ -3856,7 +3807,61 @@ export function EditorView(props) {
3856
3807
  onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB, svnLog.target || '') },
3857
3808
  onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision, svnLog.target || '') },
3858
3809
  onOpenFile: (relPath) => { setSvnLog(null); openFile(relPath, false) },
3859
- onClose: () => { setSvnLog(null); setSvnLogError('') },
3810
+ onClose: () => {
3811
+ setSvnLog(null)
3812
+ setSvnLogError('')
3813
+ if (svnUpdateReturn?.from === 'log') {
3814
+ setSvnUpdateDialog(svnUpdateReturn.dialog)
3815
+ setSvnUpdateReturn(null)
3816
+ }
3817
+ },
3818
+ })
3819
+ : null
3820
+ /**
3821
+ * 更新终态只刷新一次目录树与变更缓存,切到日志再返回不重复执行。
3822
+ * @author ddj 2026年09月24号
3823
+ * @param result 已结束的更新任务
3824
+ */
3825
+ const onSvnUpdateDone = (result) => {
3826
+ if (svnUpdateDoneRef.current.has(result.jobId)) return
3827
+ svnUpdateDoneRef.current.add(result.jobId)
3828
+ const message = result.phase === 'completed' ? 'SVN 更新完成'
3829
+ : result.phase === 'cancelled' ? 'SVN 更新已取消(可能部分完成)' : 'SVN 更新失败:' + result.error
3830
+ setStatus(message)
3831
+ setRecentUpdate({ path: result.target, sessionId, scope, resumeJobId: result.jobId })
3832
+ refreshSvnChanges(sessionId, scope)
3833
+ emitRefresh()
3834
+ }
3835
+ const svnUpdateEl = sameUpdateOwner(svnUpdateDialog, sessionId, scope)
3836
+ ? React.createElement(SvnUpdateDialog, {
3837
+ key: 'edrv-svn-update:' + svnUpdateDialog.path + ':' + (svnUpdateDialog.resumeJobId || ''),
3838
+ sessionId, cwd, path: svnUpdateDialog.path, resumeJobId: svnUpdateDialog.resumeJobId,
3839
+ childOverlay: updateOpenWith,
3840
+ onDone: onSvnUpdateDone, onNote: (message) => setStatus(message),
3841
+ onClose: () => setSvnUpdateDialog(null),
3842
+ onShowLog: (target, jobId) => {
3843
+ setSvnUpdateReturn({ from: 'log', dialog: { ...svnUpdateDialog, resumeJobId: jobId } })
3844
+ setSvnUpdateDialog(null)
3845
+ openSvnLog(target)
3846
+ },
3847
+ onCompare: (path, revision, jobId) => runSvnDiffWorking(path, revision, undefined, () => {
3848
+ setSvnUpdateReturn({ from: 'diff', dialog: { ...svnUpdateDialog, resumeJobId: jobId } })
3849
+ setSvnUpdateDialog(null)
3850
+ }),
3851
+ onOpenFile: (path) => openFile(path, false),
3852
+ onOpenWith: (path) => {
3853
+ if (!props.fileOpeners) { setStatus('打开方式不可用'); return }
3854
+ setUpdateOpenWith(true)
3855
+ openWithDialog(props.fileOpeners, { sessionId, cwd: cwd ?? undefined }, path, (opener) => {
3856
+ setUpdateOpenWith(false)
3857
+ if (opener) setStatus('已用「' + opener.label + '」打开:' + baseNameOf(path))
3858
+ })
3859
+ },
3860
+ onOpenParent: (path) => {
3861
+ void revealPathInExplorer(sessionId, path).then((outcome) => {
3862
+ setStatus(outcome.ok ? '已打开父目录' : '打开父目录失败:' + (outcome.error ?? '未知错误'))
3863
+ })
3864
+ },
3860
3865
  })
3861
3866
  : null
3862
3867
  // W2-1 补丁对话框 / W2-2 目录对比对话框:portal 到 body,与日志弹窗同一挂载方式
@@ -3954,6 +3959,7 @@ export function EditorView(props) {
3954
3959
  tabMenuEl,
3955
3960
  paletteEl,
3956
3961
  svnLogEl,
3962
+ svnUpdateEl,
3957
3963
  patchDialogEl,
3958
3964
  sumDialogEl,
3959
3965
  dlogEl,
@@ -3965,6 +3971,7 @@ export function EditorView(props) {
3965
3971
  tabMenuEl,
3966
3972
  paletteEl,
3967
3973
  svnLogEl,
3974
+ svnUpdateEl,
3968
3975
  patchDialogEl,
3969
3976
  sumDialogEl,
3970
3977
  dlogEl,
@@ -1,8 +1,9 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
3
+ * dsh-vscode-mode client — VSCodeMode 设置区:通用 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
4
+ * (快捷键管理已迁出为独立顶层设置区 ui/ShortcutSettings.ts,与 VSCodeMode 平级。)
4
5
  * MCP 管理 Tab 下含三个子页签:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
5
- * 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号
6
+ * 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号 / 2026年10月
6
7
  */
7
8
  import React from 'react'
8
9
  import { rpc } from '../rpc.js'
@@ -14,7 +15,6 @@ import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
14
15
  import { EDITOR_LIMIT_CEIL, EDITOR_LIMIT_DEFAULT, normalizeMaxOpenEditors } from '../../shared/editorLimit.js'
15
16
  import { TORTOISE_DIR_DEFAULT } from '../../shared/svn.js'
16
17
  import { DEFAULT_NATIVE_CSV } from '../../shared/nativeOpen.js'
17
- import { KeybindingsPanel } from './KeybindingsPanel.js'
18
18
  import { LspSettings } from './LspSettings.js'
19
19
  import { PerfSettings } from './PerfSettings.js'
20
20
  import { IntegrationSettings } from './IntegrationSettings.js'
@@ -581,7 +581,6 @@ export function McpSettings(props) {
581
581
  let body
582
582
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
583
583
  else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
584
- else if (tab === 'keys') body = React.createElement(KeybindingsPanel, null)
585
584
  else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
586
585
  saveGlobal: (close) => saveGlobal(close), saveProject: (workspacePath, close) => saveProject(workspacePath, close),
587
586
  refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
@@ -591,7 +590,7 @@ export function McpSettings(props) {
591
590
  else body = React.createElement(PerfSettings, null)
592
591
  return React.createElement('section', { className: 'vsm-mcp-page' },
593
592
  React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。'))),
594
- React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'ai' ? 'active' : '', onClick: () => setTab('ai') }, 'AI 补全'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
593
+ React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'ai' ? 'active' : '', onClick: () => setTab('ai') }, 'AI 补全'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
595
594
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
596
595
  body,
597
596
  )
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import React from 'react'
11
11
  import { rpc } from '../rpc.js'
12
- import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
12
+ import { chordOf, useKeybindingsVersion } from '../keybindings.js'
13
13
  import { stepIndex } from './searchNav.js'
14
14
 
15
15
  /**
@@ -64,19 +64,18 @@ export function QuickOpen(props) {
64
64
  inputRef.current?.focus?.()
65
65
  setOpen(true)
66
66
  }
67
- const onKey = (e) => {
68
- if (matchEvent(e, bindingsOf('edrv.quickOpen'))) {
69
- e.preventDefault(); e.stopPropagation()
70
- openBox()
71
- } else if (e.key === 'Escape') {
72
- setOpen(false); inputRef.current?.blur?.()
73
- }
67
+ // Esc 关闭(仅收起浮窗;历史 capture 行为保留,不参与命令派发、不吞键)
68
+ const onEscape = (e) => {
69
+ if (e.key !== 'Escape') return
70
+ setOpen(false)
71
+ inputRef.current?.blur?.()
74
72
  }
75
- window.addEventListener('keydown', onKey, true)
76
- // 指令系统入口(命令栏「快速打开文件」):与键位复用同一动作
73
+ // 快速打开键位(Ctrl+P)已归官方 shortcuts 机制派发:官方 resolve → 注册表 run →
74
+ // 派发 `edrv.command.quickOpen` 事件到此处(历史窗口 capture 键位自检移除,防双开)。
75
+ window.addEventListener('keydown', onEscape, true)
77
76
  window.addEventListener('edrv.command.quickOpen', openBox)
78
77
  return () => {
79
- window.removeEventListener('keydown', onKey, true)
78
+ window.removeEventListener('keydown', onEscape, true)
80
79
  window.removeEventListener('edrv.command.quickOpen', openBox)
81
80
  }
82
81
  }, [])