dsh-vscode-mode 0.14.5 → 0.14.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.14.5",
3
+ "version": "0.14.6",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
@@ -0,0 +1,102 @@
1
+ /** Workspace-local Ctrl+F state bridge; ddj 2026年10月09号. */
2
+ export interface FindSnapshot {
3
+ searchString: string
4
+ isRevealed: boolean
5
+ matchCase: boolean
6
+ wholeWord: boolean
7
+ isRegex: boolean
8
+ }
9
+
10
+ interface FindState extends FindSnapshot {
11
+ change(value: Partial<FindSnapshot> & { searchScope?: null }, moveCursor: boolean): void
12
+ onFindReplaceStateChange(listener: () => void): { dispose(): void }
13
+ }
14
+ interface FindController {
15
+ getState(): FindState
16
+ start(options: Record<string, unknown>, state: FindSnapshot & { searchScope: null }): Promise<unknown>
17
+ }
18
+ export interface FindEditor {
19
+ getContribution(id: string): unknown
20
+ }
21
+ const snapshots = new Map<string, FindSnapshot>()
22
+
23
+ /** @private @author ddj 2026年10月09号 @param state Native find state. @returns Only fields safe to share across files. */
24
+ function snapshotOf(state: FindState): FindSnapshot {
25
+ return {
26
+ searchString: state.searchString, isRevealed: state.isRevealed,
27
+ matchCase: state.matchCase, wholeWord: state.wholeWord, isRegex: state.isRegex,
28
+ }
29
+ }
30
+
31
+ /** @private @author ddj 2026年10月09号 @param editor Monaco editor. @returns Supported controller, or null for native-only fallback. */
32
+ function controllerOf(editor: FindEditor): FindController | null {
33
+ try {
34
+ const controller = editor.getContribution('editor.contrib.findController') as FindController | null
35
+ const state = controller?.getState?.()
36
+ return state && typeof state.change === 'function'
37
+ && typeof state.onFindReplaceStateChange === 'function'
38
+ && typeof controller?.start === 'function' ? controller : null
39
+ } catch { return null }
40
+ }
41
+
42
+ /** @private @author ddj 2026年10月09号 @param controller Optional native controller. @param listener State observer. @returns Disposable subscription, or null when the internal API is incompatible. */
43
+ function listenFind(controller: FindController | null, listener: () => void): { dispose(): void } | null {
44
+ try { return controller?.getState().onFindReplaceStateChange(listener) ?? null } catch { return null }
45
+ }
46
+
47
+ /** @private @author ddj 2026年10月09号 @param controller Native controller. @param saved Workspace snapshot. @returns Completion when revealing; never focuses or seeds from selection. */
48
+ function applyFind(controller: FindController, saved: FindSnapshot): Promise<unknown> | void {
49
+ const value = { ...saved, searchScope: null }
50
+ if (!saved.isRevealed) { controller.getState().change(value, false); return }
51
+ return controller.start({
52
+ forceRevealReplace: false, seedSearchStringFromSelection: 'none',
53
+ seedSearchStringFromNonEmptySelection: false, seedSearchStringFromGlobalClipboard: false,
54
+ shouldFocus: 0, shouldAnimate: false, updateSearchScope: false, loop: true,
55
+ }, value)
56
+ }
57
+
58
+ /** @public @author ddj 2026年10月09号 @param editor Monaco editor. @returns Model-switch bridge; dispose releases the native listener. */
59
+ export function createFindBridge(editor: FindEditor) {
60
+ const controller = controllerOf(editor)
61
+ let scope: string | null = null
62
+ let paused = true
63
+ let disposed = false
64
+
65
+ /** @private @author ddj 2026年10月09号 Capture only user changes, not model-reset or restoration events. */
66
+ function capture(): void {
67
+ if (disposed || paused || !scope || !controller) return
68
+ try {
69
+ const saved = snapshotOf(controller.getState())
70
+ snapshots.delete(scope)
71
+ snapshots.set(scope, saved)
72
+ if (snapshots.size > 20) snapshots.delete(snapshots.keys().next().value!)
73
+ } catch { /* A disposed contribution must not interrupt view capture. */ }
74
+ }
75
+ const listener = listenFind(controller, capture)
76
+
77
+ /** @public @author ddj 2026年10月09号 Save before setModel; suppress Monaco's per-model option resets. */
78
+ function pause(): void { capture(); paused = true }
79
+
80
+ /** @public @author ddj 2026年10月09号 @param nextScope Workspace owning the bound model. Restore with moveCursor=false; no per-file range or match index. */
81
+ function resume(nextScope: string): void {
82
+ if (disposed || !controller) return
83
+ scope = nextScope
84
+ const saved = snapshots.get(scope) ?? {
85
+ searchString: '', isRevealed: false, matchCase: false, wholeWord: false, isRegex: false,
86
+ }
87
+ try {
88
+ const result = applyFind(controller, saved)
89
+ result?.catch(() => {}) // Unsupported internal API leaves native Ctrl+F usable.
90
+ } catch { /* Internal contribution compatibility must not break the editor. */ }
91
+ paused = false
92
+ }
93
+
94
+ /** @public @author ddj 2026年10月09号 Save final query and remove listener before editor destruction. */
95
+ function dispose(): void {
96
+ if (disposed) return
97
+ capture()
98
+ disposed = true
99
+ try { listener?.dispose() } catch { /* Already disposed by native editor cleanup. */ }
100
+ }
101
+ return { pause, resume, dispose }
102
+ }
@@ -0,0 +1,56 @@
1
+ /** Model-owned view/find lifecycle; ddj 2026年10月09号. */
2
+ import { createFindBridge, type FindEditor } from './editorFindState.js'
3
+ import { upsertViewState, viewStatesLoad, viewStatesSave } from './viewStateCache.js'
4
+
5
+ export interface ViewEditor extends FindEditor {
6
+ getModel(): unknown
7
+ setModel(model: unknown): void
8
+ saveViewState(): unknown
9
+ restoreViewState(state: unknown): void
10
+ dispose(): void
11
+ }
12
+
13
+ /** @public @author ddj 2026年10月09号 @param editor Monaco instance. @returns Keeper owning the model's scope/path, independent of React's active closure. */
14
+ export function createViewKeeper(editor: ViewEditor) {
15
+ const find = createFindBridge(editor)
16
+ let bound: { scope: string; path: string; model: unknown } | null = null
17
+ let disposed = false
18
+
19
+ /** @public @author ddj 2026年10月09号 @param path Optional expected path; mismatches never write a snapshot under the wrong file. Persist viewport even if cursor never moved. */
20
+ function save(path?: string): void {
21
+ if (disposed || !bound || (path && path !== bound.path)) return
22
+ try {
23
+ if (editor.getModel() !== bound.model) return
24
+ const states = viewStatesLoad(bound.scope)
25
+ const next = upsertViewState(states, bound.path, editor.saveViewState())
26
+ if (next !== states) viewStatesSave(bound.scope, next)
27
+ } catch { /* A missing/disposed model must not overwrite a valid snapshot. */ }
28
+ }
29
+
30
+ /** @public @author ddj 2026年10月09号 @param scope Model workspace. @param path Cache path. @param model Ready text model. Restore only on identity change, before explicit navigation effects. */
31
+ function bind(scope: string, path: string, model: unknown): void {
32
+ if (disposed || !path || !model) return
33
+ if (bound?.scope === scope && bound.path === path && bound.model === model
34
+ && editor.getModel() === model) return
35
+ save()
36
+ find.pause()
37
+ if (editor.getModel() !== model) editor.setModel(model)
38
+ bound = { scope, path, model }
39
+ const state = viewStatesLoad(scope)[path]
40
+ if (state) {
41
+ try { editor.restoreViewState(state) } catch { /* Stale fold ranges may be invalid. */ }
42
+ }
43
+ find.resume(scope)
44
+ }
45
+
46
+ /** @public @author ddj 2026年10月09号 Save before dispose/ref clearing; repeated cleanup is a no-op. */
47
+ function dispose(): void {
48
+ if (disposed) return
49
+ save()
50
+ find.dispose()
51
+ disposed = true
52
+ bound = null
53
+ editor.dispose()
54
+ }
55
+ return { save, bind, dispose }
56
+ }
@@ -53,7 +53,7 @@ import { displayDiffTotal, editorDockMode } from '../diffDock.js'
53
53
  import { editorHeight } from '../editorLayout.js'
54
54
  import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
55
55
  import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
56
- import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
56
+ import { createViewKeeper } from '../state/editorViewState.js'
57
57
  import { mdPreviewLoad, mdPreviewSave } from '../state/mdPreviewCache.js'
58
58
  import { onUiStateReady, reportUiStateEvent, uiStateSettled } from '../state/uiStatePersist.js'
59
59
  import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
@@ -333,8 +333,8 @@ export function EditorView(props) {
333
333
  const modelsRef = React.useRef(null)
334
334
  // model 跨挂载缓存(按工作区作用域):重挂载秒显内容,消除切换对话闪烁
335
335
  if (!modelsRef.current) modelsRef.current = modelsForScope(scope)
336
- // 视图状态缓存(path → Monaco viewState;重启后恢复光标/滚动/折叠位置)
337
- const viewStatesRef = React.useRef({})
336
+ // 视图/查找生命周期跟随实际绑定的 model,不能从新一帧 active/scope 猜旧模型归属。
337
+ const viewKeeperRef = React.useRef(null)
338
338
  // 防抖保存槽(arm/flush/cancel;见 saveDebounce.ts 的缺陷说明,勿退回单一取消句柄)
339
339
  const saveTimerRef = React.useRef(createSaveTimer())
340
340
  const savingRef = React.useRef(new Set()) // 在途保存的路径(关闭前落盘去重,防同内容双发)
@@ -561,39 +561,16 @@ export function EditorView(props) {
561
561
  return normalized
562
562
  }
563
563
 
564
- /**
565
- * 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
566
- * @author ddj 2026年08月28号
567
- * @param path 要保存的文件路径(缺省 = 当前 active)
568
- */
569
- const saveViewState = (path) => {
570
- const ed = editorRef.current
571
- const target = path ?? active
572
- if (!ed || !target) return
573
- try {
574
- const state = ed.saveViewState()
575
- if (!state) return
576
- viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state)
577
- viewStatesSave(scope, viewStatesRef.current)
578
- } catch (e) { /* 视图状态保存失败忽略 */ }
579
- }
564
+ /** @private @author ddj 2026年10月09号 @param path 可选预期文件路径。按已绑定模型归属保存,避免新 active 覆盖旧文件状态。 */
565
+ const saveViewState = (path) => viewKeeperRef.current?.save(path)
580
566
  saveViewStateRef.current = saveViewState
581
567
 
582
- /**
583
- * 恢复指定文件的视图状态(内容就绪、setModel 后调用;恢复后即消费删除)。
584
- * @author ddj 2026年08月28号
585
- * @param path 文件路径
586
- */
587
- const restoreViewState = (path) => {
588
- const ed = editorRef.current
589
- if (!ed || !path) return
590
- const saved = viewStatesRef.current[path]
591
- if (!saved) return
592
- const next = Object.assign({}, viewStatesRef.current)
593
- delete next[path]
594
- viewStatesRef.current = next
595
- try { ed.restoreViewState(saved) } catch (e) { /* 恢复失败忽略(行号越界自动兜底) */ }
596
- }
568
+ /** @private @author ddj 2026年10月09号 ref 脱离与 effect 卸载共用:保存视图/查找后销毁,重复调用无副作用。 */
569
+ const disposeEditor = React.useCallback(() => {
570
+ viewKeeperRef.current?.dispose()
571
+ viewKeeperRef.current = null
572
+ editorRef.current = null
573
+ }, [])
597
574
 
598
575
  /**
599
576
  * 采集当前焦点条目:路径从编辑器 model 实时读取(避免空依赖闭包读到过期 active),
@@ -1366,7 +1343,6 @@ export function EditorView(props) {
1366
1343
  restoredScopeRef.current = scope
1367
1344
  // model 缓存跟随作用域(切工作区释放旧作用域模型;同工作区跨挂载复用)
1368
1345
  modelsRef.current = modelsForScope(scope)
1369
- viewStatesRef.current = viewStatesLoad(scope)
1370
1346
  // Markdown 预览态同样按作用域恢复(重启/换端口后由界面状态镜像回填,见 lateHydrateRef 分支)
1371
1347
  mdPreviewRef.current = mdPreviewLoad(String(scope))
1372
1348
  try {
@@ -2077,11 +2053,10 @@ export function EditorView(props) {
2077
2053
  if (!monaco || !editorRef.current || !active || !contentReady) return
2078
2054
  const ed = editorRef.current
2079
2055
  const model = getModel(active, content)
2080
- if (ed.getModel() !== model) ed.setModel(model)
2056
+ viewKeeperRef.current?.bind(String(scope), active, model)
2081
2057
  syncTypeSuggest(ed, monaco)
2082
- restoreViewState(active)
2083
2058
  setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
2084
- }, [monaco, active, content, contentReady, mdPreviewing, svnDiff])
2059
+ }, [monaco, scope, active, content, contentReady, mdPreviewing, svnDiff])
2085
2060
 
2086
2061
  // PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
2087
2062
  const ensurePdfHost = (node) => { pdfHostRef.current = node }
@@ -2140,7 +2115,7 @@ export function EditorView(props) {
2140
2115
  // 跳转目标高亮的计时器随卸载清理(装饰随编辑器 dispose 一并消失)
2141
2116
  if (navFlashTimerRef.current) { clearTimeout(navFlashTimerRef.current); navFlashTimerRef.current = null }
2142
2117
  navFlashRef.current = []
2143
- if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
2118
+ disposeEditor()
2144
2119
  // model 不在此销毁:跨挂载缓存按作用域存活(modelCache 切作用域时统一释放)
2145
2120
  for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
2146
2121
  pdfCtlRef.current.clear()
@@ -2155,7 +2130,7 @@ export function EditorView(props) {
2155
2130
  // 普通 Monaco 编辑器(可编辑、单列行号)+ 自绘行内差异(decoration 绿底+ / view zone 删除块)
2156
2131
  const ensureEditor = React.useCallback((node) => {
2157
2132
  if (!node) {
2158
- if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
2133
+ disposeEditor()
2159
2134
  return
2160
2135
  }
2161
2136
  if (editorRef.current || !monacoRef.current) return
@@ -2367,6 +2342,7 @@ export function EditorView(props) {
2367
2342
  }
2368
2343
  node.addEventListener('contextmenu', onBpCtxCapture, true)
2369
2344
  editorRef.current = ed
2345
+ viewKeeperRef.current = createViewKeeper(ed)
2370
2346
  }, [])
2371
2347
 
2372
2348
  // 调试断点装饰:断点表/活动文件变化时全量替换行首圆点(独立 collection,与差异/下划线互不干扰);