@trim21/personal-pi-extensions 0.1.476 → 0.1.477

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.476",
3
+ "version": "0.1.477",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -75,14 +75,27 @@ export class RenameNotPossibleError extends Error {}
75
75
  */
76
76
  export class RenameIncompleteError extends Error {
77
77
  readonly missing: readonly string[];
78
- constructor(missing: readonly string[]) {
78
+ readonly extra: readonly string[];
79
+ constructor(missing: readonly string[], extra: readonly string[] = []) {
80
+ const parts: string[] = [];
81
+ if (missing.length > 0) {
82
+ parts.push(
83
+ `textDocument/references found the symbol in ${missing.length} file(s) ` +
84
+ `that the rename edit does not cover (${missing.join(", ")})`,
85
+ );
86
+ }
87
+ if (extra.length > 0) {
88
+ parts.push(
89
+ `the rename edit touches ${extra.length} file(s) ` +
90
+ `that textDocument/references did not report (${extra.join(", ")})`,
91
+ );
92
+ }
79
93
  super(
80
- `LSP rename incomplete: textDocument/references found the symbol in ` +
81
- `${missing.length} file(s) that the rename edit does not cover ` +
82
- `(${missing.join(", ")}). The server index may still be loading; ` +
83
- `nothing was modified, retry shortly.`,
94
+ `LSP rename incomplete: ${parts.join("; ")}. ` +
95
+ `The server index may still be loading; nothing was modified, retry shortly.`,
84
96
  );
85
97
  this.missing = missing;
98
+ this.extra = extra;
86
99
  }
87
100
  }
88
101
 
@@ -956,14 +969,18 @@ export async function create(input: CreateInput): Promise<LspClient> {
956
969
  const notRenameable = () =>
957
970
  new RenameNotPossibleError(`LSP server "${input.serverID}" cannot rename at ${at}`);
958
971
 
959
- // references 前置 + rename 覆盖校验:LSP 没有标准化的"索引完成"信号,
972
+ // references 前置 + rename 双向校验:LSP 没有标准化的"索引完成"信号,
960
973
  // 服务器(如 tsserver)可能在项目加载完成前回答,导致 rename 漏掉
961
- // 尚未入索引的文件。对策分两层:
974
+ // 尚未入索引的文件。对策分三层:
962
975
  // 1. 收敛检测:references 连续两次文件集合一致才认为索引稳定,防止
963
976
  // "服务器根本还没发现某文件"时校验形同虚设;
964
- // 2. 覆盖校验:references 报告的文件必须都被 rename edit 覆盖,
965
- // 不完整时等待重试,预算耗尽仍不完整抛 RenameIncompleteError——
966
- // 调用方尚未写盘,整个操作无副作用,可稍后重试。
977
+ // 2. 覆盖校验(missing):references 报告的文件必须都被 rename edit
978
+ // 覆盖,缺失说明服务器索引落后,抛 RenameIncompleteError;
979
+ // 3. 一致性校验(extra):rename 触及的文件超出已收敛的 references
980
+ // 集合,说明两次请求之间项目覆盖在增长(rename 晚于 references,
981
+ // 索引仍在加载),此时 rename 的结果本身不可信——回到 references
982
+ // 轮询等重新收敛,再重发 rename 复检;预算耗尽仍不一致时抛
983
+ // RenameIncompleteError——调用方尚未写盘,整个操作无副作用。
967
984
  // 服务器不支持 references(MethodNotFound)时跳过校验,信任服务器,
968
985
  // 与编辑器行为一致。
969
986
  const referencesRequest = () =>
@@ -1028,14 +1045,27 @@ export async function create(input: CreateInput): Promise<LspClient> {
1028
1045
  let current = toPaths(locations);
1029
1046
  for (;;) {
1030
1047
  const settled = previous !== undefined && samePaths(previous, current);
1031
- if (settled || Date.now() >= deadline) {
1048
+ const expired = Date.now() >= deadline;
1049
+ if (settled || expired) {
1032
1050
  const edit = await sendRename();
1033
1051
  if (!edit) throw notRenameable();
1034
- const missing = [...current].filter((path) => !editFilePaths(edit).has(path));
1035
- if (missing.length === 0) {
1052
+ const editPaths = editFilePaths(edit);
1053
+ const missing: string[] = [];
1054
+ const extra: string[] = [];
1055
+ for (const path of current) {
1056
+ if (!editPaths.has(path)) missing.push(path);
1057
+ }
1058
+ for (const path of editPaths) {
1059
+ if (!current.has(path)) extra.push(path);
1060
+ }
1061
+ if (missing.length === 0 && extra.length === 0) {
1036
1062
  return placeholder === undefined ? { edit } : { edit, placeholder };
1037
1063
  }
1038
- throw new RenameIncompleteError(missing);
1064
+ if (expired || missing.length > 0) {
1065
+ throw new RenameIncompleteError(missing, extra);
1066
+ }
1067
+ // 收敛后 rename 仍报出 references 没有的文件:references 快照已过时,
1068
+ // 继续轮询到重新收敛后再重发 rename 复检(预算耗尽则向上抛)。
1039
1069
  }
1040
1070
  previous = current;
1041
1071
  await sleep(renameVerificationTiming.pollMs);