dsh-vscode-mode 0.14.11 → 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/lib/index.js CHANGED
@@ -6563,14 +6563,36 @@ function pathKey(value) {
6563
6563
  return pathText(value).toLocaleLowerCase("en-US");
6564
6564
  }
6565
6565
  /**
6566
- * 判断路径是否包含连续 query。
6567
- * @author ddj 2026年08月24号
6568
- * @param value 文件路径
6566
+ * @public
6567
+ * @author ddj 2026年10月14号
6568
+ * @description 按顺序匹配不连续字符;每个位置最多消费一次。
6569
+ * @param value 已归一化候选文本
6570
+ * @param text 已归一化查询文本
6571
+ * @returns 是否完整匹配所有查询字符
6572
+ */
6573
+ function subsequence(value, text) {
6574
+ let offset = 0;
6575
+ for (const char of text) {
6576
+ const index = value.indexOf(char, offset);
6577
+ if (index < 0) return false;
6578
+ offset = index + char.length;
6579
+ }
6580
+ return true;
6581
+ }
6582
+ /**
6583
+ * @public
6584
+ * @author ddj 2026年10月14号
6585
+ * @description 连续匹配相对路径,或对无目录查询模糊匹配文件名。
6586
+ * @param value 文件路径(调用方移除工作区根前缀)
6569
6587
  * @param query 已规范化 query
6570
- * @returns 是否命中
6588
+ * @returns 是否命中;空查询不命中
6571
6589
  */
6572
6590
  function pathMatch(value, query) {
6573
- return pathText(value).toLocaleLowerCase("en-US").includes(query.text);
6591
+ if (!query.text) return false;
6592
+ const path = pathText(value).toLocaleLowerCase("en-US");
6593
+ if (path.includes(query.text)) return true;
6594
+ if (query.text.includes("/")) return false;
6595
+ return subsequence(path.slice(path.lastIndexOf("/") + 1), query.text);
6574
6596
  }
6575
6597
  //#endregion
6576
6598
  //#region src/search/ripgrep.ts
@@ -6628,18 +6650,32 @@ const FILE_EXCLUDES = [
6628
6650
  * @returns 安全 glob
6629
6651
  */
6630
6652
  function queryGlob(query) {
6653
+ return "**/*" + Array.from(pathText(query), globChar).join("") + "*";
6654
+ }
6655
+ /**
6656
+ * @private
6657
+ * @author ddj 2026年10月14号
6658
+ * @description 将字面字符转为安全且大小写不敏感的 glob 片段。
6659
+ * @param char 单个 Unicode 字符
6660
+ * @returns 转义字符或大小写字符类
6661
+ */
6662
+ function globChar(char) {
6663
+ const lower = char.toLowerCase();
6664
+ const upper = char.toUpperCase();
6665
+ if (lower !== upper && lower.length === 1 && upper.length === 1) return "[" + lower + upper + "]";
6666
+ return /[\*?\[\]{}!]/.test(char) ? "\\" + char : char;
6667
+ }
6668
+ /**
6669
+ * @public
6670
+ * @author ddj 2026年10月14号
6671
+ * @description 构造仅匹配文件名的有序子序列 glob,不让星号跨目录。
6672
+ * @param query 无目录的字面查询文本
6673
+ * @returns 模糊 glob;空查询或带目录时返回 null
6674
+ */
6675
+ function fuzzyGlob(query) {
6631
6676
  const value = pathText(query);
6632
- let body = "";
6633
- for (const char of value) {
6634
- const lower = char.toLowerCase();
6635
- const upper = char.toUpperCase();
6636
- if (lower !== upper && lower.length === 1 && upper.length === 1) {
6637
- body += "[" + lower + upper + "]";
6638
- continue;
6639
- }
6640
- body += /[\*?\[\]{}!]/.test(char) ? "\\" + char : char;
6641
- }
6642
- return "**/*" + body + "*";
6677
+ if (!value || value.includes("/")) return null;
6678
+ return "**/*" + Array.from(value, globChar).join("*") + "*";
6643
6679
  }
6644
6680
  /**
6645
6681
  * 返回当前平台的 rg 可执行文件路径,不让可选依赖影响模块加载。
@@ -6773,6 +6809,8 @@ function filesArgv(binary, root, query) {
6773
6809
  "--glob",
6774
6810
  queryGlob(query)
6775
6811
  ];
6812
+ const fuzzy = fuzzyGlob(query);
6813
+ if (fuzzy) argv.push("--glob", fuzzy);
6776
6814
  for (const excluded of EXCLUDES) argv.push("--glob", "!**/" + excluded + "/**");
6777
6815
  for (const excluded of FILE_EXCLUDES) argv.push("--glob", "!" + excluded);
6778
6816
  argv.push("--", root);
@@ -6944,30 +6982,31 @@ const EXCLUDED = /* @__PURE__ */ new Set([
6944
6982
  ]);
6945
6983
  /**
6946
6984
  * 在一个目录树中递归查找匹配文件。
6947
- * @author ddj 2026年08月24号
6985
+ * @private
6986
+ * @author ddj 2026年10月14号
6948
6987
  * @param fs 文件系统能力
6949
6988
  * @param target 当前目录目标
6950
6989
  * @param input 搜索输入
6951
6990
  * @param files 已保留候选
6952
6991
  * @param signal 取消信号
6992
+ * @param query 已规范化查询
6993
+ * @param prefix 当前目录相对于搜索根的路径
6953
6994
  * @returns 是否因达到上限而截断
6954
6995
  */
6955
- async function walk(fs, target, input, files, signal) {
6996
+ async function walk(fs, target, input, files, signal, query, prefix = "") {
6956
6997
  if (signal.aborted) throw new DOMException("搜索已取消", "AbortError");
6957
6998
  const entries = await fs.listDir(target, signal);
6958
6999
  for (const entry of entries) {
6959
7000
  if (signal.aborted) throw new DOMException("搜索已取消", "AbortError");
7001
+ const relative = prefix + entry.name;
6960
7002
  if (entry.type === "directory") {
6961
7003
  if (EXCLUDED.has(entry.name)) continue;
6962
- if (await walk(fs, entry.target, input, files, signal)) return true;
7004
+ if (await walk(fs, entry.target, input, files, signal, query, relative + "/")) return true;
6963
7005
  continue;
6964
7006
  }
6965
7007
  if (entry.type !== "file") continue;
6966
- const path = pathText(entry.target.displayPath ?? entry.name);
6967
- if (!pathMatch(path, {
6968
- raw: input.query,
6969
- text: pathText(input.query).toLocaleLowerCase("en-US")
6970
- })) continue;
7008
+ const path = pathText(entry.target.displayPath ?? relative);
7009
+ if (!pathMatch(relative, query)) continue;
6971
7010
  files.push(path);
6972
7011
  if (files.length >= Math.min(input.maxResults, RESULT_CAP)) return true;
6973
7012
  }
@@ -6990,7 +7029,7 @@ async function searchFallback(input) {
6990
7029
  } : { signal: input.signal });
6991
7030
  const signal = input.signal ?? new AbortController().signal;
6992
7031
  const files = [];
6993
- const truncated = await walk(fs, rootTarget, input, files, signal);
7032
+ const truncated = await walk(fs, rootTarget, input, files, signal, prepareQuery(input.query));
6994
7033
  return {
6995
7034
  files,
6996
7035
  truncated,
@@ -7025,29 +7064,82 @@ function candidateOf(path, source) {
7025
7064
  };
7026
7065
  }
7027
7066
  /**
7028
- * 对连续匹配候选进行排序。
7029
- * @author ddj 2026年08月24号
7067
+ * @private
7068
+ * @author ddj 2026年10月14号
7069
+ * @description 贪心匹配文件名,优先词/驼峰边界,其次紧凑程度;不枚举组合。
7070
+ * @param name 原始大小写文件名
7071
+ * @param text 已规范化查询
7072
+ * @returns 边界漏命中数与跨度代价;无匹配返回 null
7073
+ */
7074
+ function fuzzyCost(name, text) {
7075
+ const base = name.toLocaleLowerCase("en-US");
7076
+ let offset = 0;
7077
+ let first = -1;
7078
+ let misses = 0;
7079
+ for (const char of text) {
7080
+ const index = base.indexOf(char, offset);
7081
+ if (index < 0) return null;
7082
+ if (first < 0) first = index;
7083
+ if (!(index === 0 || /[\s_.\-]/.test(name[index - 1]) || /[a-z0-9]/.test(name[index - 1]) && /[A-Z]/.test(name[index]))) misses++;
7084
+ offset = index + char.length;
7085
+ }
7086
+ return [misses, offset - first - text.length];
7087
+ }
7088
+ /**
7089
+ * @private
7090
+ * @author ddj 2026年10月14号
7091
+ * @description 计算完全/前缀/连续/模糊/路径命中的排序等级。
7092
+ * @param candidate 文件候选
7093
+ * @param text 已规范化查询
7094
+ * @returns 等级、边界与跨度代价
7095
+ */
7096
+ function rankKey(candidate, text) {
7097
+ const base = candidate.basename.toLocaleLowerCase("en-US");
7098
+ if (base === text) return [
7099
+ 0,
7100
+ 0,
7101
+ 0
7102
+ ];
7103
+ if (base.startsWith(text)) return [
7104
+ 1,
7105
+ 0,
7106
+ 0
7107
+ ];
7108
+ if (base.includes(text)) return [
7109
+ 2,
7110
+ 0,
7111
+ 0
7112
+ ];
7113
+ const cost = text.includes("/") ? null : fuzzyCost(candidate.basename, text);
7114
+ if (cost) return [3, ...cost];
7115
+ return [
7116
+ candidate.normalizedPath.includes(text) ? 4 : 5,
7117
+ 0,
7118
+ 0
7119
+ ];
7120
+ }
7121
+ /**
7122
+ * @public
7123
+ * @author ddj 2026年10月14号
7124
+ * @description 每个候选只评分一次,排序不修改调用方数组。
7030
7125
  * @param candidates 候选列表
7031
7126
  * @param query 已规范化 query
7032
7127
  * @returns 排序后的候选
7033
7128
  */
7034
7129
  function rankCandidates(candidates, query) {
7035
- const text = query.text;
7036
- const score = (candidate) => {
7037
- const base = candidate.basename.toLocaleLowerCase("en-US");
7038
- if (base.startsWith(text)) return 0;
7039
- if (base.includes(text)) return 1;
7040
- if (candidate.normalizedPath.includes(text)) return 2;
7041
- return 3;
7042
- };
7043
- return [...candidates].sort((left, right) => score(left) - score(right) || left.normalizedPath.localeCompare(right.normalizedPath, "en-US"));
7130
+ const ranked = candidates.map((candidate) => ({
7131
+ candidate,
7132
+ key: rankKey(candidate, query.text)
7133
+ }));
7134
+ ranked.sort((left, right) => left.key[0] - right.key[0] || left.key[1] - right.key[1] || left.key[2] - right.key[2] || left.candidate.normalizedPath.localeCompare(right.candidate.normalizedPath, "en-US") || left.candidate.path.localeCompare(right.candidate.path, "en-US"));
7135
+ return ranked.map((item) => item.candidate);
7044
7136
  }
7045
7137
  //#endregion
7046
7138
  //#region src/search/orchestrator.ts
7047
7139
  const CACHE_TTL = 6e4;
7048
7140
  const CACHE_LIMIT = 100;
7049
7141
  const RESULT_LIMIT = 50;
7050
- const PROVIDER_VERSION$1 = "ripgrep-v1";
7142
+ const PROVIDER_VERSION$1 = "ripgrep-fuzzy-v2";
7051
7143
  const POLICY_VERSION = "search-policy-v1";
7052
7144
  /**
7053
7145
  * 短期有界搜索缓存(泛型:文件/内容搜索共用;clone 由调用方保证写隔离)。