dsh-vscode-mode 0.1.25 → 0.1.26

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/lib/client.js CHANGED
@@ -1603,6 +1603,109 @@ window.__ModuleLoader__.load({
1603
1603
  }
1604
1604
  }
1605
1605
  //#endregion
1606
+ //#region src/client/state/viewStateCache.ts
1607
+ /**
1608
+ * dsh-vscode-mode client — 编辑器视图状态持久化(纯函数,可单测)。
1609
+ * 对齐 VSCode workbench.editor.restoreViewState:恢复每个文件的光标/滚动/折叠位置。
1610
+ * 视图状态 = Monaco editor.saveViewState() 返回值(JSON 可序列化),按 path 存储。
1611
+ * 作者 ddj 2026-08-28
1612
+ */
1613
+ /** localStorage 键前缀(按会话隔离)。 */
1614
+ const KEY_PREFIX$1 = "edrv.cache.viewstate.v1.";
1615
+ /**
1616
+ * 判断对象是否为合法的 Monaco view state(保守形状校验,非深校验)。
1617
+ * @author ddj 2026年08月28号
1618
+ * @param value 待校验对象
1619
+ * @returns 是否可接受
1620
+ */
1621
+ function isViewStateLike(value) {
1622
+ if (!value || typeof value !== "object") return false;
1623
+ const v = value;
1624
+ return Array.isArray(v.cursorState) || typeof v.viewState === "object" || Array.isArray(v.contributionsState);
1625
+ }
1626
+ /**
1627
+ * 序列化视图状态映射(path → viewState)。
1628
+ * @author ddj 2026年08月28号
1629
+ * @param states path → viewState 映射
1630
+ * @returns JSON 字符串
1631
+ */
1632
+ function serializeViewStates(states) {
1633
+ return JSON.stringify({
1634
+ v: 1,
1635
+ states
1636
+ });
1637
+ }
1638
+ /**
1639
+ * 解析视图状态文本;损坏/格式不符 → null。
1640
+ * @author ddj 2026年08月28号
1641
+ * @param text localStorage 原文
1642
+ * @returns path → viewState 映射,非法返回 null
1643
+ */
1644
+ function parseViewStates(text) {
1645
+ if (!text) return null;
1646
+ try {
1647
+ const data = JSON.parse(text);
1648
+ if (!data || typeof data !== "object") return null;
1649
+ const states = data.states;
1650
+ if (!states || typeof states !== "object" || Array.isArray(states)) return null;
1651
+ const out = {};
1652
+ for (const [path, state] of Object.entries(states)) {
1653
+ if (typeof path !== "string" || !path) continue;
1654
+ if (!isViewStateLike(state)) continue;
1655
+ out[path] = state;
1656
+ }
1657
+ return out;
1658
+ } catch (error) {
1659
+ return null;
1660
+ }
1661
+ }
1662
+ /**
1663
+ * 把新视图状态并入既有映射:同 path 覆盖、按写入顺序裁剪到容量上限。
1664
+ * @author ddj 2026年08月28号
1665
+ * @param states 既有映射
1666
+ * @param path 文件路径
1667
+ * @param state 视图状态
1668
+ * @returns 新映射(容量裁剪后)
1669
+ */
1670
+ function upsertViewState(states, path, state) {
1671
+ if (!path || !isViewStateLike(state)) return states;
1672
+ try {
1673
+ if (JSON.stringify(state).length > 65536) return states;
1674
+ } catch (error) {
1675
+ return states;
1676
+ }
1677
+ const next = Object.assign({}, states);
1678
+ delete next[path];
1679
+ next[path] = state;
1680
+ const keys = Object.keys(next);
1681
+ if (keys.length > 50) for (const k of keys.slice(0, keys.length - 50)) delete next[k];
1682
+ return next;
1683
+ }
1684
+ /**
1685
+ * 读取某会话的视图状态缓存(localStorage 不可用 → 空映射)。
1686
+ * @author ddj 2026年08月28号
1687
+ * @param sessionId 会话 id
1688
+ * @returns path → viewState 映射
1689
+ */
1690
+ function viewStatesLoad(sessionId) {
1691
+ try {
1692
+ return parseViewStates(window.localStorage.getItem(KEY_PREFIX$1 + String(sessionId))) ?? {};
1693
+ } catch (error) {
1694
+ return {};
1695
+ }
1696
+ }
1697
+ /**
1698
+ * 写入某会话的视图状态缓存(损坏/配额满 → 忽略)。
1699
+ * @author ddj 2026年08月28号
1700
+ * @param sessionId 会话 id
1701
+ * @param states path → viewState 映射
1702
+ */
1703
+ function viewStatesSave(sessionId, states) {
1704
+ try {
1705
+ window.localStorage.setItem(KEY_PREFIX$1 + String(sessionId), serializeViewStates(states));
1706
+ } catch (error) {}
1707
+ }
1708
+ //#endregion
1606
1709
  //#region src/client/ui/EditorView.ts
1607
1710
  /**
1608
1711
  * dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
@@ -1663,6 +1766,7 @@ window.__ModuleLoader__.load({
1663
1766
  const viewRootRef = react.default.useRef(null);
1664
1767
  const monacoRef = react.default.useRef(null);
1665
1768
  const modelsRef = react.default.useRef(/* @__PURE__ */ new Map());
1769
+ const viewStatesRef = react.default.useRef({});
1666
1770
  const saveTimerRef = react.default.useRef(null);
1667
1771
  const loadSeqRef = react.default.useRef(0);
1668
1772
  const programmaticRef = react.default.useRef(false);
@@ -1714,8 +1818,42 @@ window.__ModuleLoader__.load({
1714
1818
  });
1715
1819
  if (select) setActive(path);
1716
1820
  };
1821
+ /**
1822
+ * 保存当前活动文件的视图状态(光标/滚动/折叠)到会话缓存。
1823
+ * @author ddj 2026年08月28号
1824
+ * @param path 要保存的文件路径(缺省 = 当前 active)
1825
+ */
1826
+ const saveViewState = (path) => {
1827
+ const ed = editorRef.current;
1828
+ const target = path ?? active;
1829
+ if (!ed || !target) return;
1830
+ try {
1831
+ const state = ed.saveViewState();
1832
+ if (!state) return;
1833
+ viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state);
1834
+ viewStatesSave(sessionId, viewStatesRef.current);
1835
+ } catch (e) {}
1836
+ };
1837
+ /**
1838
+ * 恢复指定文件的视图状态(内容就绪、setModel 后调用;恢复后即消费删除)。
1839
+ * @author ddj 2026年08月28号
1840
+ * @param path 文件路径
1841
+ */
1842
+ const restoreViewState = (path) => {
1843
+ const ed = editorRef.current;
1844
+ if (!ed || !path) return;
1845
+ const saved = viewStatesRef.current[path];
1846
+ if (!saved) return;
1847
+ const next = Object.assign({}, viewStatesRef.current);
1848
+ delete next[path];
1849
+ viewStatesRef.current = next;
1850
+ try {
1851
+ ed.restoreViewState(saved);
1852
+ } catch (e) {}
1853
+ };
1717
1854
  const closeTab = (path) => {
1718
1855
  flushSave();
1856
+ if (path === active) saveViewState(path);
1719
1857
  setTabs((prev) => {
1720
1858
  const idx = prev.findIndex((t) => t.path === path);
1721
1859
  if (idx < 0) return prev;
@@ -1757,8 +1895,38 @@ window.__ModuleLoader__.load({
1757
1895
  setRecords((prev) => sameRecords(prev, map) ? prev : map);
1758
1896
  }).catch((e) => setError("list异常:" + String(e)));
1759
1897
  };
1760
- const loadContent = (path, sid) => {
1898
+ /**
1899
+ * 加载文件内容(对齐 VSCode model 复用:会话内已打开的 model 直接秒显,
1900
+ * 后台静默 RPC 校验防陈旧;首次打开走原读取流程)。
1901
+ * @author ddj 2026年08月28号
1902
+ * @param path 文件路径
1903
+ * @param sid 会话 id
1904
+ * @param force 强制走 RPC(reloadFile 用,跳过 model 复用)
1905
+ */
1906
+ const loadContent = (path, sid, force) => {
1761
1907
  const seq = ++loadSeqRef.current;
1908
+ const cachedModel = force ? null : modelsRef.current.get(path);
1909
+ if (cachedModel) {
1910
+ setContent(cachedModel.getValue());
1911
+ setContentPath(path);
1912
+ setLoadStage((prev) => ({
1913
+ progress: Math.max(84, prev.progress),
1914
+ message: "文件已读取,准备创建编辑器…"
1915
+ }));
1916
+ setStatus("已加载");
1917
+ rpc("edrv.read", {
1918
+ sessionId: sid,
1919
+ path
1920
+ }).then((res) => {
1921
+ if (seq !== loadSeqRef.current || path !== active) return;
1922
+ if (res && res.ok && res.content !== cachedModel.getValue()) {
1923
+ saveViewState(path);
1924
+ setContent(res.content);
1925
+ setStatus("已加载");
1926
+ }
1927
+ }).catch(() => {});
1928
+ return;
1929
+ }
1762
1930
  setLoadError(null);
1763
1931
  setLoadStage({
1764
1932
  progress: monaco ? 72 : 12,
@@ -1886,6 +2054,7 @@ window.__ModuleLoader__.load({
1886
2054
  react.default.useEffect(() => {
1887
2055
  if (bootRef.current || !sessionId) return;
1888
2056
  bootRef.current = true;
2057
+ viewStatesRef.current = viewStatesLoad(sessionId);
1889
2058
  try {
1890
2059
  const raw = localStorage.getItem("edrv.editor.v2." + String(sessionId));
1891
2060
  if (raw) {
@@ -2056,6 +2225,7 @@ window.__ModuleLoader__.load({
2056
2225
  const ed = editorRef.current;
2057
2226
  const model = getModel(active, content);
2058
2227
  if (ed.getModel() !== model) ed.setModel(model);
2228
+ restoreViewState(active);
2059
2229
  setLoadStage((prev) => ({
2060
2230
  progress: Math.max(96, prev.progress),
2061
2231
  message: "创建编辑器视图…"
@@ -2080,6 +2250,7 @@ window.__ModuleLoader__.load({
2080
2250
  ]);
2081
2251
  react.default.useEffect(() => () => {
2082
2252
  flushSave();
2253
+ saveViewState(active);
2083
2254
  diffRendererRef.current?.dispose?.();
2084
2255
  if (editorRef.current) {
2085
2256
  editorRef.current.dispose();
@@ -2291,7 +2462,7 @@ window.__ModuleLoader__.load({
2291
2462
  };
2292
2463
  const reloadFile = (skipStale) => {
2293
2464
  if (!active) return;
2294
- loadContent(active, sessionId);
2465
+ loadContent(active, sessionId, true);
2295
2466
  refreshRecords(skipStale === true);
2296
2467
  };
2297
2468
  /**
@@ -2556,6 +2727,7 @@ window.__ModuleLoader__.load({
2556
2727
  onClick: () => {
2557
2728
  if (t.path !== active) {
2558
2729
  flushSave();
2730
+ saveViewState(active);
2559
2731
  setActive(t.path);
2560
2732
  }
2561
2733
  },
@@ -4297,6 +4469,108 @@ window.__ModuleLoader__.load({
4297
4469
  return out;
4298
4470
  }
4299
4471
  //#endregion
4472
+ //#region src/client/state/explorerCache.ts
4473
+ /**
4474
+ * dsh-vscode-mode client — 资源管理栏展开状态持久化(纯函数,可单测)。
4475
+ * 对齐 VSCode:只持久化展开路径集合,不缓存目录条目数据(条目实时 listDir)。
4476
+ * 作者 ddj 2026-08-28
4477
+ */
4478
+ /** localStorage 键前缀(按会话隔离)。 */
4479
+ const KEY_PREFIX = "edrv.cache.explorer.v1.";
4480
+ /**
4481
+ * 归一化展开路径:去空白、反斜杠→斜杠、去首尾斜杠,`..` 段拒绝。
4482
+ * @author ddj 2026年08月28号
4483
+ * @param rel 目录相对路径('' = 根)
4484
+ * @returns 归一化路径,非法返回 null
4485
+ */
4486
+ function normalizeExpanded(rel) {
4487
+ const s = String(rel ?? "").trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
4488
+ if (!s) return "";
4489
+ const segs = s.split("/").filter((seg) => seg !== ".");
4490
+ if (!segs.length) return "";
4491
+ for (const seg of segs) if (seg === "..") return null;
4492
+ return segs.join("/");
4493
+ }
4494
+ /**
4495
+ * 裁剪展开路径集:去重、丢弃非法路径、超容量丢弃尾部(保持传入顺序)。
4496
+ * @author ddj 2026年08月28号
4497
+ * @param expanded 原始展开路径集合
4498
+ * @returns 规范化后的路径数组
4499
+ */
4500
+ function sanitizeExpanded(expanded) {
4501
+ if (!Array.isArray(expanded)) return [];
4502
+ const seen = /* @__PURE__ */ new Set();
4503
+ const out = [];
4504
+ for (const item of expanded) {
4505
+ const rel = normalizeExpanded(typeof item === "string" ? item : null);
4506
+ if (rel === null || seen.has(rel)) continue;
4507
+ seen.add(rel);
4508
+ out.push(rel);
4509
+ if (out.length >= 500) break;
4510
+ }
4511
+ return out;
4512
+ }
4513
+ /**
4514
+ * 序列化缓存数据(JSON;root 为 null 时省略,减小体积)。
4515
+ * @author ddj 2026年08月28号
4516
+ * @param data 缓存数据
4517
+ * @returns JSON 字符串
4518
+ */
4519
+ function serializeExplorer(data) {
4520
+ const payload = data.root === null || data.root === void 0 ? {
4521
+ v: 1,
4522
+ expanded: data.expanded
4523
+ } : {
4524
+ v: 1,
4525
+ root: data.root,
4526
+ expanded: data.expanded
4527
+ };
4528
+ return JSON.stringify(payload);
4529
+ }
4530
+ /**
4531
+ * 解析缓存文本;损坏/格式不符 → null。
4532
+ * @author ddj 2026年08月28号
4533
+ * @param text localStorage 原文
4534
+ * @returns 解析后的缓存数据,非法返回 null
4535
+ */
4536
+ function parseExplorer(text) {
4537
+ if (!text) return null;
4538
+ try {
4539
+ const data = JSON.parse(text);
4540
+ if (!data || typeof data !== "object") return null;
4541
+ return {
4542
+ root: typeof data.root === "string" && data.root ? data.root : null,
4543
+ expanded: sanitizeExpanded(data.expanded)
4544
+ };
4545
+ } catch (error) {
4546
+ return null;
4547
+ }
4548
+ }
4549
+ /**
4550
+ * 读取某会话的展开状态缓存(localStorage 不可用 → null)。
4551
+ * @author ddj 2026年08月28号
4552
+ * @param sessionId 会话 id
4553
+ * @returns 缓存数据或 null
4554
+ */
4555
+ function explorerLoad(sessionId) {
4556
+ try {
4557
+ return parseExplorer(window.localStorage.getItem(KEY_PREFIX + String(sessionId)));
4558
+ } catch (error) {
4559
+ return null;
4560
+ }
4561
+ }
4562
+ /**
4563
+ * 写入某会话的展开状态缓存(损坏/配额满 → 忽略)。
4564
+ * @author ddj 2026年08月28号
4565
+ * @param sessionId 会话 id
4566
+ * @param data 缓存数据
4567
+ */
4568
+ function explorerSave(sessionId, data) {
4569
+ try {
4570
+ window.localStorage.setItem(KEY_PREFIX + String(sessionId), serializeExplorer(data));
4571
+ } catch (error) {}
4572
+ }
4573
+ //#endregion
4300
4574
  //#region src/client/sidebar/panels/FileExplorer.ts
4301
4575
  /**
4302
4576
  * dsh-vscode-mode client — 侧边栏「文件管理」面板(懒加载目录树)。
@@ -4307,6 +4581,7 @@ window.__ModuleLoader__.load({
4307
4581
  * 作者 ddj 2026-08-26 / 2026-08-27
4308
4582
  */
4309
4583
  const DIR_CAP = 4e3;
4584
+ const SAVE_DEBOUNCE_MS = 300;
4310
4585
  /**
4311
4586
  * 目录树面板主体。
4312
4587
  * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
@@ -4326,6 +4601,7 @@ window.__ModuleLoader__.load({
4326
4601
  const tokensRef = react.default.useRef({});
4327
4602
  const expandedRef = react.default.useRef({});
4328
4603
  expandedRef.current = expanded;
4604
+ const saveTimerRef = react.default.useRef(null);
4329
4605
  const loadDir = (rel) => {
4330
4606
  const token = (tokensRef.current[rel] || 0) + 1;
4331
4607
  tokensRef.current[rel] = token;
@@ -4383,8 +4659,42 @@ window.__ModuleLoader__.load({
4383
4659
  setError(null);
4384
4660
  setMenu(null);
4385
4661
  setRoot(null);
4662
+ const cached = sessionId ? explorerLoad(sessionId) : null;
4663
+ const restored = cached?.expanded ?? [];
4664
+ if (cached?.root) setRoot(cached.root);
4665
+ if (restored.length) {
4666
+ const next = {};
4667
+ for (const rel of restored) next[rel] = true;
4668
+ setExpanded(next);
4669
+ }
4386
4670
  loadDir("");
4671
+ for (const rel of restored) if (rel) loadDir(rel);
4387
4672
  }, [sessionId]);
4673
+ react.default.useEffect(() => {
4674
+ if (saveTimerRef.current) {
4675
+ clearTimeout(saveTimerRef.current);
4676
+ saveTimerRef.current = null;
4677
+ }
4678
+ if (!sessionId) return;
4679
+ saveTimerRef.current = setTimeout(() => {
4680
+ saveTimerRef.current = null;
4681
+ const expandedList = Object.keys(expanded).filter((k) => expanded[k] === true);
4682
+ explorerSave(sessionId, {
4683
+ root,
4684
+ expanded: expandedList
4685
+ });
4686
+ }, SAVE_DEBOUNCE_MS);
4687
+ return () => {
4688
+ if (saveTimerRef.current) {
4689
+ clearTimeout(saveTimerRef.current);
4690
+ saveTimerRef.current = null;
4691
+ }
4692
+ };
4693
+ }, [
4694
+ expanded,
4695
+ root,
4696
+ sessionId
4697
+ ]);
4388
4698
  react.default.useEffect(() => {
4389
4699
  const onRefresh = () => refresh();
4390
4700
  window.addEventListener("edrv:refresh", onRefresh);