dsh-vscode-mode 0.6.0 → 0.7.0

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/index.js CHANGED
@@ -130,6 +130,7 @@ const KEYBINDING_DEFAULTS = {
130
130
  "edrv.navigateForward": "Alt+ArrowRight|Ctrl+Shift+-",
131
131
  "edrv.nextTab": "Ctrl+Alt+ArrowRight|Ctrl+PageDown",
132
132
  "edrv.prevTab": "Ctrl+Alt+ArrowLeft|Ctrl+PageUp",
133
+ "edrv.goToLine": "Ctrl+G",
133
134
  "edrv.showCommands": "Ctrl+Shift+P|F1",
134
135
  "edrv.nextEditorRow": "Ctrl+Alt+ArrowDown",
135
136
  "edrv.prevEditorRow": "Ctrl+Alt+ArrowUp",
@@ -6670,6 +6671,102 @@ function pendingState(token, take) {
6670
6671
  function clientsAlive() {
6671
6672
  return Date.now() - lastPollAt <= PRESENCE_WINDOW_MS;
6672
6673
  }
6674
+ /**
6675
+ * 路径归一:去首尾空白 + 反斜杠转 '/'(Windows 输入等价处理)。
6676
+ * @author ddj 2026年09月22号
6677
+ * @param raw 原始输入
6678
+ * @returns 归一后的字符串
6679
+ */
6680
+ function normPath(raw) {
6681
+ return String(raw ?? "").trim().replace(/\\/g, "/");
6682
+ }
6683
+ /**
6684
+ * 单个名称段的通用违规检查(两套校验共用)。
6685
+ * @author ddj 2026年09月22号
6686
+ * @param seg 名称段
6687
+ * @returns 错误文案;null = 通过
6688
+ */
6689
+ function checkSegment(seg) {
6690
+ if (!seg) return "名称包含空路径段";
6691
+ if (seg === "." || seg === "..") return "名称不能包含 . 或 .. 路径段";
6692
+ if (seg.length > 255) return "名称过长(单段超过 255 字符)";
6693
+ if (/[\0-\x1f]/.test(seg)) return "名称包含非法控制字符";
6694
+ return null;
6695
+ }
6696
+ /**
6697
+ * 校验新建文件/文件夹的相对路径(允许 a/b.c 嵌套段)。
6698
+ * @author ddj 2026年09月22号
6699
+ * @param raw 用户输入的名称或相对路径
6700
+ * @returns 错误文案;null = 校验通过
6701
+ */
6702
+ function checkNewName(raw) {
6703
+ const text = normPath(raw);
6704
+ if (!text) return "名称不能为空";
6705
+ if (text.startsWith("/")) return "名称不能是绝对路径";
6706
+ if (/^[a-zA-Z]:/.test(text)) return "名称不能带盘符";
6707
+ for (const seg of text.split("/")) {
6708
+ const bad = checkSegment(seg);
6709
+ if (bad) return bad;
6710
+ }
6711
+ return null;
6712
+ }
6713
+ /**
6714
+ * 校验重命名的新名称(仅单个名称段,不接受路径)。
6715
+ * @author ddj 2026年09月22号
6716
+ * @param raw 用户输入的新名称
6717
+ * @returns 错误文案;null = 校验通过
6718
+ */
6719
+ function checkRenameName(raw) {
6720
+ const text = normPath(raw);
6721
+ if (!text) return "名称不能为空";
6722
+ if (text.includes("/")) return "新名称不能包含路径分隔符";
6723
+ return checkSegment(text);
6724
+ }
6725
+ /**
6726
+ * 拼接工作区相对路径(dir 为空 = 根;name 新建时可含多段)。
6727
+ * @author ddj 2026年09月22号
6728
+ * @param dir 目标目录(工作区相对)
6729
+ * @param name 名称
6730
+ * @returns 拼接后的相对路径
6731
+ */
6732
+ function joinRelPath(dir, name) {
6733
+ const base = normPath(dir).replace(/\/+$/, "");
6734
+ const add = normPath(name).replace(/^\/+/, "");
6735
+ return base ? base + "/" + add : add;
6736
+ }
6737
+ /**
6738
+ * 取相对路径的父目录(顶层条目返回空串 = 根)。
6739
+ * @author ddj 2026年09月22号
6740
+ * @param path 工作区相对路径
6741
+ * @returns 父目录相对路径
6742
+ */
6743
+ function parentRelOf(path) {
6744
+ const parts = normPath(path).split("/").filter(Boolean);
6745
+ parts.pop();
6746
+ return parts.join("/");
6747
+ }
6748
+ /**
6749
+ * 取相对路径的名称段(末段)。
6750
+ * @author ddj 2026年09月22号
6751
+ * @param path 工作区相对路径
6752
+ * @returns 末段名称
6753
+ */
6754
+ function baseNameOf(path) {
6755
+ return normPath(path).split("/").filter(Boolean).pop() ?? "";
6756
+ }
6757
+ /**
6758
+ * other 是否为 base 本身或其内部路径(复制/移动「不能以自身或其子目录为目标」护栏)。
6759
+ * @author ddj 2026年09月22号
6760
+ * @param base 基准相对路径(源)
6761
+ * @param other 待判定相对路径(目标目录)
6762
+ * @returns 是否为自身或子路径
6763
+ */
6764
+ function isSubPath(base, other) {
6765
+ const b = normPath(base).replace(/\/+$/, "");
6766
+ const o = normPath(other).replace(/\/+$/, "");
6767
+ if (!b) return false;
6768
+ return o === b || o.startsWith(b + "/");
6769
+ }
6673
6770
  //#endregion
6674
6771
  //#region src/reveal.ts
6675
6772
  /**
@@ -7539,6 +7636,103 @@ async function integrationBaseUrlOf(ctx) {
7539
7636
  const value = (ctx.get("settings")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
7540
7637
  return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
7541
7638
  }
7639
+ /**
7640
+ * 文件操作目标解析:工作区相对路径 → 绝对路径,并做工作区边界检查。
7641
+ * edrv.fs.* 各方法共用(对齐 edrv.saveBinary 的 contains 护栏口径)。
7642
+ * @author ddj 2026年09月22号
7643
+ * @param ctx DSH 上下文
7644
+ * @param session 当前会话
7645
+ * @param rel 工作区相对路径
7646
+ * @returns 绝对路径;缺 fs/越界/解析失败返回错误文案
7647
+ */
7648
+ async function fsOpsTarget(ctx, session, rel) {
7649
+ const fs = ctx.get("fs");
7650
+ if (!fs) return { err: "缺少 fs" };
7651
+ try {
7652
+ const target = await resolveTarget(ctx, session, rel);
7653
+ const rootTarget = await fs.resolve(policyOf(ctx, session)?.workspaceRoot ?? ".", {});
7654
+ if (!fs.contains(rootTarget, target)) return { err: "拒绝操作:目标不在会话工作区内" };
7655
+ return { abs: fs.processPath(target) };
7656
+ } catch (error) {
7657
+ return { err: "路径解析失败:" + String(error) };
7658
+ }
7659
+ }
7660
+ /**
7661
+ * 取异常的系统错误码(EEXIST/EXDEV 等分支判定用)。
7662
+ * @author ddj 2026年09月22号
7663
+ * @param error 捕获到的异常
7664
+ * @returns 错误码;取不到返回空串
7665
+ */
7666
+ function fsErrCode(error) {
7667
+ const code = error?.code;
7668
+ return typeof code === "string" ? code : "";
7669
+ }
7670
+ /**
7671
+ * 复制/移动共用核心(edrv.fsCopy / edrv.fsMove):目标目录存在 + 同名拒绝覆盖 +
7672
+ * 「不能以自身或其子目录为目标」护栏;move 跨设备(EXDEV)回退「递归复制 + 删除源」。
7673
+ * @author ddj 2026年09月22号
7674
+ * @param ctx DSH 上下文
7675
+ * @param sc requireSession 成功结果(session + cwd)
7676
+ * @param args 请求载荷(from / toDir 均为工作区相对路径)
7677
+ * @param mode copy = 复制;move = 移动
7678
+ * @returns 成功返回 from/to 相对路径;失败返回错误文案
7679
+ */
7680
+ async function fsTransfer(ctx, sc, args, mode) {
7681
+ const fromRel = normalizeRel(args.from);
7682
+ const toDirRel = normalizeRel(args.toDir);
7683
+ if (fromRel === null || fromRel === "" || toDirRel === null) return { err: "路径不合法" };
7684
+ if (isSubPath(fromRel, toDirRel)) return { err: "不能以自身或其子目录为目标" };
7685
+ const src = await fsOpsTarget(ctx, sc.session, fromRel);
7686
+ if ("err" in src) return src;
7687
+ const dir = await fsOpsTarget(ctx, sc.session, toDirRel);
7688
+ if ("err" in dir) return dir;
7689
+ const dirInfo = await stat(dir.abs).catch(() => null);
7690
+ if (!dirInfo || !dirInfo.isDirectory()) return { err: "目标目录不存在" };
7691
+ const toRel = joinRelPath(toDirRel, baseNameOf(fromRel));
7692
+ const dst = await fsOpsTarget(ctx, sc.session, toRel);
7693
+ if ("err" in dst) return dst;
7694
+ if (!await stat(src.abs).catch(() => null)) return { err: "源不存在" };
7695
+ if (await stat(dst.abs).catch(() => null)) return { err: "目标目录已存在同名文件或目录" };
7696
+ try {
7697
+ await transferEntry(src.abs, dst.abs, mode);
7698
+ } catch (error) {
7699
+ return { err: (mode === "copy" ? "复制失败:" : "移动失败:") + String(error) };
7700
+ }
7701
+ invalidateIndex(ctx, sc.cwd, fromRel);
7702
+ invalidateIndex(ctx, sc.cwd, toRel);
7703
+ return {
7704
+ from: fromRel,
7705
+ to: toRel
7706
+ };
7707
+ }
7708
+ /**
7709
+ * 单条目复制/移动落盘(文件/目录通用;move 跨设备回退复制+删除源)。
7710
+ * @author ddj 2026年09月22号
7711
+ * @param absFrom 源绝对路径
7712
+ * @param absTo 目标绝对路径
7713
+ * @param mode copy = 复制;move = 移动
7714
+ */
7715
+ async function transferEntry(absFrom, absTo, mode) {
7716
+ if (mode === "copy") {
7717
+ await cp(absFrom, absTo, {
7718
+ recursive: true,
7719
+ force: false,
7720
+ errorOnExist: true
7721
+ });
7722
+ return;
7723
+ }
7724
+ try {
7725
+ await rename(absFrom, absTo);
7726
+ } catch (error) {
7727
+ if (fsErrCode(error) !== "EXDEV") throw error;
7728
+ await cp(absFrom, absTo, {
7729
+ recursive: true,
7730
+ force: false,
7731
+ errorOnExist: true
7732
+ });
7733
+ await rm(absFrom, { recursive: true });
7734
+ }
7735
+ }
7542
7736
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
7543
7737
  function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers) {
7544
7738
  return {
@@ -8140,6 +8334,185 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8140
8334
  };
8141
8335
  }
8142
8336
  },
8337
+ "edrv.fsCreateFile": async (args) => {
8338
+ const sc = await requireSession(ctx, args.sessionId);
8339
+ if ("err" in sc) return {
8340
+ ok: false,
8341
+ error: sc.err
8342
+ };
8343
+ const bad = checkNewName(args.path);
8344
+ if (bad) return {
8345
+ ok: false,
8346
+ error: bad
8347
+ };
8348
+ const r = await fsOpsTarget(ctx, sc.session, args.path);
8349
+ if ("err" in r) return {
8350
+ ok: false,
8351
+ error: r.err
8352
+ };
8353
+ try {
8354
+ await mkdir(dirname(r.abs), { recursive: true });
8355
+ await writeFile(r.abs, "", { flag: "wx" });
8356
+ } catch (error) {
8357
+ return {
8358
+ ok: false,
8359
+ error: fsErrCode(error) === "EEXIST" ? "已存在同名文件或目录" : "新建文件失败:" + String(error)
8360
+ };
8361
+ }
8362
+ invalidateIndex(ctx, sc.cwd, args.path);
8363
+ return {
8364
+ ok: true,
8365
+ path: args.path
8366
+ };
8367
+ },
8368
+ "edrv.fsCreateDir": async (args) => {
8369
+ const sc = await requireSession(ctx, args.sessionId);
8370
+ if ("err" in sc) return {
8371
+ ok: false,
8372
+ error: sc.err
8373
+ };
8374
+ const bad = checkNewName(args.path);
8375
+ if (bad) return {
8376
+ ok: false,
8377
+ error: bad
8378
+ };
8379
+ const r = await fsOpsTarget(ctx, sc.session, args.path);
8380
+ if ("err" in r) return {
8381
+ ok: false,
8382
+ error: r.err
8383
+ };
8384
+ try {
8385
+ await mkdir(dirname(r.abs), { recursive: true });
8386
+ await mkdir(r.abs);
8387
+ } catch (error) {
8388
+ return {
8389
+ ok: false,
8390
+ error: fsErrCode(error) === "EEXIST" ? "已存在同名文件或目录" : "新建文件夹失败:" + String(error)
8391
+ };
8392
+ }
8393
+ invalidateIndex(ctx, sc.cwd, args.path);
8394
+ return {
8395
+ ok: true,
8396
+ path: args.path
8397
+ };
8398
+ },
8399
+ "edrv.fsRename": async (args) => {
8400
+ const sc = await requireSession(ctx, args.sessionId);
8401
+ if ("err" in sc) return {
8402
+ ok: false,
8403
+ error: sc.err
8404
+ };
8405
+ const bad = checkRenameName(args.newName);
8406
+ if (bad) return {
8407
+ ok: false,
8408
+ error: bad
8409
+ };
8410
+ const fromRel = normalizeRel(args.path);
8411
+ if (fromRel === null || fromRel === "") return {
8412
+ ok: false,
8413
+ error: "路径不合法"
8414
+ };
8415
+ const toRel = joinRelPath(parentRelOf(fromRel), args.newName);
8416
+ if (toRel === fromRel) return {
8417
+ ok: true,
8418
+ from: fromRel,
8419
+ to: fromRel
8420
+ };
8421
+ const src = await fsOpsTarget(ctx, sc.session, fromRel);
8422
+ if ("err" in src) return {
8423
+ ok: false,
8424
+ error: src.err
8425
+ };
8426
+ const dst = await fsOpsTarget(ctx, sc.session, toRel);
8427
+ if ("err" in dst) return {
8428
+ ok: false,
8429
+ error: dst.err
8430
+ };
8431
+ if (!await stat(src.abs).catch(() => null)) return {
8432
+ ok: false,
8433
+ error: "源不存在"
8434
+ };
8435
+ try {
8436
+ await rename(src.abs, dst.abs);
8437
+ } catch (error) {
8438
+ const code = fsErrCode(error);
8439
+ return {
8440
+ ok: false,
8441
+ error: code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM" ? "已存在同名文件或目录" : "重命名失败:" + String(error)
8442
+ };
8443
+ }
8444
+ invalidateIndex(ctx, sc.cwd, fromRel);
8445
+ invalidateIndex(ctx, sc.cwd, toRel);
8446
+ return {
8447
+ ok: true,
8448
+ from: fromRel,
8449
+ to: toRel
8450
+ };
8451
+ },
8452
+ "edrv.fsDelete": async (args) => {
8453
+ const sc = await requireSession(ctx, args.sessionId);
8454
+ if ("err" in sc) return {
8455
+ ok: false,
8456
+ error: sc.err
8457
+ };
8458
+ const rel = normalizeRel(args.path);
8459
+ if (rel === null || rel === "") return {
8460
+ ok: false,
8461
+ error: "路径不合法"
8462
+ };
8463
+ const r = await fsOpsTarget(ctx, sc.session, rel);
8464
+ if ("err" in r) return {
8465
+ ok: false,
8466
+ error: r.err
8467
+ };
8468
+ try {
8469
+ await rm(r.abs, { recursive: true });
8470
+ } catch (error) {
8471
+ return {
8472
+ ok: false,
8473
+ error: "删除失败:" + String(error)
8474
+ };
8475
+ }
8476
+ invalidateIndex(ctx, sc.cwd, rel);
8477
+ return {
8478
+ ok: true,
8479
+ path: rel
8480
+ };
8481
+ },
8482
+ "edrv.fsCopy": async (args) => {
8483
+ const sc = await requireSession(ctx, args.sessionId);
8484
+ if ("err" in sc) return {
8485
+ ok: false,
8486
+ error: sc.err
8487
+ };
8488
+ const r = await fsTransfer(ctx, sc, args, "copy");
8489
+ if ("err" in r) return {
8490
+ ok: false,
8491
+ error: r.err
8492
+ };
8493
+ return {
8494
+ ok: true,
8495
+ from: r.from,
8496
+ to: r.to
8497
+ };
8498
+ },
8499
+ "edrv.fsMove": async (args) => {
8500
+ const sc = await requireSession(ctx, args.sessionId);
8501
+ if ("err" in sc) return {
8502
+ ok: false,
8503
+ error: sc.err
8504
+ };
8505
+ const r = await fsTransfer(ctx, sc, args, "move");
8506
+ if ("err" in r) return {
8507
+ ok: false,
8508
+ error: r.err
8509
+ };
8510
+ return {
8511
+ ok: true,
8512
+ from: r.from,
8513
+ to: r.to
8514
+ };
8515
+ },
8143
8516
  "mcp.list": async () => ({
8144
8517
  ok: true,
8145
8518
  ...listMcp(ctx)