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 +5 -4
- package/lib/client.js +170 -45
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/state/editorFindState.ts +102 -0
- package/src/client/state/editorViewState.ts +56 -0
- package/src/client/ui/EditorView.ts +16 -40
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
20787
|
-
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|