@trim21/personal-pi-extensions 0.1.575 → 0.1.577
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/README.md +3 -1
- package/package.json +1 -1
- package/src/lib/file-reads.ts +41 -7
- package/src/lib/lsp/client.ts +50 -5
- package/src/lib/lsp/lsp.ts +6 -0
- package/src/opencode/files.ts +13 -4
- package/src/skills/lsp-config/SKILL.md +3 -2
package/README.md
CHANGED
|
@@ -138,7 +138,9 @@ bwrap 已集成进 bash 工具实现(opencode 风格 `bash` 位于 `src/openco
|
|
|
138
138
|
|
|
139
139
|
所有匹配策略按顺序尝试,第一个匹配成功即返回。同时自动处理 BOM、CRLF/LF 行尾转换和文件写入队列。`filePath` 接受绝对路径或相对工作目录的路径。
|
|
140
140
|
|
|
141
|
-
edit 要求目标文件已被 `read` 读过且内容未变(内容指纹比对,与 Claude Code 风格 Edit 同一套语义):没读过报 `File has not been read yet. Read it first before
|
|
141
|
+
edit 要求目标文件已被 `read` 读过且内容未变(内容指纹比对,与 Claude Code 风格 Edit 同一套语义):没读过报 `File has not been read yet. Read it first before writing to it.`,读后文件被外部改动报 `File has been modified since read...`,两种情况都要重新 `read`。
|
|
142
|
+
|
|
143
|
+
`write` 不要求先读——没读过的文件可以直接写;但如果该文件已被读过、之后又被外部改动(或删除),`write` 会与 `edit` 一样要求重新 `read`,不让读取记录过期的内容被盲覆盖。`read`/`edit`/`write`/`lsp-rename` 都会刷新记账并随工具结果持久化,session 恢复 / fork / rewind 后依然有效;写完也会刷新,紧随其后的 `edit` 不必重新读。
|
|
142
144
|
|
|
143
145
|
### 使用
|
|
144
146
|
|
package/package.json
CHANGED
package/src/lib/file-reads.ts
CHANGED
|
@@ -45,6 +45,25 @@ export async function fileDigest(filePath: string): Promise<string> {
|
|
|
45
45
|
return hash.digest("hex");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** ENOENT / ENOTDIR:路径不存在。 */
|
|
49
|
+
function isMissingPath(error: unknown): boolean {
|
|
50
|
+
return (
|
|
51
|
+
error instanceof Error &&
|
|
52
|
+
"code" in error &&
|
|
53
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 磁盘上当前内容的指纹;文件已不存在时返回 undefined(视为「与读取时不同」)。 */
|
|
58
|
+
export async function digestIfExists(filePath: string): Promise<string | undefined> {
|
|
59
|
+
try {
|
|
60
|
+
return await fileDigest(filePath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (isMissingPath(error)) return undefined;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
/**
|
|
49
68
|
* 记账 key:解析 symlink 后的真实路径。文件尚不存在(Write 新建 / Edit 空
|
|
50
69
|
* old_string 创建)时 realpath 抛 ENOENT,回退到调用方给出的路径。
|
|
@@ -53,13 +72,7 @@ export async function readStateKey(filePath: string): Promise<string> {
|
|
|
53
72
|
try {
|
|
54
73
|
return await realpath(filePath);
|
|
55
74
|
} catch (error) {
|
|
56
|
-
if (
|
|
57
|
-
error instanceof Error &&
|
|
58
|
-
"code" in error &&
|
|
59
|
-
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
60
|
-
) {
|
|
61
|
-
return filePath;
|
|
62
|
-
}
|
|
75
|
+
if (isMissingPath(error)) return filePath;
|
|
63
76
|
throw error;
|
|
64
77
|
}
|
|
65
78
|
}
|
|
@@ -89,6 +102,27 @@ export function requireCurrentRead(
|
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 过期校验:该文件已有读取记录时,要求磁盘上的当前指纹与记录一致,否则拒绝
|
|
107
|
+
* 写入;从未读过(没有记录)时直接放行。
|
|
108
|
+
*
|
|
109
|
+
* opencode 的 write 用这个:没读过的文件允许直接写,读过之后再被外部改动就必须
|
|
110
|
+
* 重新 read。`currentDigest` 为 undefined(文件已不存在)同样算过期。
|
|
111
|
+
*/
|
|
112
|
+
export function requireUnchangedRead(
|
|
113
|
+
state: ReadsState,
|
|
114
|
+
key: string,
|
|
115
|
+
currentDigest: string | undefined,
|
|
116
|
+
): void {
|
|
117
|
+
const readSnapshot = state.reads.get(key);
|
|
118
|
+
if (!readSnapshot) return;
|
|
119
|
+
if (currentDigest === undefined || readSnapshot.digest !== currentDigest) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
92
126
|
/**
|
|
93
127
|
* 从工具结果 details 恢复已读记账(跨进程 resume / reload / fork)。数据来自
|
|
94
128
|
* session 文件,可能缺失或损坏:逐条 TypeBox 校验,非法条目丢弃。只接受
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -307,6 +307,16 @@ interface ServerCapabilities {
|
|
|
307
307
|
export const clientDefaults = {
|
|
308
308
|
diagnosticsDebounceMs: 150,
|
|
309
309
|
diagnosticsDocumentWaitTimeoutMs: 5_000,
|
|
310
|
+
/**
|
|
311
|
+
* document 模式等待上限的补充:该文档上一份诊断为空(服务器手里是干净文档)时,
|
|
312
|
+
* 内容变化后只用这段安静期等新 push,不再等满 diagnosticsDocumentWaitTimeoutMs。
|
|
313
|
+
*
|
|
314
|
+
* typescript-language-server 的 FileDiagnostics.update 在「该类诊断上一轮为空、
|
|
315
|
+
* 这一轮仍为空」时直接 return 不推送,且它不实现 pull 诊断;这类文档等满整个
|
|
316
|
+
* 窗口也只会在窗口末尾返回同样的「无诊断」,白等一次编辑。诊断集合变成非空时
|
|
317
|
+
* 服务器必定推送,所以短安静期不会漏掉这次变更引入的错误。
|
|
318
|
+
*/
|
|
319
|
+
diagnosticsSilentWaitTimeoutMs: 1_500,
|
|
310
320
|
diagnosticsFullWaitTimeoutMs: 10_000,
|
|
311
321
|
diagnosticsRequestTimeoutMs: 3_000,
|
|
312
322
|
initializeTimeoutMs: 45_000,
|
|
@@ -321,6 +331,8 @@ export interface CreateInput {
|
|
|
321
331
|
/** 可覆盖的超时参数(缺省用 client 默认值,由全局/本地 lsp.json 配置注入)。 */
|
|
322
332
|
diagnosticsDebounceMs?: number;
|
|
323
333
|
diagnosticsDocumentWaitTimeoutMs?: number;
|
|
334
|
+
/** 上一份诊断为空的文档的等待上限;缺省见 clientDefaults。 */
|
|
335
|
+
diagnosticsSilentWaitTimeoutMs?: number;
|
|
324
336
|
diagnosticsFullWaitTimeoutMs?: number;
|
|
325
337
|
diagnosticsRequestTimeoutMs?: number;
|
|
326
338
|
initializeTimeoutMs?: number;
|
|
@@ -522,6 +534,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
522
534
|
const diagnosticsDebounceMs = input.diagnosticsDebounceMs ?? clientDefaults.diagnosticsDebounceMs;
|
|
523
535
|
const diagnosticsDocumentWaitTimeoutMs =
|
|
524
536
|
input.diagnosticsDocumentWaitTimeoutMs ?? clientDefaults.diagnosticsDocumentWaitTimeoutMs;
|
|
537
|
+
const diagnosticsSilentWaitTimeoutMs =
|
|
538
|
+
input.diagnosticsSilentWaitTimeoutMs ?? clientDefaults.diagnosticsSilentWaitTimeoutMs;
|
|
525
539
|
const diagnosticsFullWaitTimeoutMs =
|
|
526
540
|
input.diagnosticsFullWaitTimeoutMs ?? clientDefaults.diagnosticsFullWaitTimeoutMs;
|
|
527
541
|
const diagnosticsRequestTimeoutMs =
|
|
@@ -599,6 +613,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
599
613
|
at: Date.now(),
|
|
600
614
|
version: typeof params.version === "number" ? params.version : undefined,
|
|
601
615
|
});
|
|
616
|
+
const document = files[filePath];
|
|
617
|
+
if (document !== undefined) document.lastPushEmpty = params.diagnostics.length === 0;
|
|
602
618
|
updatePushDiagnostics(filePath, params.diagnostics);
|
|
603
619
|
},
|
|
604
620
|
);
|
|
@@ -702,7 +718,15 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
702
718
|
await connection.sendNotification("workspace/didChangeConfiguration", { settings });
|
|
703
719
|
}
|
|
704
720
|
|
|
705
|
-
|
|
721
|
+
/**
|
|
722
|
+
* syncedAt:最后一次把该文档内容同步给服务器的时刻(didOpen / didChange)。
|
|
723
|
+
* lastPushEmpty:服务器对该文档最近一次 push 是否为空(无诊断),随驻留记录
|
|
724
|
+
* 一起在 didClose 时清除——只在当前驻留周期内成立。
|
|
725
|
+
*/
|
|
726
|
+
const files: Record<
|
|
727
|
+
string,
|
|
728
|
+
{ version: number; text: string; syncedAt: number; lastPushEmpty?: boolean } | undefined
|
|
729
|
+
> = {};
|
|
706
730
|
|
|
707
731
|
// ── 驻留 LRU ────────────────────────────────────────────────────────────────
|
|
708
732
|
|
|
@@ -1009,7 +1033,20 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1009
1033
|
after?: number;
|
|
1010
1034
|
signal?: AbortSignal;
|
|
1011
1035
|
}): Promise<void> {
|
|
1036
|
+
// 服务器对当前内容的最新结论已在手(最后一次 push 晚于最后一次内容同步,
|
|
1037
|
+
// 即此后没有再通知过内容变化):直接返回已有结果。部分服务器在诊断集合
|
|
1038
|
+
// 不变时不再推送(typescript-language-server 空→空不发布),等新 push 只会
|
|
1039
|
+
// 耗满整个窗口后得到同样的「无诊断」。
|
|
1040
|
+
const known = published.get(request.path);
|
|
1041
|
+
const document = files[request.path];
|
|
1042
|
+
if (known !== undefined && document !== undefined && known.at >= document.syncedAt) return;
|
|
1043
|
+
|
|
1012
1044
|
const startedAt = request.after ?? Date.now();
|
|
1045
|
+
// 「上一份 push 为空」的文档用短安静期(见 clientDefaults.diagnosticsSilentWaitTimeoutMs)。
|
|
1046
|
+
const budget =
|
|
1047
|
+
files[request.path]?.lastPushEmpty === true
|
|
1048
|
+
? Math.min(diagnosticsDocumentWaitTimeoutMs, diagnosticsSilentWaitTimeoutMs)
|
|
1049
|
+
: diagnosticsDocumentWaitTimeoutMs;
|
|
1013
1050
|
// pull 与 push 语义相同:都是等「当前文档版本」的诊断结果,统一一个循环。
|
|
1014
1051
|
// 先 pull(拿到即返回);pull 超时说明服务器未响应,不再重试 pull,只等
|
|
1015
1052
|
// 版本匹配的 push 兜底;版本不匹配的 push 一律忽略(防迟到旧结果)。
|
|
@@ -1017,11 +1054,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1017
1054
|
path: request.path,
|
|
1018
1055
|
version: request.version,
|
|
1019
1056
|
after: startedAt,
|
|
1020
|
-
timeout:
|
|
1057
|
+
timeout: budget,
|
|
1021
1058
|
});
|
|
1022
1059
|
|
|
1023
1060
|
while (!connectionClosed && !request.signal?.aborted) {
|
|
1024
|
-
const remaining =
|
|
1061
|
+
const remaining = budget - (Date.now() - startedAt);
|
|
1025
1062
|
if (remaining <= 0) return;
|
|
1026
1063
|
const result = await requestDocumentDiagnostics(request.path);
|
|
1027
1064
|
if (result.matched) return;
|
|
@@ -1088,13 +1125,21 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1088
1125
|
|
|
1089
1126
|
const document = files[resolvedPath];
|
|
1090
1127
|
if (document !== undefined) {
|
|
1128
|
+
// 内容与服务器已知文本一致:不重复通知(didChange 会把已有诊断判为过期,
|
|
1129
|
+
// 而部分服务器对内容未产生新结论的文档不再推送,见 diagnosticsSilentWaitTimeoutMs)。
|
|
1130
|
+
if (document.text === text) {
|
|
1131
|
+
touch(resolvedPath);
|
|
1132
|
+
return document.version;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1091
1135
|
// didChange:内容已变,旧诊断立即失效。清空缓存避免等待窗口内服务器
|
|
1092
1136
|
// 重算未完成时(大项目可远超窗口)聚合到过期诊断;新 push 到达即填充。
|
|
1093
1137
|
pushDiagnostics.delete(resolvedPath);
|
|
1094
1138
|
pullDiagnostics.delete(resolvedPath);
|
|
1095
1139
|
|
|
1096
1140
|
const next = document.version + 1;
|
|
1097
|
-
|
|
1141
|
+
// 保留 lastPushEmpty:安静期判据看的是「变更前服务器最后一份结论是否为空」
|
|
1142
|
+
files[resolvedPath] = { ...document, version: next, text, syncedAt: Date.now() };
|
|
1098
1143
|
documentVersions.set(resolvedPath, next);
|
|
1099
1144
|
await connection.sendNotification("textDocument/didChange", {
|
|
1100
1145
|
textDocument: { uri, version: next },
|
|
@@ -1118,7 +1163,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1118
1163
|
await connection.sendNotification("textDocument/didOpen", {
|
|
1119
1164
|
textDocument: { uri, languageId, version: 0, text },
|
|
1120
1165
|
});
|
|
1121
|
-
files[resolvedPath] = { version: 0, text };
|
|
1166
|
+
files[resolvedPath] = { version: 0, text, syncedAt: Date.now() };
|
|
1122
1167
|
documentVersions.set(resolvedPath, 0);
|
|
1123
1168
|
touch(resolvedPath);
|
|
1124
1169
|
await evictExcess();
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -86,6 +86,8 @@ const lspConfigSchema = Type.Object({
|
|
|
86
86
|
diagnosticsDebounceMs: Type.Optional(timeoutValue),
|
|
87
87
|
/** document 模式诊断等待上限(ms)。 */
|
|
88
88
|
diagnosticsDocumentWaitTimeoutMs: Type.Optional(timeoutValue),
|
|
89
|
+
/** 上一份诊断为空的文档的等待上限(ms);见 clientDefaults.diagnosticsSilentWaitTimeoutMs。 */
|
|
90
|
+
diagnosticsSilentWaitTimeoutMs: Type.Optional(timeoutValue),
|
|
89
91
|
/** full 模式诊断等待上限(ms)。 */
|
|
90
92
|
diagnosticsFullWaitTimeoutMs: Type.Optional(timeoutValue),
|
|
91
93
|
/** 单次 pull 诊断请求超时(ms)。 */
|
|
@@ -140,6 +142,7 @@ export interface ResolvedLspConfig {
|
|
|
140
142
|
/** 以下超时均为换算后的毫秒数(缺省见 configDefaults / clientDefaults)。 */
|
|
141
143
|
diagnosticsDebounceMs: number;
|
|
142
144
|
diagnosticsDocumentWaitTimeoutMs: number;
|
|
145
|
+
diagnosticsSilentWaitTimeoutMs: number;
|
|
143
146
|
diagnosticsFullWaitTimeoutMs: number;
|
|
144
147
|
diagnosticsRequestTimeoutMs: number;
|
|
145
148
|
initializeTimeoutMs: number;
|
|
@@ -198,6 +201,8 @@ export function resolveConfig(raw: LspConfig): ResolvedLspConfig {
|
|
|
198
201
|
diagnosticsDebounceMs: toMs(raw.diagnosticsDebounceMs) ?? clientDefaults.diagnosticsDebounceMs,
|
|
199
202
|
diagnosticsDocumentWaitTimeoutMs:
|
|
200
203
|
toMs(raw.diagnosticsDocumentWaitTimeoutMs) ?? clientDefaults.diagnosticsDocumentWaitTimeoutMs,
|
|
204
|
+
diagnosticsSilentWaitTimeoutMs:
|
|
205
|
+
toMs(raw.diagnosticsSilentWaitTimeoutMs) ?? clientDefaults.diagnosticsSilentWaitTimeoutMs,
|
|
201
206
|
diagnosticsFullWaitTimeoutMs:
|
|
202
207
|
toMs(raw.diagnosticsFullWaitTimeoutMs) ?? clientDefaults.diagnosticsFullWaitTimeoutMs,
|
|
203
208
|
diagnosticsRequestTimeoutMs:
|
|
@@ -791,6 +796,7 @@ export function createLspService(
|
|
|
791
796
|
diagnosticsDebounceMs: config.diagnosticsDebounceMs,
|
|
792
797
|
diagnosticsDocumentWaitTimeoutMs:
|
|
793
798
|
adapter.diagnosticsWaitMs ?? config.diagnosticsDocumentWaitTimeoutMs,
|
|
799
|
+
diagnosticsSilentWaitTimeoutMs: config.diagnosticsSilentWaitTimeoutMs,
|
|
794
800
|
diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
|
|
795
801
|
diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
|
|
796
802
|
initializeTimeoutMs: adapter.startupTimeoutMs ?? config.initializeTimeoutMs,
|
package/src/opencode/files.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* 不接 PDF、不接 <system-reminder>;图片 magic 检测保留。
|
|
12
12
|
* - edit:匹配引擎 + 把 old/new 转到文件换行后再替换;写后等待文档诊断。
|
|
13
13
|
* edit 要求文件已被 read 读过且内容未变(read 记账见下)。
|
|
14
|
-
* - write:BOM 保留(source.bom || next.bom);写后同 edit 的诊断输出。write
|
|
15
|
-
*
|
|
14
|
+
* - write:BOM 保留(source.bom || next.bom);写后同 edit 的诊断输出。write 不
|
|
15
|
+
* 要求先 read(未读过的文件可直接写),但若已有读取记录而磁盘内容已变,则要求
|
|
16
|
+
* 重新 read;写完刷新记账,保证紧接着的 edit 不必重新 read。
|
|
16
17
|
*
|
|
17
18
|
* read 记账:read / edit / write / lsp-rename 各自把受影响文件的内容指纹写进
|
|
18
19
|
* details.reads(随 session 持久化),session_start / session_tree 时从当前分支
|
|
@@ -37,11 +38,13 @@ import { Type } from "typebox";
|
|
|
37
38
|
|
|
38
39
|
import {
|
|
39
40
|
createReadsState,
|
|
41
|
+
digestIfExists,
|
|
40
42
|
fileDigest,
|
|
41
43
|
type FileSnapshot,
|
|
42
44
|
type ReadsState,
|
|
43
45
|
readStateKey,
|
|
44
46
|
requireCurrentRead,
|
|
47
|
+
requireUnchangedRead,
|
|
45
48
|
restoreReads,
|
|
46
49
|
snapshotOf,
|
|
47
50
|
} from "../lib/file-reads.js";
|
|
@@ -790,6 +793,13 @@ function registerWriteTool(
|
|
|
790
793
|
async () => {
|
|
791
794
|
signal?.throwIfAborted();
|
|
792
795
|
|
|
796
|
+
const key = await readStateKey(absolutePath);
|
|
797
|
+
// write 不要求先读过;但若已有读取记录,磁盘内容必须仍是读取时的样子,
|
|
798
|
+
// 否则先重新 read(外部改动过的文件不让盲写覆盖)
|
|
799
|
+
if (state.reads.has(key)) {
|
|
800
|
+
requireUnchangedRead(state, key, await digestIfExists(absolutePath));
|
|
801
|
+
}
|
|
802
|
+
|
|
793
803
|
// opencode: desiredBom = source.bom || next.bom —— 保留原文件 BOM,
|
|
794
804
|
// 否则用新内容自带的 BOM
|
|
795
805
|
let existing: Buffer | undefined;
|
|
@@ -810,8 +820,7 @@ function registerWriteTool(
|
|
|
810
820
|
await mkdir(dir, { recursive: true });
|
|
811
821
|
signal?.throwIfAborted();
|
|
812
822
|
await writeFile(absolutePath, desiredBom + nextText, "utf8");
|
|
813
|
-
//
|
|
814
|
-
const key = await readStateKey(absolutePath);
|
|
823
|
+
// 写后记账(key 在写入前已算过):紧接着的 edit 不该再要求重新 read
|
|
815
824
|
const snapshot = snapshotOf(desiredBom + nextText);
|
|
816
825
|
state.reads.set(key, snapshot);
|
|
817
826
|
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
@@ -27,12 +27,13 @@ description: Use when 编写、检查或排查 LSP 语言服务器配置 ——
|
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
version / servers / enabled / disabled / watch / maxOpenDocuments /
|
|
30
|
-
diagnosticsDebounceMs / diagnosticsDocumentWaitTimeoutMs /
|
|
30
|
+
diagnosticsDebounceMs / diagnosticsDocumentWaitTimeoutMs / diagnosticsSilentWaitTimeoutMs /
|
|
31
31
|
diagnosticsFullWaitTimeoutMs / diagnosticsRequestTimeoutMs / initializeTimeoutMs
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
- `enabled`:只启用列出的服务器 id(缺省 = 全部启用);`disabled`:从启用集中排除
|
|
35
|
-
- 时长字段:毫秒数字,或带单位的字符串(`"300ms"` / `"5s"` / `"1m"` / `"2h"`,空单位按 ms);默认值见 `clientDefaults`(client.ts):debounce 150ms、document 等待 5s、full 等待 10s、pull 请求 3s、initialize 45s、`maxOpenDocuments` 32
|
|
35
|
+
- 时长字段:毫秒数字,或带单位的字符串(`"300ms"` / `"5s"` / `"1m"` / `"2h"`,空单位按 ms);默认值见 `clientDefaults`(client.ts):debounce 150ms、document 等待 5s、安静期 1.5s、full 等待 10s、pull 请求 3s、initialize 45s、`maxOpenDocuments` 32
|
|
36
|
+
- `diagnosticsSilentWaitTimeoutMs`:只用于「该文档上一份诊断为空」的情形——内容变化后最多只等这段安静期,不再等满 `diagnosticsDocumentWaitTimeoutMs`。typescript-language-server 在诊断集合空→空时不重复推送且不实现 pull 诊断,这类文档等满整个窗口也只会得到同样的「无诊断」,白等一次编辑;集合变成非空时服务器必定推送,所以调小它不会漏掉变更引入的错误,调大则更保守(默认 1.5s,约等于实测热态推送延迟的三倍)。设为不小于 `diagnosticsDocumentWaitTimeoutMs` 即等价于不做这个缩短
|
|
36
37
|
- `watch`:`enabled` / `debounceMs`(缺省 300)/ `maxBatch`(缺省 500)/ `ignore`(glob,相对**每个被监听的项目根**的 POSIX 路径)。注意 `flushMs` 只在默认值里、**不可配**
|
|
37
38
|
- 监听范围不是整个 cwd,而是**当前活跃服务器 client 的项目根**:root 在 cwd 内就只监听 root(被其他 root 包含的 root 不重复监听,root 是 cwd 的祖先时退化为 cwd);没有活跃 client 就不监听。因此容器 cwd(`~/projects` 下多个仓库)里只有活跃服务器所在的项目会被 watch;`ignore` 的匹配基准也随之是各自的项目根。资源耗尽(ENOSPC)时该 root 的监听器会停止并提示一次,调大 `fs.inotify.max_user_watches` 后跑 `/lsp-reload` 重试
|
|
38
39
|
|