@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12
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/CHANGELOG.md +53 -0
- package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
- package/dist/cli.js +10775 -10762
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +9 -52
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +33 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +46 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/components/agent-hub.d.ts +2 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +26 -2
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +8 -0
- package/dist/types/session/turn-recovery.d.ts +9 -4
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +18 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/advisor/runtime.ts +12 -7
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/command-help.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/share.ts +71 -0
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +84 -704
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +53 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +75 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/export/share.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +78 -14
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/main.ts +3 -2
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +15 -4
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/components/agent-hub.ts +5 -0
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +6 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +8 -2
- package/src/modes/controllers/event-controller.ts +15 -15
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +51 -6
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +146 -21
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-context.ts +3 -5
- package/src/session/session-handoff.ts +39 -16
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/session-tools.ts +37 -3
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +115 -84
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -2996
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/task/index.ts +21 -6
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/debug.ts +1 -1
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +82 -1824
- package/src/tools/shell-tokenize.ts +99 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/zai.ts +12 -3
|
@@ -27,6 +27,11 @@ export interface Skill {
|
|
|
27
27
|
* prompt's `<skills>` listing.
|
|
28
28
|
*/
|
|
29
29
|
hide?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Filesystem-resolved plugin root for Agent Plugin skills (spec §4.1):
|
|
32
|
+
* every `skill://` resource access must realpath-resolve within it.
|
|
33
|
+
*/
|
|
34
|
+
containRoot?: string;
|
|
30
35
|
/** Source metadata for display */
|
|
31
36
|
_source?: SourceMeta;
|
|
32
37
|
}
|
|
@@ -104,6 +109,7 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
104
109
|
filePath: capSkill.path,
|
|
105
110
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
106
111
|
source: options.source,
|
|
112
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
107
113
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
108
114
|
_source: capSkill._source,
|
|
109
115
|
})),
|
|
@@ -239,6 +245,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
239
245
|
filePath: capSkill.path,
|
|
240
246
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
241
247
|
source: `${capSkill._source.provider}:${capSkill.level}`,
|
|
248
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
242
249
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
243
250
|
_source: capSkill._source,
|
|
244
251
|
});
|
|
@@ -276,6 +283,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
276
283
|
filePath: capSkill.path,
|
|
277
284
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
278
285
|
source: "custom:user",
|
|
286
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
279
287
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
280
288
|
_source: { ...capSkill._source, providerName: "Custom" },
|
|
281
289
|
},
|
|
@@ -376,6 +384,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
376
384
|
filePath: capSkill.path,
|
|
377
385
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
378
386
|
source: `${capSkill._source.provider}:${capSkill.level}`,
|
|
387
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
379
388
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
380
389
|
_source: capSkill._source,
|
|
381
390
|
});
|
|
@@ -11,6 +11,7 @@ import type * as fsTypes from "node:fs";
|
|
|
11
11
|
import * as fs from "node:fs/promises";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { resolveContainedPath } from "../discovery/contained-path";
|
|
14
15
|
import { getActiveSkills } from "../extensibility/skills";
|
|
15
16
|
import { isMarkdownPath } from "../utils/lang-from-path";
|
|
16
17
|
import { buildDirectoryResource } from "./filesystem-resource";
|
|
@@ -76,6 +77,19 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
76
77
|
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
|
|
77
78
|
throw new Error("Path traversal is not allowed");
|
|
78
79
|
}
|
|
80
|
+
// Agent Plugin skills (§4.1): the resource must canonically resolve
|
|
81
|
+
// within the plugin root; a dangling or unresolvable path fails closed.
|
|
82
|
+
// Symlinks may target other files inside the same package.
|
|
83
|
+
if (skill.containRoot) {
|
|
84
|
+
const contained = await resolveContainedPath(skill.containRoot, resolvedPath);
|
|
85
|
+
if (contained.status === "outside") {
|
|
86
|
+
throw new Error(`skill:// path resolves outside the plugin root: ${url.href}`);
|
|
87
|
+
}
|
|
88
|
+
if (contained.status === "missing") {
|
|
89
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
90
|
+
}
|
|
91
|
+
targetPath = contained.realPath;
|
|
92
|
+
}
|
|
79
93
|
} else {
|
|
80
94
|
targetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;
|
|
81
95
|
}
|
|
@@ -787,7 +787,7 @@ export class VaultProtocolHandler implements ProtocolHandler {
|
|
|
787
787
|
const cacheKey = parsed.ref.active ? "_" : (parsed.ref.vault ?? "_");
|
|
788
788
|
let cliInfo = cachedVaultInfo.get(cacheKey);
|
|
789
789
|
if (cliInfo === undefined) {
|
|
790
|
-
const result = await this.#spawn([
|
|
790
|
+
const result = await this.#spawn([...this.#vaultCliArg(parsed.ref), "vault", "info"], context);
|
|
791
791
|
assertCliSuccess("vault info", result);
|
|
792
792
|
cliInfo = result.stdout.trim();
|
|
793
793
|
cachedVaultInfo.set(cacheKey, cliInfo);
|
|
@@ -926,7 +926,7 @@ export class VaultProtocolHandler implements ProtocolHandler {
|
|
|
926
926
|
context?: ResolveContext,
|
|
927
927
|
): Promise<InternalResource> {
|
|
928
928
|
const invocation = buildObsidianCliInvocation(parsed);
|
|
929
|
-
const args = [...
|
|
929
|
+
const args = [...this.#vaultCliArg(parsed.ref), ...invocation.args];
|
|
930
930
|
const result = await this.#spawn(args, context);
|
|
931
931
|
assertCliSuccess(invocation.opLabel, result);
|
|
932
932
|
return {
|
package/src/launch/client.ts
CHANGED
|
@@ -367,7 +367,17 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
367
367
|
try {
|
|
368
368
|
message = parseDaemonWireMessage(decoded);
|
|
369
369
|
} catch (error) {
|
|
370
|
-
|
|
370
|
+
const parseError = error instanceof Error ? error : new Error(String(error));
|
|
371
|
+
if (
|
|
372
|
+
typeof decoded === "object" &&
|
|
373
|
+
decoded !== null &&
|
|
374
|
+
"event" in decoded &&
|
|
375
|
+
decoded.event === "daemon-completed"
|
|
376
|
+
) {
|
|
377
|
+
logger.warn("Ignoring malformed daemon completion", { error: parseError.message });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
this.#rejectPending(parseError);
|
|
371
381
|
continue;
|
|
372
382
|
}
|
|
373
383
|
if ("event" in message) {
|
package/src/launch/protocol.ts
CHANGED
|
@@ -168,6 +168,11 @@ function optionalString(value: unknown, label: string): string | undefined {
|
|
|
168
168
|
return stringValue(value, label);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function optionalRawString(value: unknown, label: string): string | undefined {
|
|
172
|
+
if (value === undefined) return undefined;
|
|
173
|
+
return rawString(value, label);
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
function booleanValue(value: unknown, label: string): boolean {
|
|
172
177
|
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
173
178
|
return value;
|
|
@@ -272,7 +277,7 @@ export function parseDaemonSnapshot(value: unknown): DaemonSnapshot {
|
|
|
272
277
|
restartCount: numberValue(source.restartCount, "daemon.restartCount"),
|
|
273
278
|
outputBytes: numberValue(source.outputBytes, "daemon.outputBytes"),
|
|
274
279
|
owner: optionalString(source.owner, "daemon.owner"),
|
|
275
|
-
readyMatch:
|
|
280
|
+
readyMatch: optionalRawString(source.readyMatch, "daemon.readyMatch"),
|
|
276
281
|
readyPending: source.readyPending === undefined ? undefined : readyPendingList(source.readyPending),
|
|
277
282
|
persist: booleanValue(source.persist, "daemon.persist"),
|
|
278
283
|
detached: source.detached === undefined ? false : booleanValue(source.detached, "daemon.detached"),
|
|
@@ -427,7 +432,7 @@ export function parseDaemonRpcResult(operation: DaemonOperation, value: unknown)
|
|
|
427
432
|
return {
|
|
428
433
|
op: "wait",
|
|
429
434
|
daemon: parseDaemonSnapshot(source.daemon),
|
|
430
|
-
matched:
|
|
435
|
+
matched: optionalRawString(source.matched, "result.matched"),
|
|
431
436
|
timedOut: booleanValue(source.timedOut, "result.timedOut"),
|
|
432
437
|
};
|
|
433
438
|
case "send":
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { formatPathRelativeToCwd } from "../tools/path-utils";
|
|
5
|
+
import { throwIfAborted } from "../tools/tool-errors";
|
|
6
|
+
import { getOrCreateClient, sendRequest, supportsDocumentDiagnostics, waitForProjectLoaded } from "./client";
|
|
7
|
+
import { getLinterClient } from "./clients";
|
|
8
|
+
import { hasRootMarkerAncestor } from "./config";
|
|
9
|
+
import { applyTextEditsToString } from "./edits";
|
|
10
|
+
import { resolveFormatOptions } from "./format-options";
|
|
11
|
+
import { isProjectAwareLspServer } from "./servers";
|
|
12
|
+
import type {
|
|
13
|
+
Diagnostic,
|
|
14
|
+
Location,
|
|
15
|
+
LocationLink,
|
|
16
|
+
LspClient,
|
|
17
|
+
Position,
|
|
18
|
+
PublishedDiagnostics,
|
|
19
|
+
ServerConfig,
|
|
20
|
+
TextEdit,
|
|
21
|
+
} from "./types";
|
|
22
|
+
import {
|
|
23
|
+
fileToUri,
|
|
24
|
+
formatDiagnostic,
|
|
25
|
+
formatDiagnosticsSummary,
|
|
26
|
+
formatLocation,
|
|
27
|
+
readLocationContext,
|
|
28
|
+
sortDiagnostics,
|
|
29
|
+
uriToFile,
|
|
30
|
+
} from "./utils";
|
|
31
|
+
|
|
32
|
+
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
|
|
33
|
+
export const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
|
|
34
|
+
export const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
|
|
35
|
+
const DIAGNOSTICS_POLL_MS = 100;
|
|
36
|
+
const DIAGNOSTICS_SETTLE_MS = 250;
|
|
37
|
+
/**
|
|
38
|
+
* How long the edit/write writethrough blocks inline waiting for fresh
|
|
39
|
+
* diagnostics before handing slow servers off to the deferred late-injection
|
|
40
|
+
* channel. Keeps the common fast-server case inline while letting an edit
|
|
41
|
+
* return promptly when a server (e.g. a large-monorepo tsserver) is slow to
|
|
42
|
+
* publish fresh diagnostics.
|
|
43
|
+
*/
|
|
44
|
+
export const INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 500;
|
|
45
|
+
/**
|
|
46
|
+
* Inner per-server diagnostics wait budget for the background/deferred fetch.
|
|
47
|
+
* Longer than the inline cap (and the old 3s default) so a slow server still
|
|
48
|
+
* delivers late instead of giving up before it ever publishes.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS = 12_000;
|
|
51
|
+
export const MAX_GLOB_DIAGNOSTIC_TARGETS = 20;
|
|
52
|
+
export const WORKSPACE_SYMBOL_LIMIT = 200;
|
|
53
|
+
export const PROJECT_INDEXED_ACTIONS: ReadonlySet<string> = new Set([
|
|
54
|
+
"definition",
|
|
55
|
+
"type_definition",
|
|
56
|
+
"implementation",
|
|
57
|
+
"references",
|
|
58
|
+
"rename",
|
|
59
|
+
"hover",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const RUST_WORKSPACE_MARKERS = ["Cargo.toml", "rust-analyzer.toml"] as const;
|
|
63
|
+
|
|
64
|
+
export function hasRustWorkspaceAncestor(filePath: string): boolean {
|
|
65
|
+
let dir = path.dirname(filePath);
|
|
66
|
+
while (true) {
|
|
67
|
+
for (const marker of RUST_WORKSPACE_MARKERS) {
|
|
68
|
+
if (fs.existsSync(path.join(dir, marker))) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const parent = path.dirname(dir);
|
|
73
|
+
if (parent === dir) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
dir = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function limitDiagnosticMessages(messages: string[]): string[] {
|
|
81
|
+
if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) {
|
|
82
|
+
return messages;
|
|
83
|
+
}
|
|
84
|
+
return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ORPHAN_TYPESCRIPT_PROJECT_DIAGNOSTIC_CODES: Record<number, true> = {
|
|
88
|
+
1375: true,
|
|
89
|
+
1378: true,
|
|
90
|
+
2307: true,
|
|
91
|
+
2580: true,
|
|
92
|
+
2591: true,
|
|
93
|
+
2792: true,
|
|
94
|
+
2867: true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function diagnosticCodeNumber(diagnostic: Diagnostic): number | null {
|
|
98
|
+
if (typeof diagnostic.code === "number") return diagnostic.code;
|
|
99
|
+
if (typeof diagnostic.code === "string" && /^\d+$/.test(diagnostic.code)) return Number(diagnostic.code);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function isTypeScriptProjectDiagnostic(serverName: string, diagnostic: Diagnostic): boolean {
|
|
103
|
+
if (diagnostic.source !== "typescript" && !serverName.toLowerCase().includes("typescript")) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const code = diagnosticCodeNumber(diagnostic);
|
|
107
|
+
return code !== null && ORPHAN_TYPESCRIPT_PROJECT_DIAGNOSTIC_CODES[code] === true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function filterOrphanProjectDiagnostics(
|
|
111
|
+
absolutePath: string,
|
|
112
|
+
serverName: string,
|
|
113
|
+
serverConfig: ServerConfig,
|
|
114
|
+
diagnostics: Diagnostic[],
|
|
115
|
+
): Diagnostic[] {
|
|
116
|
+
if (!serverConfig.rootMarkers.length || hasRootMarkerAncestor(absolutePath, serverConfig.rootMarkers)) {
|
|
117
|
+
return diagnostics;
|
|
118
|
+
}
|
|
119
|
+
return diagnostics.filter(diagnostic => !isTypeScriptProjectDiagnostic(serverName, diagnostic));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const LOCATION_CONTEXT_LINES = 1;
|
|
123
|
+
export const REFERENCE_CONTEXT_LIMIT = 50;
|
|
124
|
+
|
|
125
|
+
export const REFERENCES_RETRY_COUNT = 2;
|
|
126
|
+
export const REFERENCES_RETRY_DELAY_MS = 250;
|
|
127
|
+
|
|
128
|
+
function comparePosition(a: Position, b: Position): number {
|
|
129
|
+
return a.line === b.line ? a.character - b.character : a.line - b.line;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function rangeContainsPosition(range: Location["range"], position: Position): boolean {
|
|
133
|
+
return comparePosition(range.start, position) <= 0 && comparePosition(position, range.end) <= 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function isOnlyQueriedDeclaration(locations: Location[], uri: string, position: Position): boolean {
|
|
137
|
+
return locations.length === 1 && locations[0]?.uri === uri && rangeContainsPosition(locations[0].range, position);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function normalizeLocationResult(
|
|
141
|
+
result: Location | Location[] | LocationLink | LocationLink[] | null,
|
|
142
|
+
): Location[] {
|
|
143
|
+
if (!result) return [];
|
|
144
|
+
const raw = Array.isArray(result) ? result : [result];
|
|
145
|
+
return raw.flatMap(loc => {
|
|
146
|
+
if ("uri" in loc) {
|
|
147
|
+
return [loc as Location];
|
|
148
|
+
}
|
|
149
|
+
if ("targetUri" in loc) {
|
|
150
|
+
const link = loc as LocationLink;
|
|
151
|
+
return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function formatLocationWithContext(location: Location, cwd: string): Promise<string> {
|
|
158
|
+
const header = ` ${formatLocation(location, cwd)}`;
|
|
159
|
+
const context = await readLocationContext(
|
|
160
|
+
uriToFile(location.uri),
|
|
161
|
+
location.range.start.line + 1,
|
|
162
|
+
LOCATION_CONTEXT_LINES,
|
|
163
|
+
);
|
|
164
|
+
if (context.length === 0) {
|
|
165
|
+
return header;
|
|
166
|
+
}
|
|
167
|
+
return `${header}\n${context.map(lineText => ` ${lineText}`).join("\n")}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface WaitForDiagnosticsOptions {
|
|
171
|
+
timeoutMs?: number;
|
|
172
|
+
signal?: AbortSignal;
|
|
173
|
+
minVersion?: number;
|
|
174
|
+
expectedDocumentVersion?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Quiescence window (ms). typescript-language-server never echoes the document
|
|
177
|
+
* version (issue #983) and emits diagnostics from several sources at different
|
|
178
|
+
* times, so there is no single "complete, version-matched" publish to gate on.
|
|
179
|
+
* When the server does not exact-version-match, accept the latest publish only
|
|
180
|
+
* after no newer one has arrived for this long, letting an in-flight pre-edit
|
|
181
|
+
* publish be superseded by the fresh one.
|
|
182
|
+
*/
|
|
183
|
+
settleMs?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requestDocumentDiagnostics(
|
|
187
|
+
client: LspClient,
|
|
188
|
+
uri: string,
|
|
189
|
+
signal: AbortSignal | undefined,
|
|
190
|
+
timeoutMs: number,
|
|
191
|
+
): Promise<Diagnostic[] | undefined> {
|
|
192
|
+
return sendRequest(client, "textDocument/diagnostic", { textDocument: { uri } }, signal, timeoutMs)
|
|
193
|
+
.then(report => {
|
|
194
|
+
if (!report || typeof report !== "object" || !("kind" in report) || report.kind !== "full") {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
if (!("items" in report) || !Array.isArray(report.items)) return undefined;
|
|
198
|
+
return report.items;
|
|
199
|
+
})
|
|
200
|
+
.catch(err => {
|
|
201
|
+
if (!signal?.aborted) {
|
|
202
|
+
logger.debug("LSP document diagnostic pull failed", { server: client.name, uri, error: String(err) });
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function waitForDiagnostics(
|
|
209
|
+
client: LspClient,
|
|
210
|
+
uri: string,
|
|
211
|
+
options: WaitForDiagnosticsOptions = {},
|
|
212
|
+
): Promise<Diagnostic[]> {
|
|
213
|
+
const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, settleMs = DIAGNOSTICS_SETTLE_MS } = options;
|
|
214
|
+
const deadline = Date.now() + timeoutMs;
|
|
215
|
+
let pullAttempted = false;
|
|
216
|
+
let pullResultPromise: Promise<{ diagnostics: Diagnostic[] | undefined }> | undefined;
|
|
217
|
+
let pulled: Diagnostic[] | undefined;
|
|
218
|
+
let settledRef: PublishedDiagnostics | undefined;
|
|
219
|
+
let settledAt = 0;
|
|
220
|
+
while (Date.now() < deadline) {
|
|
221
|
+
throwIfAborted(signal);
|
|
222
|
+
if (!pullAttempted && supportsDocumentDiagnostics(client)) {
|
|
223
|
+
pullAttempted = true;
|
|
224
|
+
pullResultPromise = requestDocumentDiagnostics(client, uri, signal, Math.max(1, deadline - Date.now())).then(
|
|
225
|
+
diagnostics => ({ diagnostics }),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
230
|
+
const published = client.diagnostics.get(uri);
|
|
231
|
+
if (published && versionOk) {
|
|
232
|
+
// Server honored our exact document version → authoritative, accept now.
|
|
233
|
+
if (expectedDocumentVersion !== undefined && published.version === expectedDocumentVersion) {
|
|
234
|
+
return published.diagnostics;
|
|
235
|
+
}
|
|
236
|
+
// Unversioned/mismatched publish: wait for the stream to go quiet so an
|
|
237
|
+
// in-flight publish for the pre-edit content is superseded by the fresh one.
|
|
238
|
+
if (published !== settledRef) {
|
|
239
|
+
settledRef = published;
|
|
240
|
+
settledAt = Date.now();
|
|
241
|
+
} else if (Date.now() - settledAt >= settleMs) {
|
|
242
|
+
return published.diagnostics;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const pollMs = Math.min(DIAGNOSTICS_POLL_MS, Math.max(0, deadline - Date.now()));
|
|
247
|
+
if (!pullResultPromise) {
|
|
248
|
+
await Bun.sleep(pollMs);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const pullResult = await Promise.race([pullResultPromise, Bun.sleep(pollMs).then(() => undefined)]);
|
|
252
|
+
if (pullResult) {
|
|
253
|
+
pullResultPromise = undefined;
|
|
254
|
+
pulled = pullResult.diagnostics;
|
|
255
|
+
if (pulled !== undefined) break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
260
|
+
const published = client.diagnostics.get(uri);
|
|
261
|
+
if (published && versionOk) {
|
|
262
|
+
return published.diagnostics;
|
|
263
|
+
}
|
|
264
|
+
if (pullResultPromise) {
|
|
265
|
+
pulled = (await pullResultPromise).diagnostics;
|
|
266
|
+
}
|
|
267
|
+
throwIfAborted(signal);
|
|
268
|
+
if (pulled === undefined) return [];
|
|
269
|
+
client.diagnostics.set(uri, {
|
|
270
|
+
diagnostics: pulled,
|
|
271
|
+
version: expectedDocumentVersion ?? client.openFiles.get(uri)?.version ?? null,
|
|
272
|
+
});
|
|
273
|
+
client.diagnosticsVersion += 1;
|
|
274
|
+
return pulled;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Result from getDiagnosticsForFile */
|
|
278
|
+
export interface FileDiagnosticsResult {
|
|
279
|
+
/** Name of the LSP server used (if available) */
|
|
280
|
+
server?: string;
|
|
281
|
+
/** Formatted diagnostic messages */
|
|
282
|
+
messages: string[];
|
|
283
|
+
/** Summary string (e.g., "2 error(s), 1 warning(s)") */
|
|
284
|
+
summary: string;
|
|
285
|
+
/** Whether there are any errors (severity 1) */
|
|
286
|
+
errored: boolean;
|
|
287
|
+
/** Whether the file was formatted */
|
|
288
|
+
formatter?: FileFormatResult;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export type ServerVersionMap = Map<string, number>;
|
|
292
|
+
|
|
293
|
+
interface GetDiagnosticsForFileOptions {
|
|
294
|
+
signal?: AbortSignal;
|
|
295
|
+
minVersions?: ServerVersionMap;
|
|
296
|
+
expectedDocumentVersions?: ServerVersionMap;
|
|
297
|
+
/** Per-server wait budget (ms). Defaults to {@link SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS}. */
|
|
298
|
+
timeoutMs?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Capture current diagnostic versions for all LSP servers.
|
|
303
|
+
* Call this BEFORE syncing content to detect stale diagnostics later.
|
|
304
|
+
*/
|
|
305
|
+
export async function captureDiagnosticVersions(
|
|
306
|
+
cwd: string,
|
|
307
|
+
servers: Array<[string, ServerConfig]>,
|
|
308
|
+
initTimeoutMs?: number,
|
|
309
|
+
signal?: AbortSignal,
|
|
310
|
+
): Promise<ServerVersionMap> {
|
|
311
|
+
const versions = new Map<string, number>();
|
|
312
|
+
await Promise.allSettled(
|
|
313
|
+
servers.map(async ([serverName, serverConfig]) => {
|
|
314
|
+
if (serverConfig.createClient) return;
|
|
315
|
+
const client = await getOrCreateClient(serverConfig, cwd, initTimeoutMs, signal);
|
|
316
|
+
versions.set(serverName, client.diagnosticsVersion);
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
return versions;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function captureOpenFileVersions(
|
|
323
|
+
absolutePath: string,
|
|
324
|
+
cwd: string,
|
|
325
|
+
servers: Array<[string, ServerConfig]>,
|
|
326
|
+
signal?: AbortSignal,
|
|
327
|
+
): Promise<ServerVersionMap> {
|
|
328
|
+
const uri = fileToUri(absolutePath);
|
|
329
|
+
const versions = new Map<string, number>();
|
|
330
|
+
await Promise.allSettled(
|
|
331
|
+
servers.map(async ([serverName, serverConfig]) => {
|
|
332
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
333
|
+
const version = client.openFiles.get(uri)?.version;
|
|
334
|
+
if (version !== undefined) {
|
|
335
|
+
versions.set(serverName, version);
|
|
336
|
+
}
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
return versions;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get diagnostics for a file using LSP or custom linter client.
|
|
344
|
+
*
|
|
345
|
+
* @param absolutePath - Absolute path to the file
|
|
346
|
+
* @param cwd - Working directory for LSP config resolution
|
|
347
|
+
* @param servers - Servers to query diagnostics for
|
|
348
|
+
* @param minVersions - Minimum diagnostic versions per server (to detect stale results)
|
|
349
|
+
* @returns Diagnostic results or undefined if no servers
|
|
350
|
+
*/
|
|
351
|
+
export async function getDiagnosticsForFile(
|
|
352
|
+
absolutePath: string,
|
|
353
|
+
cwd: string,
|
|
354
|
+
servers: Array<[string, ServerConfig]>,
|
|
355
|
+
options: GetDiagnosticsForFileOptions = {},
|
|
356
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
357
|
+
const { signal, minVersions, expectedDocumentVersions, timeoutMs } = options;
|
|
358
|
+
if (servers.length === 0) {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const uri = fileToUri(absolutePath);
|
|
363
|
+
const relPath = formatPathRelativeToCwd(absolutePath, cwd);
|
|
364
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
365
|
+
const serverNames: string[] = [];
|
|
366
|
+
|
|
367
|
+
// Wait for diagnostics from all servers in parallel
|
|
368
|
+
const results = await Promise.allSettled(
|
|
369
|
+
servers.map(async ([serverName, serverConfig]) => {
|
|
370
|
+
throwIfAborted(signal);
|
|
371
|
+
// Use custom linter client if configured
|
|
372
|
+
if (serverConfig.createClient) {
|
|
373
|
+
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
374
|
+
const diagnostics = await linterClient.lint(absolutePath);
|
|
375
|
+
return { serverName, serverConfig, diagnostics };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Default: use LSP
|
|
379
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
380
|
+
throwIfAborted(signal);
|
|
381
|
+
if (isProjectAwareLspServer(serverConfig)) {
|
|
382
|
+
await waitForProjectLoaded(client, signal);
|
|
383
|
+
throwIfAborted(signal);
|
|
384
|
+
}
|
|
385
|
+
// Content already synced + didSave sent, wait for fresh diagnostics
|
|
386
|
+
const minVersion = minVersions?.get(serverName);
|
|
387
|
+
const expectedDocumentVersion = expectedDocumentVersions?.get(serverName);
|
|
388
|
+
const diagnostics = await waitForDiagnostics(client, uri, {
|
|
389
|
+
timeoutMs: timeoutMs ?? SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
390
|
+
signal,
|
|
391
|
+
minVersion,
|
|
392
|
+
expectedDocumentVersion,
|
|
393
|
+
});
|
|
394
|
+
return { serverName, serverConfig, diagnostics };
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
for (const result of results) {
|
|
399
|
+
if (result.status === "fulfilled") {
|
|
400
|
+
serverNames.push(result.value.serverName);
|
|
401
|
+
allDiagnostics.push(
|
|
402
|
+
...filterOrphanProjectDiagnostics(
|
|
403
|
+
absolutePath,
|
|
404
|
+
result.value.serverName,
|
|
405
|
+
result.value.serverConfig,
|
|
406
|
+
result.value.diagnostics,
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (serverNames.length === 0) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (allDiagnostics.length === 0) {
|
|
417
|
+
return {
|
|
418
|
+
server: serverNames.join(", "),
|
|
419
|
+
messages: [],
|
|
420
|
+
summary: "OK",
|
|
421
|
+
errored: false,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Deduplicate diagnostics by range + message (different servers might report similar issues)
|
|
426
|
+
const seen = new Set<string>();
|
|
427
|
+
const uniqueDiagnostics: Diagnostic[] = [];
|
|
428
|
+
for (const d of allDiagnostics) {
|
|
429
|
+
const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
|
|
430
|
+
if (!seen.has(key)) {
|
|
431
|
+
seen.add(key);
|
|
432
|
+
uniqueDiagnostics.push(d);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
sortDiagnostics(uniqueDiagnostics);
|
|
437
|
+
const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
|
|
438
|
+
const limited = limitDiagnosticMessages(formatted);
|
|
439
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
440
|
+
const hasErrors = uniqueDiagnostics.some(d => d.severity === 1);
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
server: serverNames.join(", "),
|
|
444
|
+
messages: limited,
|
|
445
|
+
summary,
|
|
446
|
+
errored: hasErrors,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export enum FileFormatResult {
|
|
451
|
+
UNCHANGED = "unchanged",
|
|
452
|
+
FORMATTED = "formatted",
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Format content using LSP or custom linter client.
|
|
457
|
+
*
|
|
458
|
+
* @param absolutePath - Absolute path (for URI)
|
|
459
|
+
* @param content - Content to format
|
|
460
|
+
* @param cwd - Working directory for LSP config resolution
|
|
461
|
+
* @param servers - Servers to try formatting with
|
|
462
|
+
* @returns Formatted content, or original if no formatter available
|
|
463
|
+
*/
|
|
464
|
+
export async function formatContent(
|
|
465
|
+
absolutePath: string,
|
|
466
|
+
content: string,
|
|
467
|
+
cwd: string,
|
|
468
|
+
servers: Array<[string, ServerConfig]>,
|
|
469
|
+
signal?: AbortSignal,
|
|
470
|
+
): Promise<string> {
|
|
471
|
+
if (servers.length === 0) {
|
|
472
|
+
return content;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const uri = fileToUri(absolutePath);
|
|
476
|
+
|
|
477
|
+
for (const [serverName, serverConfig] of servers) {
|
|
478
|
+
try {
|
|
479
|
+
throwIfAborted(signal);
|
|
480
|
+
// Use custom linter client if configured
|
|
481
|
+
if (serverConfig.createClient) {
|
|
482
|
+
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
483
|
+
return await linterClient.format(absolutePath, content);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Default: use LSP
|
|
487
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
488
|
+
throwIfAborted(signal);
|
|
489
|
+
|
|
490
|
+
const caps = client.serverCapabilities;
|
|
491
|
+
if (!caps?.documentFormattingProvider) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Request formatting (content already synced)
|
|
496
|
+
const edits = (await sendRequest(
|
|
497
|
+
client,
|
|
498
|
+
"textDocument/formatting",
|
|
499
|
+
{
|
|
500
|
+
textDocument: { uri },
|
|
501
|
+
options: resolveFormatOptions(absolutePath, content),
|
|
502
|
+
},
|
|
503
|
+
signal,
|
|
504
|
+
)) as TextEdit[] | null;
|
|
505
|
+
|
|
506
|
+
if (!edits || edits.length === 0) {
|
|
507
|
+
return content;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Apply edits in-memory and return
|
|
511
|
+
return applyTextEditsToString(content, edits);
|
|
512
|
+
} catch {}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return content;
|
|
516
|
+
}
|