@trim21/personal-pi-extensions 0.1.476 → 0.1.478
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 +1 -1
- package/src/lib/lsp/client.ts +87 -28
package/package.json
CHANGED
package/src/lib/lsp/client.ts
CHANGED
|
@@ -63,6 +63,9 @@ function samePaths(a: Set<string>, b: Set<string>): boolean {
|
|
|
63
63
|
/** LSP MethodNotFound(-32601):服务器未实现 prepareRename / rename 请求。 */
|
|
64
64
|
const LSP_METHOD_NOT_FOUND = -32601;
|
|
65
65
|
|
|
66
|
+
/** LSP ContentModified(-32801):服务器处理请求期间内容被修改,重发请求即可。 */
|
|
67
|
+
const LSP_CONTENT_MODIFIED = -32801;
|
|
68
|
+
|
|
66
69
|
/**
|
|
67
70
|
* 位置不可 rename 或服务器不具备 rename 能力;与传输失败等意外错误区分,
|
|
68
71
|
* 供调用方在多候选探测时跳过该 client 而不是中断整个操作。
|
|
@@ -75,14 +78,27 @@ export class RenameNotPossibleError extends Error {}
|
|
|
75
78
|
*/
|
|
76
79
|
export class RenameIncompleteError extends Error {
|
|
77
80
|
readonly missing: readonly string[];
|
|
78
|
-
|
|
81
|
+
readonly extra: readonly string[];
|
|
82
|
+
constructor(missing: readonly string[], extra: readonly string[] = []) {
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
if (missing.length > 0) {
|
|
85
|
+
parts.push(
|
|
86
|
+
`textDocument/references found the symbol in ${missing.length} file(s) ` +
|
|
87
|
+
`that the rename edit does not cover (${missing.join(", ")})`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (extra.length > 0) {
|
|
91
|
+
parts.push(
|
|
92
|
+
`the rename edit touches ${extra.length} file(s) ` +
|
|
93
|
+
`that textDocument/references did not report (${extra.join(", ")})`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
79
96
|
super(
|
|
80
|
-
`LSP rename incomplete:
|
|
81
|
-
|
|
82
|
-
`(${missing.join(", ")}). The server index may still be loading; ` +
|
|
83
|
-
`nothing was modified, retry shortly.`,
|
|
97
|
+
`LSP rename incomplete: ${parts.join("; ")}. ` +
|
|
98
|
+
`The server index may still be loading; nothing was modified, retry shortly.`,
|
|
84
99
|
);
|
|
85
100
|
this.missing = missing;
|
|
101
|
+
this.extra = extra;
|
|
86
102
|
}
|
|
87
103
|
}
|
|
88
104
|
|
|
@@ -90,7 +106,27 @@ export class RenameIncompleteError extends Error {
|
|
|
90
106
|
* rename 覆盖校验的轮询节奏。budgetMs 是 references 收敛 + 重试的总预算;
|
|
91
107
|
* 测试可临时缩小以缩短等待。
|
|
92
108
|
*/
|
|
93
|
-
export const renameVerificationTiming = {
|
|
109
|
+
export const renameVerificationTiming = {
|
|
110
|
+
pollMs: 400,
|
|
111
|
+
budgetMs: 10_000,
|
|
112
|
+
/** ContentModified(-32801) 重试上限:服务器处理期间文档被修改,重发请求即可。 */
|
|
113
|
+
contentModifiedRetries: 3,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ContentModified 语义:请求处理期间 salsa 数据库被文件变更修改,服务器请
|
|
118
|
+
* 客户端重发请求。重发无副作用,直接重试;仅连续超限才向上抛,避免无限循环。
|
|
119
|
+
*/
|
|
120
|
+
async function retryOnContentModified<T>(fn: () => Promise<T>): Promise<T> {
|
|
121
|
+
for (let attempt = 0; ; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
return await fn();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!(error instanceof ResponseError && error.code === LSP_CONTENT_MODIFIED)) throw error;
|
|
126
|
+
if (attempt >= renameVerificationTiming.contentModifiedRetries) throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
94
130
|
|
|
95
131
|
interface PrepareRenameResponse {
|
|
96
132
|
range?: unknown;
|
|
@@ -956,22 +992,28 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
956
992
|
const notRenameable = () =>
|
|
957
993
|
new RenameNotPossibleError(`LSP server "${input.serverID}" cannot rename at ${at}`);
|
|
958
994
|
|
|
959
|
-
// references 前置 + rename
|
|
995
|
+
// references 前置 + rename 双向校验:LSP 没有标准化的"索引完成"信号,
|
|
960
996
|
// 服务器(如 tsserver)可能在项目加载完成前回答,导致 rename 漏掉
|
|
961
|
-
//
|
|
997
|
+
// 尚未入索引的文件。对策分三层:
|
|
962
998
|
// 1. 收敛检测:references 连续两次文件集合一致才认为索引稳定,防止
|
|
963
999
|
// "服务器根本还没发现某文件"时校验形同虚设;
|
|
964
|
-
// 2.
|
|
965
|
-
//
|
|
966
|
-
//
|
|
1000
|
+
// 2. 覆盖校验(missing):references 报告的文件必须都被 rename edit
|
|
1001
|
+
// 覆盖,缺失说明服务器索引落后,抛 RenameIncompleteError;
|
|
1002
|
+
// 3. 一致性校验(extra):rename 触及的文件超出已收敛的 references
|
|
1003
|
+
// 集合,说明两次请求之间项目覆盖在增长(rename 晚于 references,
|
|
1004
|
+
// 索引仍在加载),此时 rename 的结果本身不可信——回到 references
|
|
1005
|
+
// 轮询等重新收敛,再重发 rename 复检;预算耗尽仍不一致时抛
|
|
1006
|
+
// RenameIncompleteError——调用方尚未写盘,整个操作无副作用。
|
|
967
1007
|
// 服务器不支持 references(MethodNotFound)时跳过校验,信任服务器,
|
|
968
1008
|
// 与编辑器行为一致。
|
|
969
1009
|
const referencesRequest = () =>
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1010
|
+
retryOnContentModified(() =>
|
|
1011
|
+
connection.sendRequest<{ uri: string }[] | null>("textDocument/references", {
|
|
1012
|
+
textDocument: { uri },
|
|
1013
|
+
position,
|
|
1014
|
+
context: { includeDeclaration: true },
|
|
1015
|
+
}),
|
|
1016
|
+
);
|
|
975
1017
|
|
|
976
1018
|
const toPaths = (locations: { uri: string }[] | null): Set<string> =>
|
|
977
1019
|
new Set(
|
|
@@ -982,11 +1024,13 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
982
1024
|
|
|
983
1025
|
const sendRename = async (): Promise<WorkspaceEdit | null> => {
|
|
984
1026
|
try {
|
|
985
|
-
return await
|
|
986
|
-
textDocument
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1027
|
+
return await retryOnContentModified(() =>
|
|
1028
|
+
connection.sendRequest<WorkspaceEdit | null>("textDocument/rename", {
|
|
1029
|
+
textDocument: { uri },
|
|
1030
|
+
position,
|
|
1031
|
+
newName: request.newName,
|
|
1032
|
+
}),
|
|
1033
|
+
);
|
|
990
1034
|
} catch (error) {
|
|
991
1035
|
if (error instanceof ResponseError && error.code === LSP_METHOD_NOT_FOUND) {
|
|
992
1036
|
throw notRenameable();
|
|
@@ -999,9 +1043,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
999
1043
|
if (hasPrepareProvider) {
|
|
1000
1044
|
let prepared: PrepareRenameResponse | null;
|
|
1001
1045
|
try {
|
|
1002
|
-
prepared = await
|
|
1003
|
-
"textDocument/prepareRename",
|
|
1004
|
-
|
|
1046
|
+
prepared = await retryOnContentModified(() =>
|
|
1047
|
+
connection.sendRequest<PrepareRenameResponse | null>("textDocument/prepareRename", {
|
|
1048
|
+
textDocument: { uri },
|
|
1049
|
+
position,
|
|
1050
|
+
}),
|
|
1005
1051
|
);
|
|
1006
1052
|
} catch (error) {
|
|
1007
1053
|
if (error instanceof ResponseError && error.code === LSP_METHOD_NOT_FOUND) {
|
|
@@ -1028,14 +1074,27 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1028
1074
|
let current = toPaths(locations);
|
|
1029
1075
|
for (;;) {
|
|
1030
1076
|
const settled = previous !== undefined && samePaths(previous, current);
|
|
1031
|
-
|
|
1077
|
+
const expired = Date.now() >= deadline;
|
|
1078
|
+
if (settled || expired) {
|
|
1032
1079
|
const edit = await sendRename();
|
|
1033
1080
|
if (!edit) throw notRenameable();
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1081
|
+
const editPaths = editFilePaths(edit);
|
|
1082
|
+
const missing: string[] = [];
|
|
1083
|
+
const extra: string[] = [];
|
|
1084
|
+
for (const path of current) {
|
|
1085
|
+
if (!editPaths.has(path)) missing.push(path);
|
|
1086
|
+
}
|
|
1087
|
+
for (const path of editPaths) {
|
|
1088
|
+
if (!current.has(path)) extra.push(path);
|
|
1089
|
+
}
|
|
1090
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
1036
1091
|
return placeholder === undefined ? { edit } : { edit, placeholder };
|
|
1037
1092
|
}
|
|
1038
|
-
|
|
1093
|
+
if (expired || missing.length > 0) {
|
|
1094
|
+
throw new RenameIncompleteError(missing, extra);
|
|
1095
|
+
}
|
|
1096
|
+
// 收敛后 rename 仍报出 references 没有的文件:references 快照已过时,
|
|
1097
|
+
// 继续轮询到重新收敛后再重发 rename 复检(预算耗尽则向上抛)。
|
|
1039
1098
|
}
|
|
1040
1099
|
previous = current;
|
|
1041
1100
|
await sleep(renameVerificationTiming.pollMs);
|