opencode-agent-skill 14.4.0 → 15.1.0
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 +34 -16
- package/docs/PI-COMPAT.md +151 -2
- package/docs/V15-MANAGED-RUNTIME.md +128 -0
- package/global-config/agents/architect.md +63 -1
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/plan-checker.md +8 -1
- package/global-config/agents/verifier.md +3 -3
- package/global-config/skills/database-engineering/references/workflow.md +15 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
- package/lib/activity-deadline.mjs +70 -0
- package/lib/adaptive-context-budget.mjs +25 -5
- package/lib/affected-tests.mjs +38 -5
- package/lib/capability-fabric.mjs +17 -0
- package/lib/code-intelligence/index.mjs +3 -2
- package/lib/code-intelligence/lsp-provider.mjs +326 -52
- package/lib/compaction-resume-guard.mjs +255 -0
- package/lib/completion-auditor.mjs +25 -5
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/evidence-store.mjs +92 -8
- package/lib/execution-contract.mjs +592 -0
- package/lib/leaf-runtime-optimizer.mjs +76 -0
- package/lib/memory-engine.mjs +189 -16
- package/lib/performance-fabric.mjs +262 -107
- package/lib/pi-child-invocation.mjs +65 -0
- package/lib/pi-rpc-pool.mjs +61 -9
- package/lib/plan-salvage.mjs +124 -0
- package/lib/planning-speed-policy.mjs +131 -0
- package/lib/repo-graph.mjs +1 -1
- package/lib/repo-inspect.mjs +1 -0
- package/lib/runtime-artifacts.mjs +42 -0
- package/lib/runtime-events.mjs +33 -1
- package/lib/safety.mjs +1 -0
- package/lib/semantic-index.mjs +1 -1
- package/lib/service-manager.mjs +526 -0
- package/lib/session-display.mjs +49 -0
- package/lib/subagent-artifacts.mjs +182 -0
- package/lib/task-graph.mjs +87 -0
- package/lib/task-policy.mjs +150 -4
- package/lib/trajectory.mjs +111 -1
- package/lib/turbo-fast-path.mjs +44 -0
- package/lib/verification-broker.mjs +32 -27
- package/lib/workspace-fingerprint.mjs +45 -19
- package/lib/workspace-hygiene.mjs +481 -0
- package/lib/workspace-root.mjs +49 -0
- package/lib/worktree-sandbox.mjs +234 -11
- package/package.json +8 -5
- package/pi/extensions/ues-child-runtime.ts +170 -4
- package/pi/extensions/ues.ts +2063 -188
- package/scripts/check-release-consistency.mjs +25 -2
- package/scripts/check-source-integrity.mjs +471 -4
- package/scripts/eval-pi.mjs +76 -36
- package/scripts/smoke-pi-extension.mjs +1 -1
- package/pi/prompts/ues-run.md +0 -17
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process"
|
|
2
2
|
import { lstat, readFile, realpath } from "node:fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
|
-
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
5
|
|
|
6
6
|
const PROVIDERS = [
|
|
7
7
|
{ id: "typescript-language-server", extensions: new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]), commands: [["typescript-language-server", ["--stdio"]]], languageId: (ext) => [".ts", ".tsx"].includes(ext) ? "typescript" : "javascript" },
|
|
@@ -11,6 +11,17 @@ const PROVIDERS = [
|
|
|
11
11
|
{ id: "clangd", extensions: new Set([".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"]), commands: [["clangd", []]], languageId: (ext) => [".c", ".h"].includes(ext) ? "c" : "cpp" },
|
|
12
12
|
]
|
|
13
13
|
|
|
14
|
+
export const LSP_OPERATIONS = Object.freeze([
|
|
15
|
+
"diagnostics",
|
|
16
|
+
"definition",
|
|
17
|
+
"references",
|
|
18
|
+
"symbols",
|
|
19
|
+
"hover",
|
|
20
|
+
"rename-preview",
|
|
21
|
+
"incoming-calls",
|
|
22
|
+
"outgoing-calls",
|
|
23
|
+
])
|
|
24
|
+
|
|
14
25
|
const commandCache = new Map()
|
|
15
26
|
const COMMAND_TTL_MS = 60_000
|
|
16
27
|
|
|
@@ -31,11 +42,11 @@ function inside(root, target) {
|
|
|
31
42
|
async function safeFile(root, relative) {
|
|
32
43
|
const base = await realpath(path.resolve(root)).catch(() => path.resolve(root))
|
|
33
44
|
const requested = path.resolve(base, String(relative || ""))
|
|
34
|
-
if (!inside(base, requested)) throw new Error("
|
|
45
|
+
if (!inside(base, requested)) throw new Error("LSP path escapes workspace root")
|
|
35
46
|
const info = await lstat(requested)
|
|
36
|
-
if (!info.isFile()) throw new Error("
|
|
47
|
+
if (!info.isFile()) throw new Error("LSP path is not a file")
|
|
37
48
|
const actual = await realpath(requested)
|
|
38
|
-
if (!inside(base, actual)) throw new Error("
|
|
49
|
+
if (!inside(base, actual)) throw new Error("LSP symlink escapes workspace root")
|
|
39
50
|
return { base, file: actual, info }
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -59,7 +70,13 @@ export function lspProviderStatus(file = "") {
|
|
|
59
70
|
const selected = provider.commands.find(([command]) => commandExists(command)) || null
|
|
60
71
|
return { id: provider.id, available: Boolean(selected), command: selected?.[0] || null, extensions: [...provider.extensions] }
|
|
61
72
|
})
|
|
62
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: 2,
|
|
75
|
+
extension: ext,
|
|
76
|
+
available: providers.some((row) => row.available),
|
|
77
|
+
operations: [...LSP_OPERATIONS],
|
|
78
|
+
providers,
|
|
79
|
+
}
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
function encodeMessage(payload) {
|
|
@@ -67,6 +84,23 @@ function encodeMessage(payload) {
|
|
|
67
84
|
return Buffer.concat([Buffer.from("Content-Length: " + body.length + "\r\n\r\n", "ascii"), body])
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
function relativeUri(base, uri) {
|
|
88
|
+
try {
|
|
89
|
+
if (!String(uri || "").startsWith("file:")) return String(uri || "")
|
|
90
|
+
const file = fileURLToPath(uri)
|
|
91
|
+
if (!inside(base, file)) return String(uri || "")
|
|
92
|
+
return path.relative(base, file).replaceAll("\\", "/")
|
|
93
|
+
} catch {
|
|
94
|
+
return String(uri || "")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function position(options = {}) {
|
|
99
|
+
const line = Math.max(1, Math.trunc(Number(options.line || 1)))
|
|
100
|
+
const character = Math.max(1, Math.trunc(Number(options.character || 1)))
|
|
101
|
+
return { line: line - 1, character: character - 1 }
|
|
102
|
+
}
|
|
103
|
+
|
|
70
104
|
function sanitizeDiagnostic(value = {}) {
|
|
71
105
|
return {
|
|
72
106
|
range: value.range || null,
|
|
@@ -77,73 +111,214 @@ function sanitizeDiagnostic(value = {}) {
|
|
|
77
111
|
}
|
|
78
112
|
}
|
|
79
113
|
|
|
80
|
-
|
|
114
|
+
function sanitizeLocations(base, value, limit = 120) {
|
|
115
|
+
const list = Array.isArray(value) ? value : value ? [value] : []
|
|
116
|
+
return list.slice(0, limit).map((item) => {
|
|
117
|
+
if (item?.targetUri) {
|
|
118
|
+
return {
|
|
119
|
+
file: relativeUri(base, item.targetUri),
|
|
120
|
+
range: item.targetRange || null,
|
|
121
|
+
selectionRange: item.targetSelectionRange || null,
|
|
122
|
+
originSelectionRange: item.originSelectionRange || null,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
file: relativeUri(base, item?.uri),
|
|
127
|
+
range: item?.range || null,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function flattenSymbols(base, values, output, parent = null, limit = 160) {
|
|
133
|
+
for (const item of Array.isArray(values) ? values : []) {
|
|
134
|
+
if (output.length >= limit) break
|
|
135
|
+
output.push({
|
|
136
|
+
name: String(item?.name || "").slice(0, 300),
|
|
137
|
+
detail: item?.detail == null ? null : String(item.detail).slice(0, 500),
|
|
138
|
+
kind: item?.kind ?? null,
|
|
139
|
+
containerName: item?.containerName || parent,
|
|
140
|
+
file: item?.location?.uri ? relativeUri(base, item.location.uri) : null,
|
|
141
|
+
range: item?.range || item?.location?.range || null,
|
|
142
|
+
selectionRange: item?.selectionRange || null,
|
|
143
|
+
})
|
|
144
|
+
if (Array.isArray(item?.children)) flattenSymbols(base, item.children, output, item?.name || parent, limit)
|
|
145
|
+
}
|
|
146
|
+
return output
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hoverText(contents) {
|
|
150
|
+
if (contents == null) return ""
|
|
151
|
+
if (typeof contents === "string") return contents
|
|
152
|
+
if (Array.isArray(contents)) {
|
|
153
|
+
return contents.map((item) => typeof item === "string" ? item : item?.value || "").filter(Boolean).join("\n")
|
|
154
|
+
}
|
|
155
|
+
if (typeof contents === "object") return String(contents.value || "")
|
|
156
|
+
return String(contents)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sanitizeWorkspaceEdit(base, edit, limit = 240) {
|
|
160
|
+
const rows = []
|
|
161
|
+
for (const [uri, edits] of Object.entries(edit?.changes || {})) {
|
|
162
|
+
for (const item of Array.isArray(edits) ? edits : []) {
|
|
163
|
+
rows.push({ file: relativeUri(base, uri), range: item.range || null, newText: String(item.newText || "").slice(0, 4000) })
|
|
164
|
+
if (rows.length >= limit) return { edits: rows, truncated: true }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const change of Array.isArray(edit?.documentChanges) ? edit.documentChanges : []) {
|
|
168
|
+
const uri = change?.textDocument?.uri
|
|
169
|
+
for (const item of Array.isArray(change?.edits) ? change.edits : []) {
|
|
170
|
+
rows.push({ file: relativeUri(base, uri), range: item.range || null, newText: String(item.newText || "").slice(0, 4000) })
|
|
171
|
+
if (rows.length >= limit) return { edits: rows, truncated: true }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { edits: rows, truncated: false }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function sanitizeHierarchyItem(base, item) {
|
|
178
|
+
if (!item) return null
|
|
179
|
+
return {
|
|
180
|
+
name: String(item.name || "").slice(0, 300),
|
|
181
|
+
detail: item.detail == null ? null : String(item.detail).slice(0, 500),
|
|
182
|
+
kind: item.kind ?? null,
|
|
183
|
+
file: relativeUri(base, item.uri),
|
|
184
|
+
range: item.range || null,
|
|
185
|
+
selectionRange: item.selectionRange || null,
|
|
186
|
+
_raw: item,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sanitizeHierarchyCalls(base, values, direction, limit = 120) {
|
|
191
|
+
return (Array.isArray(values) ? values : []).slice(0, limit).map((call) => {
|
|
192
|
+
const item = direction === "incoming" ? call.from : call.to
|
|
193
|
+
const clean = sanitizeHierarchyItem(base, item)
|
|
194
|
+
if (clean) delete clean._raw
|
|
195
|
+
return {
|
|
196
|
+
item: clean,
|
|
197
|
+
ranges: direction === "incoming" ? (call.fromRanges || []) : (call.fromRanges || []),
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function withLspSession(root, relative, options, operation) {
|
|
81
203
|
const target = await safeFile(root, relative)
|
|
82
204
|
const provider = providerFor(target.file)
|
|
83
205
|
const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
|
|
84
206
|
if (!provider?.command) {
|
|
85
|
-
return {
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: 2,
|
|
209
|
+
file: relativeFile,
|
|
210
|
+
available: false,
|
|
211
|
+
provider: provider?.id || null,
|
|
212
|
+
operation: options.operation || null,
|
|
213
|
+
reason: provider ? "lsp-command-unavailable" : "unsupported-extension",
|
|
214
|
+
result: null,
|
|
215
|
+
}
|
|
86
216
|
}
|
|
87
217
|
|
|
88
218
|
const maxBytes = Math.max(8 * 1024, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 1024 * 1024)))
|
|
89
|
-
if (target.info.size > maxBytes) throw new Error("
|
|
219
|
+
if (target.info.size > maxBytes) throw new Error("LSP file exceeds limit (" + target.info.size + " > " + maxBytes + " bytes)")
|
|
90
220
|
const source = await readFile(target.file, "utf8")
|
|
91
221
|
const uri = pathToFileURL(target.file).href
|
|
92
222
|
const rootUri = pathToFileURL(target.base + path.sep).href
|
|
93
|
-
const timeoutMs = Math.max(500, Math.min(
|
|
94
|
-
const maxDiagnostics = Math.max(1, Math.min(500, Number(options.maxDiagnostics || 100)))
|
|
223
|
+
const timeoutMs = Math.max(500, Math.min(15_000, Number(options.timeoutMs || 5000)))
|
|
95
224
|
|
|
96
225
|
return await new Promise((resolve) => {
|
|
97
|
-
const child = spawn(provider.command, provider.args, {
|
|
226
|
+
const child = spawn(provider.command, provider.args, {
|
|
227
|
+
cwd: target.base,
|
|
228
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
229
|
+
windowsHide: true,
|
|
230
|
+
shell: false,
|
|
231
|
+
})
|
|
98
232
|
let buffer = Buffer.alloc(0)
|
|
99
233
|
let stderr = ""
|
|
100
234
|
let settled = false
|
|
101
|
-
let
|
|
102
|
-
|
|
235
|
+
let nextId = 1
|
|
236
|
+
const pending = new Map()
|
|
237
|
+
const notificationWaiters = new Map()
|
|
238
|
+
const recentNotifications = new Map()
|
|
103
239
|
let timer = null
|
|
104
240
|
|
|
105
|
-
const finish = (reason, error = null) => {
|
|
241
|
+
const finish = (reason, result = null, error = null) => {
|
|
106
242
|
if (settled) return
|
|
107
243
|
settled = true
|
|
108
244
|
if (timer) clearTimeout(timer)
|
|
245
|
+
for (const waiter of pending.values()) waiter.reject(new Error("LSP session settled: " + reason))
|
|
246
|
+
pending.clear()
|
|
109
247
|
try {
|
|
110
|
-
if (
|
|
111
|
-
child.stdin.write(encodeMessage({ jsonrpc: "2.0", id: 2, method: "shutdown", params: null }))
|
|
112
|
-
child.stdin.write(encodeMessage({ jsonrpc: "2.0", method: "exit", params: null }))
|
|
113
|
-
}
|
|
248
|
+
if (child.stdin.writable) child.stdin.write(encodeMessage({ jsonrpc: "2.0", method: "exit", params: null }))
|
|
114
249
|
} catch {}
|
|
115
250
|
setTimeout(() => { try { child.kill() } catch {} }, 50).unref?.()
|
|
116
251
|
resolve({
|
|
117
|
-
schemaVersion:
|
|
252
|
+
schemaVersion: 2,
|
|
118
253
|
file: relativeFile,
|
|
119
254
|
available: true,
|
|
120
255
|
provider: provider.id,
|
|
121
256
|
command: provider.command,
|
|
122
|
-
|
|
257
|
+
operation: options.operation || null,
|
|
123
258
|
reason,
|
|
259
|
+
result,
|
|
124
260
|
stderr: stderr.trim().slice(0, 2000) || null,
|
|
125
|
-
error: error ? String(error instanceof Error ? error.message : error).slice(0,
|
|
261
|
+
error: error ? String(error instanceof Error ? error.message : error).slice(0, 1200) : null,
|
|
126
262
|
})
|
|
127
263
|
}
|
|
128
264
|
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
265
|
+
const send = (payload) => {
|
|
266
|
+
if (settled || !child.stdin.writable) throw new Error("LSP stdin unavailable")
|
|
267
|
+
child.stdin.write(encodeMessage(payload))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const request = (method, params) => new Promise((res, rej) => {
|
|
271
|
+
const id = nextId++
|
|
272
|
+
pending.set(id, { resolve: res, reject: rej, method })
|
|
273
|
+
try { send({ jsonrpc: "2.0", id, method, params }) }
|
|
274
|
+
catch (error) { pending.delete(id); rej(error) }
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const notify = (method, params) => send({ jsonrpc: "2.0", method, params })
|
|
278
|
+
|
|
279
|
+
const waitForNotification = (method) => {
|
|
280
|
+
if (recentNotifications.has(method)) {
|
|
281
|
+
const value = recentNotifications.get(method)
|
|
282
|
+
recentNotifications.delete(method)
|
|
283
|
+
return Promise.resolve(value)
|
|
284
|
+
}
|
|
285
|
+
return new Promise((res) => {
|
|
286
|
+
const list = notificationWaiters.get(method) || []
|
|
287
|
+
list.push(res)
|
|
288
|
+
notificationWaiters.set(method, list)
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const dispatch = (message) => {
|
|
293
|
+
if (message?.id != null && pending.has(message.id)) {
|
|
294
|
+
const waiter = pending.get(message.id)
|
|
295
|
+
pending.delete(message.id)
|
|
296
|
+
if (message.error) waiter.reject(new Error(String(message.error.message || "LSP request failed")))
|
|
297
|
+
else waiter.resolve(message.result)
|
|
142
298
|
return
|
|
143
299
|
}
|
|
144
|
-
if (message?.method
|
|
145
|
-
|
|
146
|
-
|
|
300
|
+
if (message?.method) {
|
|
301
|
+
// Language servers may issue configuration/capability requests. Reply
|
|
302
|
+
// deterministically so the server does not stall waiting on a client UI.
|
|
303
|
+
if (message.id != null) {
|
|
304
|
+
try {
|
|
305
|
+
const result = message.method === "workspace/configuration"
|
|
306
|
+
? (Array.isArray(message.params?.items) ? message.params.items.map(() => null) : [])
|
|
307
|
+
: null
|
|
308
|
+
send({ jsonrpc: "2.0", id: message.id, result })
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
const waiters = notificationWaiters.get(message.method) || []
|
|
312
|
+
if (waiters.length) {
|
|
313
|
+
notificationWaiters.delete(message.method)
|
|
314
|
+
for (const waiter of waiters) waiter(message.params)
|
|
315
|
+
} else {
|
|
316
|
+
recentNotifications.set(message.method, message.params)
|
|
317
|
+
if (recentNotifications.size > 16) {
|
|
318
|
+
const oldest = recentNotifications.keys().next().value
|
|
319
|
+
if (oldest) recentNotifications.delete(oldest)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
147
322
|
}
|
|
148
323
|
}
|
|
149
324
|
|
|
@@ -159,31 +334,130 @@ export async function diagnoseCode(root, relative, options = {}) {
|
|
|
159
334
|
if (buffer.length < bodyStart + length) return
|
|
160
335
|
const body = buffer.subarray(bodyStart, bodyStart + length).toString("utf8")
|
|
161
336
|
buffer = buffer.subarray(bodyStart + length)
|
|
162
|
-
try {
|
|
337
|
+
try { dispatch(JSON.parse(body)) } catch {}
|
|
163
338
|
}
|
|
164
339
|
}
|
|
165
340
|
|
|
166
341
|
child.stdout.on("data", (chunk) => { buffer = Buffer.concat([buffer, Buffer.from(chunk)]); parse() })
|
|
167
342
|
child.stderr.on("data", (chunk) => { if (stderr.length < 64 * 1024) stderr += chunk.toString() })
|
|
168
|
-
child.on("error", (error) => finish("spawn-error", error))
|
|
343
|
+
child.on("error", (error) => finish("spawn-error", null, error))
|
|
169
344
|
child.on("close", (code) => { if (!settled) finish("server-exit:" + (code ?? "unknown")) })
|
|
170
345
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
346
|
+
timer = setTimeout(() => finish("timeout"), timeoutMs)
|
|
347
|
+
timer.unref?.()
|
|
348
|
+
|
|
349
|
+
;(async () => {
|
|
350
|
+
try {
|
|
351
|
+
await request("initialize", {
|
|
177
352
|
processId: process.pid,
|
|
178
353
|
rootUri,
|
|
179
|
-
capabilities: {
|
|
354
|
+
capabilities: {
|
|
355
|
+
textDocument: {
|
|
356
|
+
publishDiagnostics: { relatedInformation: true },
|
|
357
|
+
definition: { linkSupport: true },
|
|
358
|
+
references: {},
|
|
359
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
360
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
361
|
+
rename: { prepareSupport: true },
|
|
362
|
+
callHierarchy: {},
|
|
363
|
+
},
|
|
364
|
+
workspace: { workspaceFolders: true, applyEdit: false },
|
|
365
|
+
},
|
|
180
366
|
workspaceFolders: [{ uri: rootUri, name: path.basename(target.base) || "workspace" }],
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
367
|
+
})
|
|
368
|
+
notify("initialized", {})
|
|
369
|
+
notify("textDocument/didOpen", {
|
|
370
|
+
textDocument: { uri, languageId: provider.languageId, version: 1, text: source },
|
|
371
|
+
})
|
|
372
|
+
const result = await operation({
|
|
373
|
+
request,
|
|
374
|
+
notify,
|
|
375
|
+
waitForNotification,
|
|
376
|
+
uri,
|
|
377
|
+
base: target.base,
|
|
378
|
+
file: relativeFile,
|
|
379
|
+
})
|
|
380
|
+
finish("ok", result)
|
|
381
|
+
} catch (error) {
|
|
382
|
+
finish("request-error", null, error)
|
|
383
|
+
}
|
|
384
|
+
})()
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function lspOperation(root, relative, operation, options = {}) {
|
|
389
|
+
const op = String(operation || "")
|
|
390
|
+
if (!LSP_OPERATIONS.includes(op)) throw new Error("unsupported LSP operation: " + op)
|
|
391
|
+
const maxResults = Math.max(1, Math.min(500, Number(options.maxResults || 120)))
|
|
392
|
+
return withLspSession(root, relative, { ...options, operation: op }, async (session) => {
|
|
393
|
+
const pos = position(options)
|
|
394
|
+
const textDocument = { uri: session.uri }
|
|
395
|
+
|
|
396
|
+
if (op === "diagnostics") {
|
|
397
|
+
const params = await session.waitForNotification("textDocument/publishDiagnostics")
|
|
398
|
+
const values = Array.isArray(params?.diagnostics) ? params.diagnostics : []
|
|
399
|
+
return { diagnostics: values.slice(0, maxResults).map(sanitizeDiagnostic) }
|
|
185
400
|
}
|
|
186
|
-
|
|
187
|
-
|
|
401
|
+
if (op === "definition") {
|
|
402
|
+
const result = await session.request("textDocument/definition", { textDocument, position: pos })
|
|
403
|
+
return { locations: sanitizeLocations(session.base, result, maxResults) }
|
|
404
|
+
}
|
|
405
|
+
if (op === "references") {
|
|
406
|
+
const result = await session.request("textDocument/references", {
|
|
407
|
+
textDocument,
|
|
408
|
+
position: pos,
|
|
409
|
+
context: { includeDeclaration: options.includeDeclaration !== false },
|
|
410
|
+
})
|
|
411
|
+
return { locations: sanitizeLocations(session.base, result, maxResults) }
|
|
412
|
+
}
|
|
413
|
+
if (op === "symbols") {
|
|
414
|
+
const result = await session.request("textDocument/documentSymbol", { textDocument })
|
|
415
|
+
return { symbols: flattenSymbols(session.base, result, [], null, maxResults) }
|
|
416
|
+
}
|
|
417
|
+
if (op === "hover") {
|
|
418
|
+
const result = await session.request("textDocument/hover", { textDocument, position: pos })
|
|
419
|
+
return {
|
|
420
|
+
range: result?.range || null,
|
|
421
|
+
markdown: hoverText(result?.contents).slice(0, Number(options.maxHoverChars || 12000)),
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (op === "rename-preview") {
|
|
425
|
+
const newName = String(options.newName || "").trim()
|
|
426
|
+
if (!newName) throw new Error("rename-preview requires newName")
|
|
427
|
+
let prepared = null
|
|
428
|
+
try {
|
|
429
|
+
prepared = await session.request("textDocument/prepareRename", { textDocument, position: pos })
|
|
430
|
+
} catch {}
|
|
431
|
+
const result = await session.request("textDocument/rename", { textDocument, position: pos, newName })
|
|
432
|
+
return {
|
|
433
|
+
prepared: prepared || null,
|
|
434
|
+
newName,
|
|
435
|
+
...sanitizeWorkspaceEdit(session.base, result, maxResults),
|
|
436
|
+
applied: false,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (op === "incoming-calls" || op === "outgoing-calls") {
|
|
440
|
+
const prepared = await session.request("textDocument/prepareCallHierarchy", { textDocument, position: pos })
|
|
441
|
+
const item = Array.isArray(prepared) ? prepared[0] : prepared
|
|
442
|
+
if (!item) return { root: null, calls: [] }
|
|
443
|
+
const method = op === "incoming-calls" ? "callHierarchy/incomingCalls" : "callHierarchy/outgoingCalls"
|
|
444
|
+
const result = await session.request(method, { item })
|
|
445
|
+
const rootItem = sanitizeHierarchyItem(session.base, item)
|
|
446
|
+
if (rootItem) delete rootItem._raw
|
|
447
|
+
return {
|
|
448
|
+
root: rootItem,
|
|
449
|
+
calls: sanitizeHierarchyCalls(session.base, result, op === "incoming-calls" ? "incoming" : "outgoing", maxResults),
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return null
|
|
188
453
|
})
|
|
189
454
|
}
|
|
455
|
+
|
|
456
|
+
export async function diagnoseCode(root, relative, options = {}) {
|
|
457
|
+
const result = await lspOperation(root, relative, "diagnostics", options)
|
|
458
|
+
return {
|
|
459
|
+
...result,
|
|
460
|
+
diagnostics: result?.result?.diagnostics || [],
|
|
461
|
+
result: undefined,
|
|
462
|
+
}
|
|
463
|
+
}
|