apex-code 0.0.1-alpha.4 → 0.0.1-alpha.6
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 +78 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +2 -2
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +20 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +91 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +17 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +28 -4
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/lsp/client.d.ts +76 -0
- package/dist/core/lsp/client.d.ts.map +1 -0
- package/dist/core/lsp/client.js +469 -0
- package/dist/core/lsp/client.js.map +1 -0
- package/dist/core/lsp/diagnostics.d.ts +47 -0
- package/dist/core/lsp/diagnostics.d.ts.map +1 -0
- package/dist/core/lsp/diagnostics.js +224 -0
- package/dist/core/lsp/diagnostics.js.map +1 -0
- package/dist/core/lsp/pool.d.ts +48 -0
- package/dist/core/lsp/pool.d.ts.map +1 -0
- package/dist/core/lsp/pool.js +150 -0
- package/dist/core/lsp/pool.js.map +1 -0
- package/dist/core/lsp/registry.d.ts +49 -0
- package/dist/core/lsp/registry.d.ts.map +1 -0
- package/dist/core/lsp/registry.js +215 -0
- package/dist/core/lsp/registry.js.map +1 -0
- package/dist/core/lsp/status.d.ts +23 -0
- package/dist/core/lsp/status.d.ts.map +1 -0
- package/dist/core/lsp/status.js +35 -0
- package/dist/core/lsp/status.js.map +1 -0
- package/dist/core/lsp/sync.d.ts +13 -0
- package/dist/core/lsp/sync.d.ts.map +1 -0
- package/dist/core/lsp/sync.js +71 -0
- package/dist/core/lsp/sync.js.map +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +15 -0
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +43 -0
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +70 -3
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +1 -0
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +10 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +19 -7
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +77 -19
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/contract.d.ts +29 -2
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/diagnostics.d.ts +39 -0
- package/dist/core/tools/diagnostics.d.ts.map +1 -0
- package/dist/core/tools/diagnostics.js +62 -0
- package/dist/core/tools/diagnostics.js.map +1 -0
- package/dist/core/tools/edit.d.ts +10 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +17 -8
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/index.d.ts +20 -3
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +18 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +62 -0
- package/dist/core/tools/lsp.d.ts.map +1 -0
- package/dist/core/tools/lsp.js +216 -0
- package/dist/core/tools/lsp.js.map +1 -0
- package/dist/core/tools/skill-search.d.ts +32 -0
- package/dist/core/tools/skill-search.d.ts.map +1 -0
- package/dist/core/tools/skill-search.js +56 -0
- package/dist/core/tools/skill-search.js.map +1 -0
- package/dist/core/tools/write.d.ts +10 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +16 -3
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +49 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +235 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +2 -1
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +19 -6
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +172 -89
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/splash-header.d.ts +41 -0
- package/dist/modes/interactive/components/splash-header.d.ts.map +1 -0
- package/dist/modes/interactive/components/splash-header.js +145 -0
- package/dist/modes/interactive/components/splash-header.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +19 -15
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/apex.json +94 -0
- package/dist/modes/interactive/theme/light.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +6 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/themes/apex-logo.d.ts +21 -0
- package/dist/themes/apex-logo.d.ts.map +1 -0
- package/dist/themes/apex-logo.js +31 -0
- package/dist/themes/apex-logo.js.map +1 -0
- package/dist/utils/changelog.d.ts +7 -2
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +82 -13
- package/dist/utils/changelog.js.map +1 -1
- package/docs/skills.md +54 -4
- package/npm-shrinkwrap.json +5 -5
- package/package.json +2 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare const LSP_MAX_HEADER_BYTES: number;
|
|
2
|
+
export declare const LSP_MAX_MESSAGE_BYTES: number;
|
|
3
|
+
export declare const LSP_MAX_STDERR_TAIL_BYTES: number;
|
|
4
|
+
export interface LspClientOptions {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly args?: readonly string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
9
|
+
readonly initializationTimeoutMs?: number;
|
|
10
|
+
readonly requestTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LspNotification {
|
|
13
|
+
readonly method: string;
|
|
14
|
+
readonly params?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export type LspTextDocumentSyncKind = "none" | "full" | "incremental";
|
|
17
|
+
export interface LspTextDocumentSync {
|
|
18
|
+
readonly kind: LspTextDocumentSyncKind;
|
|
19
|
+
readonly openClose: boolean;
|
|
20
|
+
readonly save: boolean;
|
|
21
|
+
readonly saveIncludeText: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class LspJsonRpcError extends Error {
|
|
24
|
+
readonly code: number;
|
|
25
|
+
readonly data?: unknown;
|
|
26
|
+
constructor(error: {
|
|
27
|
+
code: number;
|
|
28
|
+
message: string;
|
|
29
|
+
data?: unknown;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** A bounded JSON-RPC 2.0 client for one language server's stdio. */
|
|
33
|
+
export declare class LspClient {
|
|
34
|
+
private readonly options;
|
|
35
|
+
private child;
|
|
36
|
+
private stdin;
|
|
37
|
+
private stdout;
|
|
38
|
+
private input;
|
|
39
|
+
private stderrTail;
|
|
40
|
+
private nextRequestId;
|
|
41
|
+
private readonly pending;
|
|
42
|
+
private readonly notificationListeners;
|
|
43
|
+
private readonly failureListeners;
|
|
44
|
+
private startPromise;
|
|
45
|
+
private disposePromise;
|
|
46
|
+
private state;
|
|
47
|
+
private failure;
|
|
48
|
+
private writeChain;
|
|
49
|
+
private syncCapabilities;
|
|
50
|
+
constructor(options: LspClientOptions);
|
|
51
|
+
get capturedStderr(): string;
|
|
52
|
+
/** Text-document synchronization negotiated during initialization. */
|
|
53
|
+
get textDocumentSync(): LspTextDocumentSync;
|
|
54
|
+
onNotification(listener: (notification: LspNotification) => void): () => void;
|
|
55
|
+
/** Subscribe to the first terminal protocol or process failure. */
|
|
56
|
+
onFailure(listener: (error: Error) => void): () => void;
|
|
57
|
+
start(): Promise<void>;
|
|
58
|
+
request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
59
|
+
notify(method: string, params?: unknown): void;
|
|
60
|
+
dispose(): Promise<void>;
|
|
61
|
+
private initializationTimeoutMs;
|
|
62
|
+
private requestTimeoutMs;
|
|
63
|
+
private initialize;
|
|
64
|
+
private disposeProcess;
|
|
65
|
+
private terminateChild;
|
|
66
|
+
private sendRequest;
|
|
67
|
+
private enqueueWrite;
|
|
68
|
+
private writeFrame;
|
|
69
|
+
private onData;
|
|
70
|
+
private onMessage;
|
|
71
|
+
private handleServerRequest;
|
|
72
|
+
private captureStderr;
|
|
73
|
+
private rejectPending;
|
|
74
|
+
private fail;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/lsp/client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { pathToFileURL } from \"node:url\";\nimport { spawnProcess } from \"../../utils/child-process.ts\";\n\nexport const LSP_MAX_HEADER_BYTES = 8 * 1024;\nexport const LSP_MAX_MESSAGE_BYTES = 8 * 1024 * 1024;\nexport const LSP_MAX_STDERR_TAIL_BYTES = 64 * 1024;\n\nconst DEFAULT_INITIALIZATION_TIMEOUT_MS = 10_000;\nconst DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\nconst SHUTDOWN_TIMEOUT_MS = 2_000;\nconst TERMINATE_GRACE_MS = 500;\nconst KILL_GRACE_MS = 1_000;\n\nexport interface LspClientOptions {\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly cwd: string;\n\treadonly env?: NodeJS.ProcessEnv;\n\treadonly initializationTimeoutMs?: number;\n\treadonly requestTimeoutMs?: number;\n}\n\nexport interface LspNotification {\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport type LspTextDocumentSyncKind = \"none\" | \"full\" | \"incremental\";\n\nexport interface LspTextDocumentSync {\n\treadonly kind: LspTextDocumentSyncKind;\n\treadonly openClose: boolean;\n\treadonly save: boolean;\n\treadonly saveIncludeText: boolean;\n}\n\nexport class LspJsonRpcError extends Error {\n\treadonly code: number;\n\treadonly data?: unknown;\n\n\tconstructor(error: { code: number; message: string; data?: unknown }) {\n\t\tsuper(error.message);\n\t\tthis.name = \"LspJsonRpcError\";\n\t\tthis.code = error.code;\n\t\tthis.data = error.data;\n\t}\n}\n\ntype JsonRpcId = number;\ntype PendingRequest = {\n\tresolve(value: unknown): void;\n\treject(reason: Error): void;\n};\n\ntype ClientState = \"idle\" | \"starting\" | \"ready\" | \"closing\" | \"closed\" | \"failed\";\n\ntype JsonRpcMessage = Record<string, unknown>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isJsonRpcId(value: unknown): value is string | number {\n\treturn typeof value === \"string\" || (typeof value === \"number\" && Number.isSafeInteger(value));\n}\n\nfunction normalizeError(error: unknown): Error {\n\treturn error instanceof Error ? error : new Error(String(error));\n}\n\nfunction parseTextDocumentSync(value: unknown): LspTextDocumentSync {\n\tlet kind: LspTextDocumentSyncKind = \"none\";\n\tlet openClose = false;\n\tlet save = false;\n\tlet saveIncludeText = false;\n\tif (typeof value === \"number\") {\n\t\tkind = value === 1 ? \"full\" : value === 2 ? \"incremental\" : \"none\";\n\t} else if (isRecord(value)) {\n\t\topenClose = value.openClose === true;\n\t\tkind = value.change === 1 ? \"full\" : value.change === 2 ? \"incremental\" : \"none\";\n\t\tif (value.save === true) save = true;\n\t\telse if (isRecord(value.save)) {\n\t\t\tsave = true;\n\t\t\tsaveIncludeText = value.save.includeText === true;\n\t\t}\n\t}\n\treturn { kind, openClose, save, saveIncludeText };\n}\n\nfunction validateTimeout(value: number | undefined, fallback: number, label: string): number {\n\tconst timeout = value ?? fallback;\n\tif (!Number.isSafeInteger(timeout) || timeout < 1) throw new Error(`${label} must be a positive safe integer.`);\n\treturn timeout;\n}\n\nfunction delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** A bounded JSON-RPC 2.0 client for one language server's stdio. */\nexport class LspClient {\n\tprivate readonly options: LspClientOptions;\n\tprivate child: ChildProcess | undefined;\n\tprivate stdin: Writable | undefined;\n\tprivate stdout: Readable | undefined;\n\tprivate input = Buffer.alloc(0);\n\tprivate stderrTail = Buffer.alloc(0);\n\tprivate nextRequestId = 1;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly notificationListeners = new Set<(notification: LspNotification) => void>();\n\tprivate readonly failureListeners = new Set<(error: Error) => void>();\n\tprivate startPromise: Promise<void> | undefined;\n\tprivate disposePromise: Promise<void> | undefined;\n\tprivate state: ClientState = \"idle\";\n\tprivate failure: Error | undefined;\n\tprivate writeChain: Promise<void> = Promise.resolve();\n\tprivate syncCapabilities: LspTextDocumentSync = {\n\t\tkind: \"none\",\n\t\topenClose: false,\n\t\tsave: false,\n\t\tsaveIncludeText: false,\n\t};\n\n\tconstructor(options: LspClientOptions) {\n\t\tthis.options = options;\n\t}\n\n\tget capturedStderr(): string {\n\t\treturn this.stderrTail.toString(\"utf8\");\n\t}\n\n\t/** Text-document synchronization negotiated during initialization. */\n\tget textDocumentSync(): LspTextDocumentSync {\n\t\treturn this.syncCapabilities;\n\t}\n\n\tonNotification(listener: (notification: LspNotification) => void): () => void {\n\t\tthis.notificationListeners.add(listener);\n\t\treturn () => this.notificationListeners.delete(listener);\n\t}\n\n\t/** Subscribe to the first terminal protocol or process failure. */\n\tonFailure(listener: (error: Error) => void): () => void {\n\t\tthis.failureListeners.add(listener);\n\t\tif (this.failure) listener(this.failure);\n\t\treturn () => this.failureListeners.delete(listener);\n\t}\n\n\tstart(): Promise<void> {\n\t\tif (this.failure) return Promise.reject(this.failure);\n\t\tif (this.state === \"closing\" || this.state === \"closed\") {\n\t\t\treturn Promise.reject(new Error(\"Language server client is disposed.\"));\n\t\t}\n\t\tif (!this.startPromise) {\n\t\t\tthis.state = \"starting\";\n\t\t\tthis.startPromise = this.initialize().catch(async (error) => {\n\t\t\t\tconst failure = normalizeError(error);\n\t\t\t\tthis.fail(failure);\n\t\t\t\tawait this.terminateChild();\n\t\t\t\tthrow failure;\n\t\t\t});\n\t\t}\n\t\treturn this.startPromise;\n\t}\n\n\tasync request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown> {\n\t\tawait this.start();\n\t\tif (this.state !== \"ready\") throw this.failure ?? new Error(\"Language server did not become ready.\");\n\t\treturn this.sendRequest(method, params, signal, this.requestTimeoutMs());\n\t}\n\n\tnotify(method: string, params?: unknown): void {\n\t\tif (this.failure) throw this.failure;\n\t\tif (this.state !== \"ready\") throw new Error(\"Language server is not ready.\");\n\t\tvoid this.enqueueWrite({ jsonrpc: \"2.0\", method, ...(params === undefined ? {} : { params }) }).catch((error) => {\n\t\t\tthis.fail(normalizeError(error));\n\t\t});\n\t}\n\n\tdispose(): Promise<void> {\n\t\tif (!this.disposePromise) this.disposePromise = this.disposeProcess();\n\t\treturn this.disposePromise;\n\t}\n\n\tprivate initializationTimeoutMs(): number {\n\t\treturn validateTimeout(\n\t\t\tthis.options.initializationTimeoutMs,\n\t\t\tDEFAULT_INITIALIZATION_TIMEOUT_MS,\n\t\t\t\"initializationTimeoutMs\",\n\t\t);\n\t}\n\n\tprivate requestTimeoutMs(): number {\n\t\treturn validateTimeout(this.options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, \"requestTimeoutMs\");\n\t}\n\n\tprivate async initialize(): Promise<void> {\n\t\tconst child = spawnProcess(this.options.command, [...(this.options.args ?? [])], {\n\t\t\tcwd: this.options.cwd,\n\t\t\tenv: this.options.env,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t});\n\t\tif (!child.stdin || !child.stdout) {\n\t\t\tchild.kill();\n\t\t\tthrow new Error(\"Language server must expose piped stdin and stdout.\");\n\t\t}\n\t\tthis.child = child;\n\t\tthis.stdin = child.stdin;\n\t\tthis.stdout = child.stdout;\n\t\tthis.stdout.on(\"data\", (chunk: Buffer | string) => this.onData(chunk));\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer | string) => this.captureStderr(chunk));\n\t\tchild.stdin.on(\"error\", (error) => this.fail(error));\n\t\tchild.once(\"error\", (error) => this.fail(error));\n\t\tchild.once(\"exit\", (code, signal) => {\n\t\t\tif (this.state !== \"closing\" && this.state !== \"closed\") {\n\t\t\t\tthis.fail(new Error(`Language server exited before shutdown (code=${code} signal=${signal}).`));\n\t\t\t}\n\t\t});\n\n\t\tconst initializeResult = await this.sendRequest(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprocessId: process.pid,\n\t\t\t\tclientInfo: { name: \"Apex Code\" },\n\t\t\t\trootUri: pathToFileURL(this.options.cwd).href,\n\t\t\t\tcapabilities: {\n\t\t\t\t\tgeneral: { positionEncodings: [\"utf-16\"] },\n\t\t\t\t\tworkspace: { workspaceFolders: false },\n\t\t\t\t\ttextDocument: {\n\t\t\t\t\t\tdefinition: {},\n\t\t\t\t\t\treferences: {},\n\t\t\t\t\t\tdocumentSymbol: {},\n\t\t\t\t\t\tpublishDiagnostics: { versionSupport: true },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tworkspaceFolders: null,\n\t\t\t},\n\t\t\tundefined,\n\t\t\tthis.initializationTimeoutMs(),\n\t\t\t\"Language server initialize timed out\",\n\t\t);\n\t\tif (!isRecord(initializeResult) || !isRecord(initializeResult.capabilities)) {\n\t\t\tthrow new Error(\"Language server returned an invalid initialize result.\");\n\t\t}\n\t\tthis.syncCapabilities = parseTextDocumentSync(initializeResult.capabilities.textDocumentSync);\n\t\tif (this.state !== \"starting\") throw new Error(\"Language server initialization was interrupted.\");\n\t\tawait this.enqueueWrite({ jsonrpc: \"2.0\", method: \"initialized\", params: {} });\n\t\tthis.state = \"ready\";\n\t}\n\n\tprivate async disposeProcess(): Promise<void> {\n\t\tif (this.state === \"closed\") return;\n\t\tconst wasReady = this.state === \"ready\";\n\t\tthis.state = \"closing\";\n\t\tthis.rejectPending(new Error(\"Language server client is closing.\"));\n\n\t\tif (wasReady) {\n\t\t\ttry {\n\t\t\t\tawait this.sendRequest(\n\t\t\t\t\t\"shutdown\",\n\t\t\t\t\tundefined,\n\t\t\t\t\tundefined,\n\t\t\t\t\tSHUTDOWN_TIMEOUT_MS,\n\t\t\t\t\t\"Language server shutdown timed out\",\n\t\t\t\t\ttrue,\n\t\t\t\t);\n\t\t\t\tawait this.enqueueWrite({ jsonrpc: \"2.0\", method: \"exit\" });\n\t\t\t} catch {\n\t\t\t\t// Bounded termination below is the fallback for an uncooperative server.\n\t\t\t}\n\t\t}\n\t\tawait this.terminateChild();\n\t\tthis.stdin?.end();\n\t\tthis.state = \"closed\";\n\t}\n\n\tprivate async terminateChild(): Promise<void> {\n\t\tconst child = this.child;\n\t\tif (!child || child.exitCode !== null || child.signalCode !== null) return;\n\t\tconst exited = new Promise<void>((resolve) => child.once(\"exit\", () => resolve()));\n\t\tconst graceful = await Promise.race([exited.then(() => true), delay(TERMINATE_GRACE_MS).then(() => false)]);\n\t\tif (graceful) return;\n\t\tchild.kill(\"SIGTERM\");\n\t\tconst terminated = await Promise.race([exited.then(() => true), delay(KILL_GRACE_MS).then(() => false)]);\n\t\tif (terminated) return;\n\t\tchild.kill(\"SIGKILL\");\n\t\tawait exited;\n\t}\n\n\tprivate sendRequest(\n\t\tmethod: string,\n\t\tparams?: unknown,\n\t\tsignal?: AbortSignal,\n\t\ttimeoutMs?: number,\n\t\ttimeoutMessage = `Language server request ${method} timed out`,\n\t\tallowClosing = false,\n\t): Promise<unknown> {\n\t\tif (this.failure && !allowClosing) return Promise.reject(this.failure);\n\t\tif ((!allowClosing && (this.state === \"closing\" || this.state === \"closed\")) || this.state === \"failed\") {\n\t\t\treturn Promise.reject(this.failure ?? new Error(\"Language server client is disposed.\"));\n\t\t}\n\t\tconst id = this.nextRequestId++;\n\t\tif (!Number.isSafeInteger(id)) return Promise.reject(new Error(\"Language server request id space exhausted.\"));\n\t\tif (signal?.aborted)\n\t\t\treturn Promise.reject(normalizeError(signal.reason ?? new Error(\"Language server request aborted.\")));\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet timer: NodeJS.Timeout | undefined;\n\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", abort);\n\t\t\t\tfn();\n\t\t\t};\n\t\t\tconst abort = () => {\n\t\t\t\tif (!this.pending.delete(id)) return;\n\t\t\t\tvoid this.enqueueWrite({ jsonrpc: \"2.0\", method: \"$/cancelRequest\", params: { id } }).catch(() => {});\n\t\t\t\tsettle(() => reject(normalizeError(signal?.reason ?? new Error(\"Language server request aborted.\"))));\n\t\t\t};\n\t\t\tthis.pending.set(id, {\n\t\t\t\tresolve: (value) => settle(() => resolve(value)),\n\t\t\t\treject: (error) => settle(() => reject(error)),\n\t\t\t});\n\t\t\tsignal?.addEventListener(\"abort\", abort, { once: true });\n\t\t\tif (timeoutMs !== undefined) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tif (!this.pending.delete(id)) return;\n\t\t\t\t\tvoid this.enqueueWrite({ jsonrpc: \"2.0\", method: \"$/cancelRequest\", params: { id } }).catch(() => {});\n\t\t\t\t\tsettle(() => reject(new Error(`${timeoutMessage} after ${timeoutMs}ms.`)));\n\t\t\t\t}, timeoutMs);\n\t\t\t}\n\t\t\tvoid this.enqueueWrite({ jsonrpc: \"2.0\", id, method, ...(params === undefined ? {} : { params }) }).catch(\n\t\t\t\t(error) => {\n\t\t\t\t\tconst pending = this.pending.get(id);\n\t\t\t\t\tif (!pending) return;\n\t\t\t\t\tthis.pending.delete(id);\n\t\t\t\t\tpending.reject(normalizeError(error));\n\t\t\t\t},\n\t\t\t);\n\t\t});\n\t}\n\n\tprivate enqueueWrite(message: object): Promise<void> {\n\t\tconst body = Buffer.from(JSON.stringify(message), \"utf8\");\n\t\tif (body.length > LSP_MAX_MESSAGE_BYTES)\n\t\t\treturn Promise.reject(new Error(\"Language server message exceeds 8 MiB.\"));\n\t\tconst frame = Buffer.concat([Buffer.from(`Content-Length: ${body.length}\\r\\n\\r\\n`, \"ascii\"), body]);\n\t\tconst writing = this.writeChain.then(() => this.writeFrame(frame));\n\t\tthis.writeChain = writing.catch(() => {});\n\t\treturn writing;\n\t}\n\n\tprivate writeFrame(frame: Buffer): Promise<void> {\n\t\tconst stream = this.stdin;\n\t\tif (!stream || stream.destroyed || !stream.writable) {\n\t\t\treturn Promise.reject(new Error(\"Language server stdin is not writable.\"));\n\t\t}\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet waitingForDrain = false;\n\t\t\tconst cleanup = () => {\n\t\t\t\tstream.removeListener(\"error\", onError);\n\t\t\t\tstream.removeListener(\"drain\", onDrain);\n\t\t\t};\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst onDrain = () => {\n\t\t\t\twaitingForDrain = false;\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tstream.once(\"error\", onError);\n\t\t\tconst accepted = stream.write(frame, (error) => {\n\t\t\t\tif (error) onError(error);\n\t\t\t\telse if (!waitingForDrain) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (!accepted) {\n\t\t\t\twaitingForDrain = true;\n\t\t\t\tstream.once(\"drain\", onDrain);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate onData(chunk: Buffer | string): void {\n\t\tif (this.failure) return;\n\t\tthis.input = Buffer.concat([this.input, typeof chunk === \"string\" ? Buffer.from(chunk) : chunk]);\n\t\twhile (true) {\n\t\t\tconst headerEnd = this.input.indexOf(\"\\r\\n\\r\\n\");\n\t\t\tif (headerEnd < 0) {\n\t\t\t\tif (this.input.length > LSP_MAX_HEADER_BYTES) this.fail(new Error(\"Language server header exceeds 8 KiB.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (headerEnd > LSP_MAX_HEADER_BYTES) {\n\t\t\t\tthis.fail(new Error(\"Language server header exceeds 8 KiB.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst header = this.input.subarray(0, headerEnd).toString(\"ascii\");\n\t\t\tconst lengths = [...header.matchAll(/(?:^|\\r\\n)Content-Length:\\s*(\\d+)(?=\\r\\n|$)/gi)];\n\t\t\tif (lengths.length !== 1) {\n\t\t\t\tthis.fail(new Error(\"Language server frame must contain exactly one Content-Length header.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst contentLength = Number(lengths[0][1]);\n\t\t\tif (!Number.isSafeInteger(contentLength) || contentLength > LSP_MAX_MESSAGE_BYTES) {\n\t\t\t\tthis.fail(new Error(\"Language server message exceeds 8 MiB.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst bodyStart = headerEnd + 4;\n\t\t\tif (this.input.length < bodyStart + contentLength) return;\n\t\t\tconst body = this.input.subarray(bodyStart, bodyStart + contentLength).toString(\"utf8\");\n\t\t\tthis.input = this.input.subarray(bodyStart + contentLength);\n\t\t\ttry {\n\t\t\t\tconst parsed: unknown = JSON.parse(body);\n\t\t\t\tif (!isRecord(parsed)) throw new Error(\"Language server JSON-RPC message must be an object.\");\n\t\t\t\tthis.onMessage(parsed);\n\t\t\t} catch (error) {\n\t\t\t\tthis.fail(normalizeError(error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate onMessage(message: JsonRpcMessage): void {\n\t\tif (message.jsonrpc !== \"2.0\") {\n\t\t\tthis.fail(new Error('Language server message must declare jsonrpc \"2.0\".'));\n\t\t\treturn;\n\t\t}\n\t\tif (typeof message.method === \"string\") {\n\t\t\tif (message.id !== undefined && message.id !== null) {\n\t\t\t\tvoid this.handleServerRequest(message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst notification: LspNotification = {\n\t\t\t\tmethod: message.method,\n\t\t\t\t...(message.params === undefined ? {} : { params: message.params }),\n\t\t\t};\n\t\t\tfor (const listener of this.notificationListeners) listener(notification);\n\t\t\treturn;\n\t\t}\n\t\tif (typeof message.id !== \"number\" || !Number.isSafeInteger(message.id)) return;\n\t\tconst pending = this.pending.get(message.id);\n\t\tif (!pending) return;\n\t\tconst hasResult = Object.hasOwn(message, \"result\");\n\t\tconst hasError = Object.hasOwn(message, \"error\");\n\t\tif (hasResult === hasError) {\n\t\t\tthis.fail(new Error(\"Language server response must contain exactly one of result or error.\"));\n\t\t\treturn;\n\t\t}\n\t\tthis.pending.delete(message.id);\n\t\tif (hasError) {\n\t\t\tconst error = message.error;\n\t\t\tif (!isRecord(error) || !Number.isSafeInteger(error.code) || typeof error.message !== \"string\") {\n\t\t\t\tthis.fail(new Error(\"Language server returned an invalid JSON-RPC error.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpending.reject(new LspJsonRpcError({ code: error.code as number, message: error.message, data: error.data }));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(message.result);\n\t}\n\n\tprivate async handleServerRequest(message: JsonRpcMessage): Promise<void> {\n\t\tif (!isJsonRpcId(message.id) || typeof message.method !== \"string\") return;\n\t\tlet result: unknown;\n\t\tlet error: { code: number; message: string } | undefined;\n\t\tswitch (message.method) {\n\t\t\tcase \"workspace/configuration\": {\n\t\t\t\tconst params = message.params;\n\t\t\t\tif (!isRecord(params) || !Array.isArray(params.items)) error = { code: -32602, message: \"Invalid params\" };\n\t\t\t\telse result = params.items.map(() => null);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"workspace/workspaceFolders\":\n\t\t\tcase \"window/showMessageRequest\":\n\t\t\t\tresult = null;\n\t\t\t\tbreak;\n\t\t\tcase \"client/registerCapability\":\n\t\t\tcase \"client/unregisterCapability\":\n\t\t\tcase \"window/workDoneProgress/create\":\n\t\t\tcase \"workspace/semanticTokens/refresh\":\n\t\t\tcase \"workspace/inlayHint/refresh\":\n\t\t\tcase \"workspace/diagnostic/refresh\":\n\t\t\tcase \"workspace/codeLens/refresh\":\n\t\t\t\tresult = null;\n\t\t\t\tbreak;\n\t\t\tcase \"workspace/applyEdit\":\n\t\t\t\tresult = { applied: false, failureReason: \"Apex Code does not accept server-initiated workspace edits.\" };\n\t\t\t\tbreak;\n\t\t\tcase \"window/showDocument\":\n\t\t\t\tresult = { success: false };\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\terror = { code: -32601, message: \"Method not found\" };\n\t\t}\n\t\tawait this.enqueueWrite({\n\t\t\tjsonrpc: \"2.0\",\n\t\t\tid: message.id,\n\t\t\t...(error ? { error } : { result }),\n\t\t}).catch((writeError) => this.fail(normalizeError(writeError)));\n\t}\n\n\tprivate captureStderr(chunk: Buffer | string): void {\n\t\tconst bytes = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n\t\tthis.stderrTail = Buffer.concat([this.stderrTail, bytes]);\n\t\tif (this.stderrTail.length > LSP_MAX_STDERR_TAIL_BYTES) {\n\t\t\tthis.stderrTail = this.stderrTail.subarray(this.stderrTail.length - LSP_MAX_STDERR_TAIL_BYTES);\n\t\t}\n\t}\n\n\tprivate rejectPending(error: Error): void {\n\t\tfor (const request of this.pending.values()) request.reject(error);\n\t\tthis.pending.clear();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.state === \"closed\") return;\n\t\tconst firstFailure = this.failure === undefined;\n\t\tthis.failure ??= error;\n\t\tif (this.state !== \"closing\") this.state = \"failed\";\n\t\tthis.rejectPending(this.failure);\n\t\tif (firstFailure) for (const listener of this.failureListeners) listener(this.failure);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { spawnProcess } from "../../utils/child-process.js";
|
|
3
|
+
export const LSP_MAX_HEADER_BYTES = 8 * 1024;
|
|
4
|
+
export const LSP_MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
export const LSP_MAX_STDERR_TAIL_BYTES = 64 * 1024;
|
|
6
|
+
const DEFAULT_INITIALIZATION_TIMEOUT_MS = 10_000;
|
|
7
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
8
|
+
const SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
9
|
+
const TERMINATE_GRACE_MS = 500;
|
|
10
|
+
const KILL_GRACE_MS = 1_000;
|
|
11
|
+
export class LspJsonRpcError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
data;
|
|
14
|
+
constructor(error) {
|
|
15
|
+
super(error.message);
|
|
16
|
+
this.name = "LspJsonRpcError";
|
|
17
|
+
this.code = error.code;
|
|
18
|
+
this.data = error.data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function isJsonRpcId(value) {
|
|
25
|
+
return typeof value === "string" || (typeof value === "number" && Number.isSafeInteger(value));
|
|
26
|
+
}
|
|
27
|
+
function normalizeError(error) {
|
|
28
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
function parseTextDocumentSync(value) {
|
|
31
|
+
let kind = "none";
|
|
32
|
+
let openClose = false;
|
|
33
|
+
let save = false;
|
|
34
|
+
let saveIncludeText = false;
|
|
35
|
+
if (typeof value === "number") {
|
|
36
|
+
kind = value === 1 ? "full" : value === 2 ? "incremental" : "none";
|
|
37
|
+
}
|
|
38
|
+
else if (isRecord(value)) {
|
|
39
|
+
openClose = value.openClose === true;
|
|
40
|
+
kind = value.change === 1 ? "full" : value.change === 2 ? "incremental" : "none";
|
|
41
|
+
if (value.save === true)
|
|
42
|
+
save = true;
|
|
43
|
+
else if (isRecord(value.save)) {
|
|
44
|
+
save = true;
|
|
45
|
+
saveIncludeText = value.save.includeText === true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { kind, openClose, save, saveIncludeText };
|
|
49
|
+
}
|
|
50
|
+
function validateTimeout(value, fallback, label) {
|
|
51
|
+
const timeout = value ?? fallback;
|
|
52
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1)
|
|
53
|
+
throw new Error(`${label} must be a positive safe integer.`);
|
|
54
|
+
return timeout;
|
|
55
|
+
}
|
|
56
|
+
function delay(ms) {
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
/** A bounded JSON-RPC 2.0 client for one language server's stdio. */
|
|
60
|
+
export class LspClient {
|
|
61
|
+
options;
|
|
62
|
+
child;
|
|
63
|
+
stdin;
|
|
64
|
+
stdout;
|
|
65
|
+
input = Buffer.alloc(0);
|
|
66
|
+
stderrTail = Buffer.alloc(0);
|
|
67
|
+
nextRequestId = 1;
|
|
68
|
+
pending = new Map();
|
|
69
|
+
notificationListeners = new Set();
|
|
70
|
+
failureListeners = new Set();
|
|
71
|
+
startPromise;
|
|
72
|
+
disposePromise;
|
|
73
|
+
state = "idle";
|
|
74
|
+
failure;
|
|
75
|
+
writeChain = Promise.resolve();
|
|
76
|
+
syncCapabilities = {
|
|
77
|
+
kind: "none",
|
|
78
|
+
openClose: false,
|
|
79
|
+
save: false,
|
|
80
|
+
saveIncludeText: false,
|
|
81
|
+
};
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.options = options;
|
|
84
|
+
}
|
|
85
|
+
get capturedStderr() {
|
|
86
|
+
return this.stderrTail.toString("utf8");
|
|
87
|
+
}
|
|
88
|
+
/** Text-document synchronization negotiated during initialization. */
|
|
89
|
+
get textDocumentSync() {
|
|
90
|
+
return this.syncCapabilities;
|
|
91
|
+
}
|
|
92
|
+
onNotification(listener) {
|
|
93
|
+
this.notificationListeners.add(listener);
|
|
94
|
+
return () => this.notificationListeners.delete(listener);
|
|
95
|
+
}
|
|
96
|
+
/** Subscribe to the first terminal protocol or process failure. */
|
|
97
|
+
onFailure(listener) {
|
|
98
|
+
this.failureListeners.add(listener);
|
|
99
|
+
if (this.failure)
|
|
100
|
+
listener(this.failure);
|
|
101
|
+
return () => this.failureListeners.delete(listener);
|
|
102
|
+
}
|
|
103
|
+
start() {
|
|
104
|
+
if (this.failure)
|
|
105
|
+
return Promise.reject(this.failure);
|
|
106
|
+
if (this.state === "closing" || this.state === "closed") {
|
|
107
|
+
return Promise.reject(new Error("Language server client is disposed."));
|
|
108
|
+
}
|
|
109
|
+
if (!this.startPromise) {
|
|
110
|
+
this.state = "starting";
|
|
111
|
+
this.startPromise = this.initialize().catch(async (error) => {
|
|
112
|
+
const failure = normalizeError(error);
|
|
113
|
+
this.fail(failure);
|
|
114
|
+
await this.terminateChild();
|
|
115
|
+
throw failure;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return this.startPromise;
|
|
119
|
+
}
|
|
120
|
+
async request(method, params, signal) {
|
|
121
|
+
await this.start();
|
|
122
|
+
if (this.state !== "ready")
|
|
123
|
+
throw this.failure ?? new Error("Language server did not become ready.");
|
|
124
|
+
return this.sendRequest(method, params, signal, this.requestTimeoutMs());
|
|
125
|
+
}
|
|
126
|
+
notify(method, params) {
|
|
127
|
+
if (this.failure)
|
|
128
|
+
throw this.failure;
|
|
129
|
+
if (this.state !== "ready")
|
|
130
|
+
throw new Error("Language server is not ready.");
|
|
131
|
+
void this.enqueueWrite({ jsonrpc: "2.0", method, ...(params === undefined ? {} : { params }) }).catch((error) => {
|
|
132
|
+
this.fail(normalizeError(error));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
dispose() {
|
|
136
|
+
if (!this.disposePromise)
|
|
137
|
+
this.disposePromise = this.disposeProcess();
|
|
138
|
+
return this.disposePromise;
|
|
139
|
+
}
|
|
140
|
+
initializationTimeoutMs() {
|
|
141
|
+
return validateTimeout(this.options.initializationTimeoutMs, DEFAULT_INITIALIZATION_TIMEOUT_MS, "initializationTimeoutMs");
|
|
142
|
+
}
|
|
143
|
+
requestTimeoutMs() {
|
|
144
|
+
return validateTimeout(this.options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, "requestTimeoutMs");
|
|
145
|
+
}
|
|
146
|
+
async initialize() {
|
|
147
|
+
const child = spawnProcess(this.options.command, [...(this.options.args ?? [])], {
|
|
148
|
+
cwd: this.options.cwd,
|
|
149
|
+
env: this.options.env,
|
|
150
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
151
|
+
});
|
|
152
|
+
if (!child.stdin || !child.stdout) {
|
|
153
|
+
child.kill();
|
|
154
|
+
throw new Error("Language server must expose piped stdin and stdout.");
|
|
155
|
+
}
|
|
156
|
+
this.child = child;
|
|
157
|
+
this.stdin = child.stdin;
|
|
158
|
+
this.stdout = child.stdout;
|
|
159
|
+
this.stdout.on("data", (chunk) => this.onData(chunk));
|
|
160
|
+
child.stderr?.on("data", (chunk) => this.captureStderr(chunk));
|
|
161
|
+
child.stdin.on("error", (error) => this.fail(error));
|
|
162
|
+
child.once("error", (error) => this.fail(error));
|
|
163
|
+
child.once("exit", (code, signal) => {
|
|
164
|
+
if (this.state !== "closing" && this.state !== "closed") {
|
|
165
|
+
this.fail(new Error(`Language server exited before shutdown (code=${code} signal=${signal}).`));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const initializeResult = await this.sendRequest("initialize", {
|
|
169
|
+
processId: process.pid,
|
|
170
|
+
clientInfo: { name: "Apex Code" },
|
|
171
|
+
rootUri: pathToFileURL(this.options.cwd).href,
|
|
172
|
+
capabilities: {
|
|
173
|
+
general: { positionEncodings: ["utf-16"] },
|
|
174
|
+
workspace: { workspaceFolders: false },
|
|
175
|
+
textDocument: {
|
|
176
|
+
definition: {},
|
|
177
|
+
references: {},
|
|
178
|
+
documentSymbol: {},
|
|
179
|
+
publishDiagnostics: { versionSupport: true },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
workspaceFolders: null,
|
|
183
|
+
}, undefined, this.initializationTimeoutMs(), "Language server initialize timed out");
|
|
184
|
+
if (!isRecord(initializeResult) || !isRecord(initializeResult.capabilities)) {
|
|
185
|
+
throw new Error("Language server returned an invalid initialize result.");
|
|
186
|
+
}
|
|
187
|
+
this.syncCapabilities = parseTextDocumentSync(initializeResult.capabilities.textDocumentSync);
|
|
188
|
+
if (this.state !== "starting")
|
|
189
|
+
throw new Error("Language server initialization was interrupted.");
|
|
190
|
+
await this.enqueueWrite({ jsonrpc: "2.0", method: "initialized", params: {} });
|
|
191
|
+
this.state = "ready";
|
|
192
|
+
}
|
|
193
|
+
async disposeProcess() {
|
|
194
|
+
if (this.state === "closed")
|
|
195
|
+
return;
|
|
196
|
+
const wasReady = this.state === "ready";
|
|
197
|
+
this.state = "closing";
|
|
198
|
+
this.rejectPending(new Error("Language server client is closing."));
|
|
199
|
+
if (wasReady) {
|
|
200
|
+
try {
|
|
201
|
+
await this.sendRequest("shutdown", undefined, undefined, SHUTDOWN_TIMEOUT_MS, "Language server shutdown timed out", true);
|
|
202
|
+
await this.enqueueWrite({ jsonrpc: "2.0", method: "exit" });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Bounded termination below is the fallback for an uncooperative server.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await this.terminateChild();
|
|
209
|
+
this.stdin?.end();
|
|
210
|
+
this.state = "closed";
|
|
211
|
+
}
|
|
212
|
+
async terminateChild() {
|
|
213
|
+
const child = this.child;
|
|
214
|
+
if (!child || child.exitCode !== null || child.signalCode !== null)
|
|
215
|
+
return;
|
|
216
|
+
const exited = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
217
|
+
const graceful = await Promise.race([exited.then(() => true), delay(TERMINATE_GRACE_MS).then(() => false)]);
|
|
218
|
+
if (graceful)
|
|
219
|
+
return;
|
|
220
|
+
child.kill("SIGTERM");
|
|
221
|
+
const terminated = await Promise.race([exited.then(() => true), delay(KILL_GRACE_MS).then(() => false)]);
|
|
222
|
+
if (terminated)
|
|
223
|
+
return;
|
|
224
|
+
child.kill("SIGKILL");
|
|
225
|
+
await exited;
|
|
226
|
+
}
|
|
227
|
+
sendRequest(method, params, signal, timeoutMs, timeoutMessage = `Language server request ${method} timed out`, allowClosing = false) {
|
|
228
|
+
if (this.failure && !allowClosing)
|
|
229
|
+
return Promise.reject(this.failure);
|
|
230
|
+
if ((!allowClosing && (this.state === "closing" || this.state === "closed")) || this.state === "failed") {
|
|
231
|
+
return Promise.reject(this.failure ?? new Error("Language server client is disposed."));
|
|
232
|
+
}
|
|
233
|
+
const id = this.nextRequestId++;
|
|
234
|
+
if (!Number.isSafeInteger(id))
|
|
235
|
+
return Promise.reject(new Error("Language server request id space exhausted."));
|
|
236
|
+
if (signal?.aborted)
|
|
237
|
+
return Promise.reject(normalizeError(signal.reason ?? new Error("Language server request aborted.")));
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
let timer;
|
|
240
|
+
const settle = (fn) => {
|
|
241
|
+
if (timer)
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
signal?.removeEventListener("abort", abort);
|
|
244
|
+
fn();
|
|
245
|
+
};
|
|
246
|
+
const abort = () => {
|
|
247
|
+
if (!this.pending.delete(id))
|
|
248
|
+
return;
|
|
249
|
+
void this.enqueueWrite({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } }).catch(() => { });
|
|
250
|
+
settle(() => reject(normalizeError(signal?.reason ?? new Error("Language server request aborted."))));
|
|
251
|
+
};
|
|
252
|
+
this.pending.set(id, {
|
|
253
|
+
resolve: (value) => settle(() => resolve(value)),
|
|
254
|
+
reject: (error) => settle(() => reject(error)),
|
|
255
|
+
});
|
|
256
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
257
|
+
if (timeoutMs !== undefined) {
|
|
258
|
+
timer = setTimeout(() => {
|
|
259
|
+
if (!this.pending.delete(id))
|
|
260
|
+
return;
|
|
261
|
+
void this.enqueueWrite({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } }).catch(() => { });
|
|
262
|
+
settle(() => reject(new Error(`${timeoutMessage} after ${timeoutMs}ms.`)));
|
|
263
|
+
}, timeoutMs);
|
|
264
|
+
}
|
|
265
|
+
void this.enqueueWrite({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) }).catch((error) => {
|
|
266
|
+
const pending = this.pending.get(id);
|
|
267
|
+
if (!pending)
|
|
268
|
+
return;
|
|
269
|
+
this.pending.delete(id);
|
|
270
|
+
pending.reject(normalizeError(error));
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
enqueueWrite(message) {
|
|
275
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
276
|
+
if (body.length > LSP_MAX_MESSAGE_BYTES)
|
|
277
|
+
return Promise.reject(new Error("Language server message exceeds 8 MiB."));
|
|
278
|
+
const frame = Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]);
|
|
279
|
+
const writing = this.writeChain.then(() => this.writeFrame(frame));
|
|
280
|
+
this.writeChain = writing.catch(() => { });
|
|
281
|
+
return writing;
|
|
282
|
+
}
|
|
283
|
+
writeFrame(frame) {
|
|
284
|
+
const stream = this.stdin;
|
|
285
|
+
if (!stream || stream.destroyed || !stream.writable) {
|
|
286
|
+
return Promise.reject(new Error("Language server stdin is not writable."));
|
|
287
|
+
}
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
let waitingForDrain = false;
|
|
290
|
+
const cleanup = () => {
|
|
291
|
+
stream.removeListener("error", onError);
|
|
292
|
+
stream.removeListener("drain", onDrain);
|
|
293
|
+
};
|
|
294
|
+
const onError = (error) => {
|
|
295
|
+
cleanup();
|
|
296
|
+
reject(error);
|
|
297
|
+
};
|
|
298
|
+
const onDrain = () => {
|
|
299
|
+
waitingForDrain = false;
|
|
300
|
+
cleanup();
|
|
301
|
+
resolve();
|
|
302
|
+
};
|
|
303
|
+
stream.once("error", onError);
|
|
304
|
+
const accepted = stream.write(frame, (error) => {
|
|
305
|
+
if (error)
|
|
306
|
+
onError(error);
|
|
307
|
+
else if (!waitingForDrain) {
|
|
308
|
+
cleanup();
|
|
309
|
+
resolve();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
if (!accepted) {
|
|
313
|
+
waitingForDrain = true;
|
|
314
|
+
stream.once("drain", onDrain);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
onData(chunk) {
|
|
319
|
+
if (this.failure)
|
|
320
|
+
return;
|
|
321
|
+
this.input = Buffer.concat([this.input, typeof chunk === "string" ? Buffer.from(chunk) : chunk]);
|
|
322
|
+
while (true) {
|
|
323
|
+
const headerEnd = this.input.indexOf("\r\n\r\n");
|
|
324
|
+
if (headerEnd < 0) {
|
|
325
|
+
if (this.input.length > LSP_MAX_HEADER_BYTES)
|
|
326
|
+
this.fail(new Error("Language server header exceeds 8 KiB."));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (headerEnd > LSP_MAX_HEADER_BYTES) {
|
|
330
|
+
this.fail(new Error("Language server header exceeds 8 KiB."));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const header = this.input.subarray(0, headerEnd).toString("ascii");
|
|
334
|
+
const lengths = [...header.matchAll(/(?:^|\r\n)Content-Length:\s*(\d+)(?=\r\n|$)/gi)];
|
|
335
|
+
if (lengths.length !== 1) {
|
|
336
|
+
this.fail(new Error("Language server frame must contain exactly one Content-Length header."));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const contentLength = Number(lengths[0][1]);
|
|
340
|
+
if (!Number.isSafeInteger(contentLength) || contentLength > LSP_MAX_MESSAGE_BYTES) {
|
|
341
|
+
this.fail(new Error("Language server message exceeds 8 MiB."));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const bodyStart = headerEnd + 4;
|
|
345
|
+
if (this.input.length < bodyStart + contentLength)
|
|
346
|
+
return;
|
|
347
|
+
const body = this.input.subarray(bodyStart, bodyStart + contentLength).toString("utf8");
|
|
348
|
+
this.input = this.input.subarray(bodyStart + contentLength);
|
|
349
|
+
try {
|
|
350
|
+
const parsed = JSON.parse(body);
|
|
351
|
+
if (!isRecord(parsed))
|
|
352
|
+
throw new Error("Language server JSON-RPC message must be an object.");
|
|
353
|
+
this.onMessage(parsed);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
this.fail(normalizeError(error));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
onMessage(message) {
|
|
362
|
+
if (message.jsonrpc !== "2.0") {
|
|
363
|
+
this.fail(new Error('Language server message must declare jsonrpc "2.0".'));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (typeof message.method === "string") {
|
|
367
|
+
if (message.id !== undefined && message.id !== null) {
|
|
368
|
+
void this.handleServerRequest(message);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const notification = {
|
|
372
|
+
method: message.method,
|
|
373
|
+
...(message.params === undefined ? {} : { params: message.params }),
|
|
374
|
+
};
|
|
375
|
+
for (const listener of this.notificationListeners)
|
|
376
|
+
listener(notification);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (typeof message.id !== "number" || !Number.isSafeInteger(message.id))
|
|
380
|
+
return;
|
|
381
|
+
const pending = this.pending.get(message.id);
|
|
382
|
+
if (!pending)
|
|
383
|
+
return;
|
|
384
|
+
const hasResult = Object.hasOwn(message, "result");
|
|
385
|
+
const hasError = Object.hasOwn(message, "error");
|
|
386
|
+
if (hasResult === hasError) {
|
|
387
|
+
this.fail(new Error("Language server response must contain exactly one of result or error."));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.pending.delete(message.id);
|
|
391
|
+
if (hasError) {
|
|
392
|
+
const error = message.error;
|
|
393
|
+
if (!isRecord(error) || !Number.isSafeInteger(error.code) || typeof error.message !== "string") {
|
|
394
|
+
this.fail(new Error("Language server returned an invalid JSON-RPC error."));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
pending.reject(new LspJsonRpcError({ code: error.code, message: error.message, data: error.data }));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
pending.resolve(message.result);
|
|
401
|
+
}
|
|
402
|
+
async handleServerRequest(message) {
|
|
403
|
+
if (!isJsonRpcId(message.id) || typeof message.method !== "string")
|
|
404
|
+
return;
|
|
405
|
+
let result;
|
|
406
|
+
let error;
|
|
407
|
+
switch (message.method) {
|
|
408
|
+
case "workspace/configuration": {
|
|
409
|
+
const params = message.params;
|
|
410
|
+
if (!isRecord(params) || !Array.isArray(params.items))
|
|
411
|
+
error = { code: -32602, message: "Invalid params" };
|
|
412
|
+
else
|
|
413
|
+
result = params.items.map(() => null);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "workspace/workspaceFolders":
|
|
417
|
+
case "window/showMessageRequest":
|
|
418
|
+
result = null;
|
|
419
|
+
break;
|
|
420
|
+
case "client/registerCapability":
|
|
421
|
+
case "client/unregisterCapability":
|
|
422
|
+
case "window/workDoneProgress/create":
|
|
423
|
+
case "workspace/semanticTokens/refresh":
|
|
424
|
+
case "workspace/inlayHint/refresh":
|
|
425
|
+
case "workspace/diagnostic/refresh":
|
|
426
|
+
case "workspace/codeLens/refresh":
|
|
427
|
+
result = null;
|
|
428
|
+
break;
|
|
429
|
+
case "workspace/applyEdit":
|
|
430
|
+
result = { applied: false, failureReason: "Apex Code does not accept server-initiated workspace edits." };
|
|
431
|
+
break;
|
|
432
|
+
case "window/showDocument":
|
|
433
|
+
result = { success: false };
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
error = { code: -32601, message: "Method not found" };
|
|
437
|
+
}
|
|
438
|
+
await this.enqueueWrite({
|
|
439
|
+
jsonrpc: "2.0",
|
|
440
|
+
id: message.id,
|
|
441
|
+
...(error ? { error } : { result }),
|
|
442
|
+
}).catch((writeError) => this.fail(normalizeError(writeError)));
|
|
443
|
+
}
|
|
444
|
+
captureStderr(chunk) {
|
|
445
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
446
|
+
this.stderrTail = Buffer.concat([this.stderrTail, bytes]);
|
|
447
|
+
if (this.stderrTail.length > LSP_MAX_STDERR_TAIL_BYTES) {
|
|
448
|
+
this.stderrTail = this.stderrTail.subarray(this.stderrTail.length - LSP_MAX_STDERR_TAIL_BYTES);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
rejectPending(error) {
|
|
452
|
+
for (const request of this.pending.values())
|
|
453
|
+
request.reject(error);
|
|
454
|
+
this.pending.clear();
|
|
455
|
+
}
|
|
456
|
+
fail(error) {
|
|
457
|
+
if (this.state === "closed")
|
|
458
|
+
return;
|
|
459
|
+
const firstFailure = this.failure === undefined;
|
|
460
|
+
this.failure ??= error;
|
|
461
|
+
if (this.state !== "closing")
|
|
462
|
+
this.state = "failed";
|
|
463
|
+
this.rejectPending(this.failure);
|
|
464
|
+
if (firstFailure)
|
|
465
|
+
for (const listener of this.failureListeners)
|
|
466
|
+
listener(this.failure);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
//# sourceMappingURL=client.js.map
|