dsh-vscode-mode 0.14.12 → 0.14.13

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
@@ -464,13 +464,13 @@ profile 请替换为实际目标,不要把示例目录当成当前运行 profi
464
464
 
465
465
  ```bash
466
466
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
467
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.14.12
467
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.14.13
468
468
 
469
469
  # ② npm 注册表(发布到 npm 后)
470
- dsh plugin --profile web add dsh-vscode-mode@0.14.12
470
+ dsh plugin --profile web add dsh-vscode-mode@0.14.13
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.12/dsh-vscode-mode-0.14.12.tgz
473
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.14.13/dsh-vscode-mode-0.14.13.tgz
474
474
  ```
475
475
 
476
476
  > **DSH 版本政策(0.14.10 起)**:六项 DSH peer 统一声明为 `*`,不再维护逐版
@@ -808,6 +808,8 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
808
808
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
809
809
  近期关键版本:
810
810
 
811
+ - **v0.14.13**:**资源管理器跟随活动文件**——打开或切换编辑文件时自动展开祖先并滚动定位;资源管理器隐藏期间保留最新活动文件,切回后定位。慢目录加载不再受短重试窗口限制,仅滚动资源树、不抢输入焦点,保留手动定位。用户已确认本地功能验收通过,详见 [0.14.13 发布说明](docs/release-0.14.13.md)。
812
+
811
813
  - **v0.14.12**:**搜索框全选与模糊文件搜索**——工作区搜索打开/再次聚焦时在输入更新后全选旧词或编辑器选区种子,便于直接改写;Ctrl+P 支持大小写不敏感的文件名有序非连续匹配(如 `bn` → `BaseNavigationManager.cs`、`ISTSD` → `ISTaskSystemData.lua`),保留带目录的连续路径查询与 DAP 文件定位规则。更新后需重启 DSH Host;真实 GUI 行为、主题/滚动和 Console 尚未验收,发布不代表现场验收完成。
812
814
 
813
815
  - **v0.14.11**:**编辑快捷键文案与动作验证**——注释菜单不再写死默认键位,避免自定义绑定后显示误导;明确编辑器本地 Ctrl+D 向下复制、不使用剪贴板;加强原生动作转发与幂等清理断言。默认键位与编辑算法不变,详见 [0.14.11 发布说明](docs/release-0.14.11.md)。
package/lib/client.js CHANGED
@@ -30357,6 +30357,102 @@ window.__ModuleLoader__.load({
30357
30357
  };
30358
30358
  }
30359
30359
  //#endregion
30360
+ //#region src/client/sidebar/panels/useExplorerReveal.ts
30361
+ /** @private @author ddj 2026年10月14号 @param path Editor path. @returns Safe normalized tree path, or null. */
30362
+ function treeRevealPath(path) {
30363
+ if (!path || !isTreeRevealable(path)) return null;
30364
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
30365
+ }
30366
+ /** @private @author ddj 2026年10月14号 @param host Tree scroll container. @param path Exact row path. @returns Whether a visible row was located. Only scrolls the tree. */
30367
+ function revealTreeRow(host, path) {
30368
+ if (!host || host.clientHeight <= 0) return false;
30369
+ const escaped = path.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
30370
+ const row = host.querySelector("[data-edrv-path=\"" + escaped + "\"]");
30371
+ if (!row) return false;
30372
+ const viewport = host.getBoundingClientRect();
30373
+ const bounds = row.getBoundingClientRect();
30374
+ const top = viewport.top + host.clientTop;
30375
+ if (bounds.top < top || bounds.bottom > top + host.clientHeight) host.scrollTop += bounds.top - top - (host.clientHeight - bounds.height) / 2;
30376
+ return true;
30377
+ }
30378
+ /** @private @author ddj 2026年10月14号 @param options Current workspace, active editor and tree preparation. @returns Highlight and manual request callback. Waits for committed rows without polling; unmount cancels highlight cleanup. */
30379
+ function useExplorerReveal(options) {
30380
+ const { scope, activePath, treeRef } = options;
30381
+ const [target, setTarget] = react.default.useState(null);
30382
+ const [revealPath, setRevealPath] = react.default.useState(null);
30383
+ const pending = react.default.useRef(null);
30384
+ const timer = react.default.useRef(null);
30385
+ const latest = react.default.useRef(options);
30386
+ latest.current = options;
30387
+ /** @private @author ddj 2026年10月14号 Clears only this hook's highlight timer. */
30388
+ function clearHighlight() {
30389
+ if (timer.current !== null) clearTimeout(timer.current);
30390
+ timer.current = null;
30391
+ }
30392
+ /** @private @author ddj 2026年10月14号 @param path Requested tree path. Expands ancestors and replaces any previous pending target. */
30393
+ function requestReveal(path) {
30394
+ clearHighlight();
30395
+ setRevealPath(null);
30396
+ const normalized = treeRevealPath(path);
30397
+ const current = latest.current;
30398
+ const next = normalized ? {
30399
+ scope: current.scope,
30400
+ path: normalized,
30401
+ active: current.activePath
30402
+ } : null;
30403
+ pending.current = next;
30404
+ setTarget(next);
30405
+ if (next) current.prepare(next.path);
30406
+ }
30407
+ /** @private @author ddj 2026年10月14号 Attempts the latest target after row commit or container resize. Consumes only a successful, current request. */
30408
+ function tryReveal() {
30409
+ const next = pending.current;
30410
+ const current = latest.current;
30411
+ if (!next || next.scope !== current.scope || next.active !== current.activePath) return;
30412
+ if (!revealTreeRow(current.treeRef.current, next.path)) return;
30413
+ pending.current = null;
30414
+ setRevealPath(next.path);
30415
+ clearHighlight();
30416
+ timer.current = setTimeout(endHighlight, 2e3);
30417
+ }
30418
+ /** @private @author ddj 2026年10月14号 Ends transient reveal styling without changing the active editor row. */
30419
+ function endHighlight() {
30420
+ timer.current = null;
30421
+ setRevealPath(null);
30422
+ }
30423
+ /** @private @author ddj 2026年10月14号 Establishes the latest editor target after workspace tree restoration. */
30424
+ function followActive() {
30425
+ requestReveal(activePath);
30426
+ }
30427
+ react.default.useEffect(followActive, [scope, activePath]);
30428
+ react.default.useLayoutEffect(tryReveal);
30429
+ /** @private @author ddj 2026年10月14号 @returns Cleanup for manual events, resize observation and highlight timer. */
30430
+ function listenReveal() {
30431
+ /** @private @author ddj 2026年10月14号 @param event Existing explicit reveal request. */
30432
+ function onReveal(event) {
30433
+ const detail = event.detail;
30434
+ if (detail?.scope && detail.scope !== latest.current.scope) return;
30435
+ if (typeof detail?.path === "string") requestReveal(detail.path);
30436
+ }
30437
+ const observer = typeof ResizeObserver === "function" ? new ResizeObserver(tryReveal) : null;
30438
+ if (treeRef.current) observer?.observe(treeRef.current);
30439
+ window.addEventListener("edrv:reveal-path", onReveal);
30440
+ /** @private @author ddj 2026年10月14号 Cancels callbacks owned by this mounted tree. */
30441
+ function cleanup() {
30442
+ observer?.disconnect();
30443
+ window.removeEventListener("edrv:reveal-path", onReveal);
30444
+ pending.current = null;
30445
+ clearHighlight();
30446
+ }
30447
+ return cleanup;
30448
+ }
30449
+ react.default.useEffect(listenReveal, [treeRef]);
30450
+ return {
30451
+ revealPath,
30452
+ requestReveal
30453
+ };
30454
+ }
30455
+ //#endregion
30360
30456
  //#region src/client/sidebar/panels/FileExplorer.ts
30361
30457
  /**
30362
30458
  * dsh-vscode-mode client — 侧边栏「文件管理」面板(懒加载目录树 + SWR 缓存)。
@@ -30381,10 +30477,6 @@ window.__ModuleLoader__.load({
30381
30477
  ".pnpm",
30382
30478
  ".pnpm-store"
30383
30479
  ]);
30384
- /** 「在资源管理器视图中显示」高亮时长与定位重试上限(目录懒加载需等待行渲染)。 */
30385
- const REVEAL_HIGHLIGHT_MS = 2e3;
30386
- const REVEAL_RETRY_MAX = 6;
30387
- const REVEAL_RETRY_MS = 120;
30388
30480
  /** 外部文件变化重列去抖:一轮外部批量写入(如 agent 连写多文件)合并为一次重列。 */
30389
30481
  const FILE_CHANGE_DEBOUNCE_MS = 400;
30390
30482
  /**
@@ -30443,15 +30535,6 @@ window.__ModuleLoader__.load({
30443
30535
  }
30444
30536
  const ExplorerRow = react.default.memo(rowView);
30445
30537
  /**
30446
- * 转义 CSS 属性选择器的值(路径含引号/反斜杠时避免选择器语法错误)。
30447
- * @author ddj 2026年09月11号
30448
- * @param value 原始值
30449
- * @returns 可安全嵌入 `[attr="…"]` 的字符串
30450
- */
30451
- function cssEscape(value) {
30452
- return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
30453
- }
30454
- /**
30455
30538
  * 目录树面板主体(SWR:有缓存先渲染,无缓存才显示加载态;加载总在后台)。
30456
30539
  * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
30457
30540
  */
@@ -30470,7 +30553,6 @@ window.__ModuleLoader__.load({
30470
30553
  const [loading, setLoading] = react.default.useState({});
30471
30554
  const [errors, setErrors] = react.default.useState({});
30472
30555
  const [menu, setMenu] = react.default.useState(null);
30473
- const [revealPath, setRevealPath] = react.default.useState(null);
30474
30556
  const tokensRef = react.default.useRef({});
30475
30557
  const expandedRef = react.default.useRef({});
30476
30558
  expandedRef.current = expanded;
@@ -30481,9 +30563,6 @@ window.__ModuleLoader__.load({
30481
30563
  const saveTimerRef = react.default.useRef(null);
30482
30564
  const loadDirRef = react.default.useRef(null);
30483
30565
  const refreshRef = react.default.useRef(null);
30484
- const revealTimerRef = react.default.useRef(null);
30485
- const revealTryRef = react.default.useRef(0);
30486
- const revealInTreeRef = react.default.useRef(null);
30487
30566
  const treeRef = react.default.useRef(null);
30488
30567
  const reloadDirRef = react.default.useRef(null);
30489
30568
  const changedTimerRef = react.default.useRef(null);
@@ -30511,8 +30590,10 @@ window.__ModuleLoader__.load({
30511
30590
  const force = opts?.force === true;
30512
30591
  const doPrefetch = opts?.prefetch === true;
30513
30592
  if (!force && loadingRef.current[rel]) return;
30514
- const token = (tokensRef.current[rel] || 0) + 1;
30515
- tokensRef.current[rel] = token;
30593
+ const tokens = tokensRef.current;
30594
+ const token = (tokens[rel] || 0) + 1;
30595
+ tokens[rel] = token;
30596
+ loadingRef.current = Object.assign({}, loadingRef.current, { [rel]: true });
30516
30597
  setLoading((prev) => Object.assign({}, prev, { [rel]: true }));
30517
30598
  setErrors((prev) => {
30518
30599
  const next = Object.assign({}, prev);
@@ -30525,7 +30606,7 @@ window.__ModuleLoader__.load({
30525
30606
  };
30526
30607
  if (force) args.force = true;
30527
30608
  rpc("edrv.listDir", args).then((res) => {
30528
- if (tokensRef.current[rel] !== token) return;
30609
+ if (scopeRef.current !== scope || tokensRef.current !== tokens || tokens[rel] !== token) return;
30529
30610
  if (res && res.ok && Array.isArray(res.entries)) {
30530
30611
  setDirs((prev) => Object.assign({}, prev, { [rel]: res.entries }));
30531
30612
  if (res.root) setRoot((prev) => prev || res.root);
@@ -30533,10 +30614,10 @@ window.__ModuleLoader__.load({
30533
30614
  if (doPrefetch) prefetchDirs(rel, res.entries);
30534
30615
  } else if (!entriesOf(rel)) setErrors((prev) => Object.assign({}, prev, { [rel]: res?.error ? String(res.error) : "读取目录失败" }));
30535
30616
  }).catch((e) => {
30536
- if (tokensRef.current[rel] !== token) return;
30617
+ if (scopeRef.current !== scope || tokensRef.current !== tokens || tokens[rel] !== token) return;
30537
30618
  if (!entriesOf(rel)) setErrors((prev) => Object.assign({}, prev, { [rel]: "读取目录异常:" + String(e) }));
30538
30619
  }).finally(() => {
30539
- if (tokensRef.current[rel] !== token) return;
30620
+ if (scopeRef.current !== scope || tokensRef.current !== tokens || tokens[rel] !== token) return;
30540
30621
  setLoading((prev) => {
30541
30622
  const next = Object.assign({}, prev);
30542
30623
  delete next[rel];
@@ -30553,45 +30634,21 @@ window.__ModuleLoader__.load({
30553
30634
  setExpanded((prev) => Object.assign({}, prev, { [rel]: true }));
30554
30635
  loadDir(rel, { prefetch: true });
30555
30636
  };
30556
- /**
30557
- * 尝试把目标行滚入可见并高亮;行尚未渲染(祖先目录仍在加载)时按上限重试。
30558
- * @author ddj 2026年09月11号
30559
- * @param path 目标相对路径
30560
- */
30561
- const tryReveal = (path) => {
30562
- const row = treeRef.current?.querySelector?.("[data-edrv-path=\"" + cssEscape(path) + "\"]");
30563
- if (!row) {
30564
- revealTryRef.current += 1;
30565
- if (revealTryRef.current < REVEAL_RETRY_MAX) window.setTimeout(() => tryReveal(path), REVEAL_RETRY_MS);
30566
- return;
30567
- }
30568
- row.scrollIntoView?.({ block: "center" });
30569
- setRevealPath(path);
30570
- if (revealTimerRef.current) clearTimeout(revealTimerRef.current);
30571
- revealTimerRef.current = window.setTimeout(() => {
30572
- revealTimerRef.current = null;
30573
- setRevealPath(null);
30574
- }, REVEAL_HIGHLIGHT_MS);
30575
- };
30576
- /**
30577
- * 在资源管理器视图中显示:展开全部祖先目录并定位高亮目标文件。
30578
- * 目录条目懒加载,故先展开祖先并触发加载,再由 tryReveal 轮询等行渲染。
30579
- * @author ddj 2026年09月11号
30580
- * @param path 目标相对路径
30581
- */
30582
- const revealInTree = (path) => {
30583
- if (!path) return;
30637
+ /** @private @author ddj 2026年10月14号 @param path Safe relative file path. Expands only the ancestor chain and reuses available directory entries. */
30638
+ function prepareReveal(path) {
30584
30639
  const ancestors = ancestorDirsOf(path);
30585
- revealTryRef.current = 0;
30586
- setExpanded((prev) => {
30640
+ setExpanded(expandParents);
30641
+ for (const dir of ["", ...ancestors]) {
30642
+ if (entriesOf(dir)) continue;
30643
+ loadDir(dir, { prefetch: false });
30644
+ }
30645
+ /** @private @author ddj 2026年10月14号 @param prev Current expansion map. @returns Expanded ancestor map. */
30646
+ function expandParents(prev) {
30587
30647
  const next = Object.assign({}, prev);
30588
30648
  for (const dir of ancestors) next[dir] = true;
30589
30649
  return next;
30590
- });
30591
- for (const dir of ancestors) loadDir(dir, { prefetch: false });
30592
- tryReveal(path);
30593
- };
30594
- revealInTreeRef.current = revealInTree;
30650
+ }
30651
+ }
30595
30652
  const refresh = (event) => {
30596
30653
  if (event?.detail?.scope && event.detail.scope !== scope) return;
30597
30654
  if (event?.detail?.paths?.length) {
@@ -30600,6 +30657,7 @@ window.__ModuleLoader__.load({
30600
30657
  }
30601
30658
  const keep = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true);
30602
30659
  tokensRef.current = {};
30660
+ loadingRef.current = {};
30603
30661
  setMenu(null);
30604
30662
  setErrors({});
30605
30663
  setLoading({});
@@ -30664,13 +30722,13 @@ window.__ModuleLoader__.load({
30664
30722
  }, []);
30665
30723
  react.default.useEffect(() => {
30666
30724
  tokensRef.current = {};
30725
+ loadingRef.current = {};
30667
30726
  setDirs({});
30668
30727
  setExpanded({});
30669
30728
  setLoading({});
30670
30729
  setErrors({});
30671
30730
  setMenu(null);
30672
30731
  setRoot(null);
30673
- setRevealPath(null);
30674
30732
  const cached = scope ? explorerLoad(scope) : null;
30675
30733
  const restored = cached?.expanded ?? [];
30676
30734
  if (cached?.root) setRoot(cached.root);
@@ -30712,20 +30770,13 @@ window.__ModuleLoader__.load({
30712
30770
  window.addEventListener("edrv:refresh", onRefresh);
30713
30771
  return () => window.removeEventListener("edrv:refresh", onRefresh);
30714
30772
  }, []);
30715
- react.default.useEffect(() => {
30716
- const onReveal = (event) => {
30717
- const path = event?.detail?.path;
30718
- if (typeof path === "string" && path) revealInTreeRef.current?.(path);
30719
- };
30720
- window.addEventListener("edrv:reveal-path", onReveal);
30721
- return () => {
30722
- window.removeEventListener("edrv:reveal-path", onReveal);
30723
- if (revealTimerRef.current) {
30724
- clearTimeout(revealTimerRef.current);
30725
- revealTimerRef.current = null;
30726
- }
30727
- };
30728
- }, []);
30773
+ const { revealPath } = useExplorerReveal({
30774
+ scope,
30775
+ activePath,
30776
+ treeRef,
30777
+ prepare: prepareReveal
30778
+ });
30779
+ const activeTreePath = treeRevealPath(activePath);
30729
30780
  react.default.useEffect(() => {
30730
30781
  if (!scope) return;
30731
30782
  const timer = setInterval(() => {
@@ -30824,7 +30875,7 @@ window.__ModuleLoader__.load({
30824
30875
  isDir,
30825
30876
  isOpen,
30826
30877
  pending: isDir ? 0 : pendingByPath[e.path] ?? 0,
30827
- active: !isDir && e.path === activePath,
30878
+ active: !isDir && e.path === activeTreePath,
30828
30879
  context: Boolean(menu && menuEntries.length && menu.target.path === e.path),
30829
30880
  reveal: revealPath === e.path,
30830
30881
  mark: svn.local?.get(e.path),