@trim21/personal-pi-extensions 0.1.463 → 0.1.466
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 +82 -22
- package/src/lib/lsp/rename.ts +8 -0
package/package.json
CHANGED
package/src/lib/lsp/client.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { Diagnostic as VSCodeDiagnostic, WorkspaceEdit } from "vscode-langu
|
|
|
24
24
|
|
|
25
25
|
import type { LspServerHandle } from "./adapter.js";
|
|
26
26
|
import { LANGUAGE_EXTENSIONS } from "./language.js";
|
|
27
|
+
import { editFilePaths } from "./rename.js";
|
|
27
28
|
import type { FileChange, FileChangeType } from "./watcher.js";
|
|
28
29
|
|
|
29
30
|
// LSP spec 常量
|
|
@@ -52,6 +53,13 @@ const FILE_CHANGE_TYPE: Record<FileChangeType, number> = {
|
|
|
52
53
|
|
|
53
54
|
export type Diagnostic = VSCodeDiagnostic;
|
|
54
55
|
|
|
56
|
+
/** 两个路径集合是否一致(用于判断 references 结果是否收敛)。 */
|
|
57
|
+
function samePaths(a: Set<string>, b: Set<string>): boolean {
|
|
58
|
+
if (a.size !== b.size) return false;
|
|
59
|
+
for (const path of a) if (!b.has(path)) return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/** LSP MethodNotFound(-32601):服务器未实现 prepareRename / rename 请求。 */
|
|
56
64
|
const LSP_METHOD_NOT_FOUND = -32601;
|
|
57
65
|
|
|
@@ -61,6 +69,29 @@ const LSP_METHOD_NOT_FOUND = -32601;
|
|
|
61
69
|
*/
|
|
62
70
|
export class RenameNotPossibleError extends Error {}
|
|
63
71
|
|
|
72
|
+
/**
|
|
73
|
+
* rename edit 未覆盖 references 看到的全部文件:服务器索引可能仍在后台加载。
|
|
74
|
+
* 抛出时发生在写盘之前,整个 rename 无副作用,可稍后重试。
|
|
75
|
+
*/
|
|
76
|
+
export class RenameIncompleteError extends Error {
|
|
77
|
+
readonly missing: readonly string[];
|
|
78
|
+
constructor(missing: readonly string[]) {
|
|
79
|
+
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.`,
|
|
84
|
+
);
|
|
85
|
+
this.missing = missing;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* rename 覆盖校验的轮询节奏。budgetMs 是 references 收敛 + 重试的总预算;
|
|
91
|
+
* 测试可临时缩小以缩短等待。
|
|
92
|
+
*/
|
|
93
|
+
export const renameVerificationTiming = { pollMs: 400, budgetMs: 10_000 };
|
|
94
|
+
|
|
64
95
|
interface PrepareRenameResponse {
|
|
65
96
|
range?: unknown;
|
|
66
97
|
placeholder?: string;
|
|
@@ -925,24 +956,29 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
925
956
|
const notRenameable = () =>
|
|
926
957
|
new RenameNotPossibleError(`LSP server "${input.serverID}" cannot rename at ${at}`);
|
|
927
958
|
|
|
928
|
-
// references
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
959
|
+
// references 前置 + rename 覆盖校验:LSP 没有标准化的"索引完成"信号,
|
|
960
|
+
// 服务器(如 tsserver)可能在项目加载完成前回答,导致 rename 漏掉
|
|
961
|
+
// 尚未入索引的文件。对策分两层:
|
|
962
|
+
// 1. 收敛检测:references 连续两次文件集合一致才认为索引稳定,防止
|
|
963
|
+
// "服务器根本还没发现某文件"时校验形同虚设;
|
|
964
|
+
// 2. 覆盖校验:references 报告的文件必须都被 rename edit 覆盖,
|
|
965
|
+
// 不完整时等待重试,预算耗尽仍不完整抛 RenameIncompleteError——
|
|
966
|
+
// 调用方尚未写盘,整个操作无副作用,可稍后重试。
|
|
967
|
+
// 服务器不支持 references(MethodNotFound)时跳过校验,信任服务器,
|
|
968
|
+
// 与编辑器行为一致。
|
|
969
|
+
const referencesRequest = () =>
|
|
970
|
+
connection.sendRequest<{ uri: string }[] | null>("textDocument/references", {
|
|
971
|
+
textDocument: { uri },
|
|
972
|
+
position,
|
|
973
|
+
context: { includeDeclaration: true },
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const toPaths = (locations: { uri: string }[] | null): Set<string> =>
|
|
977
|
+
new Set(
|
|
978
|
+
(locations ?? []).flatMap((location) =>
|
|
979
|
+
location.uri.startsWith("file:") ? [normalize(fileURLToPath(location.uri))] : [],
|
|
980
|
+
),
|
|
981
|
+
);
|
|
946
982
|
|
|
947
983
|
const sendRename = async (): Promise<WorkspaceEdit | null> => {
|
|
948
984
|
try {
|
|
@@ -977,10 +1013,34 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
977
1013
|
if (typeof prepared.placeholder === "string") placeholder = prepared.placeholder;
|
|
978
1014
|
}
|
|
979
1015
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1016
|
+
let locations: { uri: string }[] | null;
|
|
1017
|
+
try {
|
|
1018
|
+
locations = await referencesRequest();
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
if (!(error instanceof ResponseError && error.code === LSP_METHOD_NOT_FOUND)) throw error;
|
|
1021
|
+
const edit = await sendRename();
|
|
1022
|
+
if (!edit) throw notRenameable();
|
|
1023
|
+
return placeholder === undefined ? { edit } : { edit, placeholder };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const deadline = Date.now() + renameVerificationTiming.budgetMs;
|
|
1027
|
+
let previous: Set<string> | undefined;
|
|
1028
|
+
let current = toPaths(locations);
|
|
1029
|
+
for (;;) {
|
|
1030
|
+
const settled = previous !== undefined && samePaths(previous, current);
|
|
1031
|
+
if (settled || Date.now() >= deadline) {
|
|
1032
|
+
const edit = await sendRename();
|
|
1033
|
+
if (!edit) throw notRenameable();
|
|
1034
|
+
const missing = [...current].filter((path) => !editFilePaths(edit).has(path));
|
|
1035
|
+
if (missing.length === 0) {
|
|
1036
|
+
return placeholder === undefined ? { edit } : { edit, placeholder };
|
|
1037
|
+
}
|
|
1038
|
+
throw new RenameIncompleteError(missing);
|
|
1039
|
+
}
|
|
1040
|
+
previous = current;
|
|
1041
|
+
await sleep(renameVerificationTiming.pollMs);
|
|
1042
|
+
current = toPaths(await referencesRequest());
|
|
1043
|
+
}
|
|
984
1044
|
},
|
|
985
1045
|
get diagnostics() {
|
|
986
1046
|
const result = new Map<string, Diagnostic[]>();
|
package/src/lib/lsp/rename.ts
CHANGED
|
@@ -159,6 +159,14 @@ export async function expandWorkspaceEdit(
|
|
|
159
159
|
return applied;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* rename edit 覆盖的文件路径集合(changes + documentChanges 的 text edits)。
|
|
164
|
+
* 供 renameSymbol 用 references 结果做覆盖校验。
|
|
165
|
+
*/
|
|
166
|
+
export function editFilePaths(edit: WorkspaceEdit): Set<string> {
|
|
167
|
+
return new Set(collectTextEdits(edit).keys());
|
|
168
|
+
}
|
|
169
|
+
|
|
162
170
|
/**
|
|
163
171
|
* 归一化 WorkspaceEdit 为稳定字符串(URI → 规范路径、结构化字段),供比较
|
|
164
172
|
* 两次 rename 的编辑集合是否一致(同一符号的多次出现 vs 不同符号)。
|