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/README.md CHANGED
@@ -459,18 +459,18 @@ DSH 每次启动监听随机端口,`http://127.0.0.1:<port>` 就是浏览器 o
459
459
 
460
460
  官方 `dsh plugin` 方式,三选一。下面以 `web` profile 为例;Desktop 或其他命名
461
461
  profile 请替换为实际目标,不要把示例目录当成当前运行 profile。升级前可阅读
462
- [0.14.5 发布说明](docs/release-0.14.5.md);从 0.14.2 或更旧版本升级还须阅读
462
+ [0.14.6 发布说明](docs/release-0.14.6.md);从 0.14.2 或更旧版本升级还须阅读
463
463
  [0.14.3 数据迁移与回退说明](docs/release-0.14.3.md)。
464
464
 
465
465
  ```bash
466
466
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
467
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.14.5
467
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.14.6
468
468
 
469
469
  # ② npm 注册表(发布到 npm 后)
470
- dsh plugin --profile web add dsh-vscode-mode@0.14.5
470
+ dsh plugin --profile web add dsh-vscode-mode@0.14.6
471
471
 
472
472
  # ③ GitHub Release tgz 直装(Release asset 可用后)
473
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.14.5/dsh-vscode-mode-0.14.5.tgz
473
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.14.6/dsh-vscode-mode-0.14.6.tgz
474
474
  ```
475
475
 
476
476
  > **DSH 0.2 兼容范围**:`0.14.1` 起精确放行 `0.2.0-rc.1`,`0.14.4` 再加入
@@ -806,6 +806,7 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
806
806
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
807
807
  近期关键版本:
808
808
 
809
+ - **v0.14.6**:**对话切换位置保持与共享查找**——同工作区切换对话时,在编辑器释放前保存实际模型的滚动、光标和选区等视图状态;只在模型重新绑定时恢复,显式行号导航仍优先。跨文件共享 Ctrl+F 查询、大小写/全词/正则与开关状态,不共享选区搜索范围或匹配序号,不抢焦点;工作区隔离并释放监听。验证范围与真实 GUI 验收缺口见 [0.14.6 发布说明](docs/release-0.14.6.md)。
809
810
  - **v0.14.5**:**计划模式接入与行内差异显示修复**——计划文件默认预览,当前会话新生成计划自动打开;官方计划及待审批计划可转入只读虚拟页签,审批仍由官方控件完成。行内增删与 Monaco diff 共用明暗色盘,删除代码按当前语言着色,旧行号仅在快照可验证时显示;修复纯删除的 BOM/CRLF 定位,清理过期异步结果与监听。验证及真实 GUI 冒烟边界见 [0.14.5 发布说明](docs/release-0.14.5.md)。
810
811
  - **v0.14.4**:**Desktop rc.2 兼容与 MCP 管理修复**——六项 DSH peer 精确纳入 `0.2.0-rc.2`;MCP 状态不再把配置启用或 ACTIVE 当成在线证据,只读快照轮询避免重复配置扫描,配置修改串行防止丢更新,跨全局/项目拒绝工具命名空间交叠并安全隐藏历史歧义工具。编辑器在没有待处理差异时隐藏底部操作条,其他文件的差异和冲突入口保留。验证范围、升级及回退注意事项见 [0.14.4 发布说明](docs/release-0.14.4.md)。
811
812
  - **v0.14.3**:**差异审查性能与持久化**——差异定位/行号与多块应用避免重复全文扫描/复制;归档从全量 JSON 改为每工作区 SQLite 事务追加,活跃记录由整份重写改为逐条更新,同库原子提交决策及 Undo 意图;归档列表按页加载、详情按批次读取。Web 实测单块 Keep RPC 17/22ms(样本值),旧归档 JSON 与迁移前活跃 JSON 保留;侧栏编辑器内的差异栏改为居中悬浮 banner。升级和回退注意事项见 [0.14.3 发布说明](docs/release-0.14.3.md)。
package/lib/client.js CHANGED
@@ -9610,6 +9610,111 @@ window.__ModuleLoader__.load({
9610
9610
  }
9611
9611
  }
9612
9612
  //#endregion
9613
+ //#region src/client/state/editorFindState.ts
9614
+ const snapshots = /* @__PURE__ */ new Map();
9615
+ /** @private @author ddj 2026年10月09号 @param state Native find state. @returns Only fields safe to share across files. */
9616
+ function snapshotOf(state) {
9617
+ return {
9618
+ searchString: state.searchString,
9619
+ isRevealed: state.isRevealed,
9620
+ matchCase: state.matchCase,
9621
+ wholeWord: state.wholeWord,
9622
+ isRegex: state.isRegex
9623
+ };
9624
+ }
9625
+ /** @private @author ddj 2026年10月09号 @param editor Monaco editor. @returns Supported controller, or null for native-only fallback. */
9626
+ function controllerOf(editor) {
9627
+ try {
9628
+ const controller = editor.getContribution("editor.contrib.findController");
9629
+ const state = controller?.getState?.();
9630
+ return state && typeof state.change === "function" && typeof state.onFindReplaceStateChange === "function" && typeof controller?.start === "function" ? controller : null;
9631
+ } catch {
9632
+ return null;
9633
+ }
9634
+ }
9635
+ /** @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. */
9636
+ function listenFind(controller, listener) {
9637
+ try {
9638
+ return controller?.getState().onFindReplaceStateChange(listener) ?? null;
9639
+ } catch {
9640
+ return null;
9641
+ }
9642
+ }
9643
+ /** @private @author ddj 2026年10月09号 @param controller Native controller. @param saved Workspace snapshot. @returns Completion when revealing; never focuses or seeds from selection. */
9644
+ function applyFind(controller, saved) {
9645
+ const value = {
9646
+ ...saved,
9647
+ searchScope: null
9648
+ };
9649
+ if (!saved.isRevealed) {
9650
+ controller.getState().change(value, false);
9651
+ return;
9652
+ }
9653
+ return controller.start({
9654
+ forceRevealReplace: false,
9655
+ seedSearchStringFromSelection: "none",
9656
+ seedSearchStringFromNonEmptySelection: false,
9657
+ seedSearchStringFromGlobalClipboard: false,
9658
+ shouldFocus: 0,
9659
+ shouldAnimate: false,
9660
+ updateSearchScope: false,
9661
+ loop: true
9662
+ }, value);
9663
+ }
9664
+ /** @public @author ddj 2026年10月09号 @param editor Monaco editor. @returns Model-switch bridge; dispose releases the native listener. */
9665
+ function createFindBridge(editor) {
9666
+ const controller = controllerOf(editor);
9667
+ let scope = null;
9668
+ let paused = true;
9669
+ let disposed = false;
9670
+ /** @private @author ddj 2026年10月09号 Capture only user changes, not model-reset or restoration events. */
9671
+ function capture() {
9672
+ if (disposed || paused || !scope || !controller) return;
9673
+ try {
9674
+ const saved = snapshotOf(controller.getState());
9675
+ snapshots.delete(scope);
9676
+ snapshots.set(scope, saved);
9677
+ if (snapshots.size > 20) snapshots.delete(snapshots.keys().next().value);
9678
+ } catch {}
9679
+ }
9680
+ const listener = listenFind(controller, capture);
9681
+ /** @public @author ddj 2026年10月09号 Save before setModel; suppress Monaco's per-model option resets. */
9682
+ function pause() {
9683
+ capture();
9684
+ paused = true;
9685
+ }
9686
+ /** @public @author ddj 2026年10月09号 @param nextScope Workspace owning the bound model. Restore with moveCursor=false; no per-file range or match index. */
9687
+ function resume(nextScope) {
9688
+ if (disposed || !controller) return;
9689
+ scope = nextScope;
9690
+ const saved = snapshots.get(scope) ?? {
9691
+ searchString: "",
9692
+ isRevealed: false,
9693
+ matchCase: false,
9694
+ wholeWord: false,
9695
+ isRegex: false
9696
+ };
9697
+ try {
9698
+ applyFind(controller, saved)?.catch(() => {});
9699
+ } catch {}
9700
+ paused = false;
9701
+ }
9702
+ /** @public @author ddj 2026年10月09号 Save final query and remove listener before editor destruction. */
9703
+ function dispose() {
9704
+ if (disposed) return;
9705
+ capture();
9706
+ disposed = true;
9707
+ try {
9708
+ listener?.dispose();
9709
+ } catch {}
9710
+ }
9711
+ return {
9712
+ pause,
9713
+ resume,
9714
+ dispose
9715
+ };
9716
+ }
9717
+ //#endregion
9613
9718
  //#region src/client/paths.ts
9614
9719
  /**
9615
9720
  * dsh-vscode-mode client — 浏览器端缓存键/路径统一出口(PathConst)。
@@ -9753,6 +9858,57 @@ window.__ModuleLoader__.load({
9753
9858
  window.localStorage.setItem(KEY_PREFIX$4 + String(scope), serializeViewStates(states));
9754
9859
  } catch (error) {}
9755
9860
  }
9861
+ //#endregion
9862
+ //#region src/client/state/editorViewState.ts
9863
+ /** Model-owned view/find lifecycle; ddj 2026年10月09号. */
9864
+ /** @public @author ddj 2026年10月09号 @param editor Monaco instance. @returns Keeper owning the model's scope/path, independent of React's active closure. */
9865
+ function createViewKeeper(editor) {
9866
+ const find = createFindBridge(editor);
9867
+ let bound = null;
9868
+ let disposed = false;
9869
+ /** @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. */
9870
+ function save(path) {
9871
+ if (disposed || !bound || path && path !== bound.path) return;
9872
+ try {
9873
+ if (editor.getModel() !== bound.model) return;
9874
+ const states = viewStatesLoad(bound.scope);
9875
+ const next = upsertViewState(states, bound.path, editor.saveViewState());
9876
+ if (next !== states) viewStatesSave(bound.scope, next);
9877
+ } catch {}
9878
+ }
9879
+ /** @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. */
9880
+ function bind(scope, path, model) {
9881
+ if (disposed || !path || !model) return;
9882
+ if (bound?.scope === scope && bound.path === path && bound.model === model && editor.getModel() === model) return;
9883
+ save();
9884
+ find.pause();
9885
+ if (editor.getModel() !== model) editor.setModel(model);
9886
+ bound = {
9887
+ scope,
9888
+ path,
9889
+ model
9890
+ };
9891
+ const state = viewStatesLoad(scope)[path];
9892
+ if (state) try {
9893
+ editor.restoreViewState(state);
9894
+ } catch {}
9895
+ find.resume(scope);
9896
+ }
9897
+ /** @public @author ddj 2026年10月09号 Save before dispose/ref clearing; repeated cleanup is a no-op. */
9898
+ function dispose() {
9899
+ if (disposed) return;
9900
+ save();
9901
+ find.dispose();
9902
+ disposed = true;
9903
+ bound = null;
9904
+ editor.dispose();
9905
+ }
9906
+ return {
9907
+ save,
9908
+ bind,
9909
+ dispose
9910
+ };
9911
+ }
9756
9912
  /**
9757
9913
  * 归一化预览态路径表:丢弃非字符串/空串、去重(保留首次出现顺序),超限截断。
9758
9914
  * @author ddj 2026年09月28号
@@ -20550,7 +20706,7 @@ window.__ModuleLoader__.load({
20550
20706
  const monacoRef = react.default.useRef(null);
20551
20707
  const modelsRef = react.default.useRef(null);
20552
20708
  if (!modelsRef.current) modelsRef.current = modelsForScope(scope);
20553
- const viewStatesRef = react.default.useRef({});
20709
+ const viewKeeperRef = react.default.useRef(null);
20554
20710
  const saveTimerRef = react.default.useRef(createSaveTimer());
20555
20711
  const savingRef = react.default.useRef(/* @__PURE__ */ new Set());
20556
20712
  const loadSeqRef = react.default.useRef(0);
@@ -20765,40 +20921,15 @@ window.__ModuleLoader__.load({
20765
20921
  addTab(normalized, select, preview);
20766
20922
  return normalized;
20767
20923
  };
20768
- /**
20769
- * 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
20770
- * @author ddj 2026年08月28号
20771
- * @param path 要保存的文件路径(缺省 = 当前 active)
20772
- */
20773
- const saveViewState = (path) => {
20774
- const ed = editorRef.current;
20775
- const target = path ?? active;
20776
- if (!ed || !target) return;
20777
- try {
20778
- const state = ed.saveViewState();
20779
- if (!state) return;
20780
- viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state);
20781
- viewStatesSave(scope, viewStatesRef.current);
20782
- } catch (e) {}
20783
- };
20924
+ /** @private @author ddj 2026年10月09号 @param path 可选预期文件路径。按已绑定模型归属保存,避免新 active 覆盖旧文件状态。 */
20925
+ const saveViewState = (path) => viewKeeperRef.current?.save(path);
20784
20926
  saveViewStateRef.current = saveViewState;
20785
- /**
20786
- * 恢复指定文件的视图状态(内容就绪、setModel 后调用;恢复后即消费删除)。
20787
- * @author ddj 2026年08月28号
20788
- * @param path 文件路径
20789
- */
20790
- const restoreViewState = (path) => {
20791
- const ed = editorRef.current;
20792
- if (!ed || !path) return;
20793
- const saved = viewStatesRef.current[path];
20794
- if (!saved) return;
20795
- const next = Object.assign({}, viewStatesRef.current);
20796
- delete next[path];
20797
- viewStatesRef.current = next;
20798
- try {
20799
- ed.restoreViewState(saved);
20800
- } catch (e) {}
20801
- };
20927
+ /** @private @author ddj 2026年10月09号 ref 脱离与 effect 卸载共用:保存视图/查找后销毁,重复调用无副作用。 */
20928
+ const disposeEditor = react.default.useCallback(() => {
20929
+ viewKeeperRef.current?.dispose();
20930
+ viewKeeperRef.current = null;
20931
+ editorRef.current = null;
20932
+ }, []);
20802
20933
  /**
20803
20934
  * 采集当前焦点条目:路径从编辑器 model 实时读取(避免空依赖闭包读到过期 active),
20804
20935
  * 位置/viewState 从编辑器读取;编辑器或模型未就绪 → null。
@@ -21611,7 +21742,6 @@ window.__ModuleLoader__.load({
21611
21742
  if (!sessionId || restoredScopeRef.current === scope) return;
21612
21743
  restoredScopeRef.current = scope;
21613
21744
  modelsRef.current = modelsForScope(scope);
21614
- viewStatesRef.current = viewStatesLoad(scope);
21615
21745
  mdPreviewRef.current = mdPreviewLoad(String(scope));
21616
21746
  try {
21617
21747
  const raw = localStorage.getItem(CACHE_KEY.editor + String(scope)) ?? localStorage.getItem(CACHE_KEY.editorLegacy + String(scope));
@@ -22338,15 +22468,15 @@ window.__ModuleLoader__.load({
22338
22468
  if (!monaco || !editorRef.current || !active || !contentReady) return;
22339
22469
  const ed = editorRef.current;
22340
22470
  const model = getModel(active, content);
22341
- if (ed.getModel() !== model) ed.setModel(model);
22471
+ viewKeeperRef.current?.bind(String(scope), active, model);
22342
22472
  syncTypeSuggest(ed, monaco);
22343
- restoreViewState(active);
22344
22473
  setLoadStage((prev) => ({
22345
22474
  progress: Math.max(96, prev.progress),
22346
22475
  message: "创建编辑器视图…"
22347
22476
  }));
22348
22477
  }, [
22349
22478
  monaco,
22479
+ scope,
22350
22480
  active,
22351
22481
  content,
22352
22482
  contentReady,
@@ -22419,10 +22549,7 @@ window.__ModuleLoader__.load({
22419
22549
  navFlashTimerRef.current = null;
22420
22550
  }
22421
22551
  navFlashRef.current = [];
22422
- if (editorRef.current) {
22423
- editorRef.current.dispose();
22424
- editorRef.current = null;
22425
- }
22552
+ disposeEditor();
22426
22553
  for (const ctl of pdfCtlRef.current.values()) ctl.destroy();
22427
22554
  pdfCtlRef.current.clear();
22428
22555
  clearZoomMem();
@@ -22434,10 +22561,7 @@ window.__ModuleLoader__.load({
22434
22561
  }, []);
22435
22562
  const ensureEditor = react.default.useCallback((node) => {
22436
22563
  if (!node) {
22437
- if (editorRef.current) {
22438
- editorRef.current.dispose();
22439
- editorRef.current = null;
22440
- }
22564
+ disposeEditor();
22441
22565
  return;
22442
22566
  }
22443
22567
  if (editorRef.current || !monacoRef.current) return;
@@ -22668,6 +22792,7 @@ window.__ModuleLoader__.load({
22668
22792
  };
22669
22793
  node.addEventListener("contextmenu", onBpCtxCapture, true);
22670
22794
  editorRef.current = ed;
22795
+ viewKeeperRef.current = createViewKeeper(ed);
22671
22796
  }, []);
22672
22797
  react.default.useEffect(() => {
22673
22798
  const ed = editorRef.current;