🔀 Merge chore/protocol-backed-lsp-helper into main
This commit is contained in:
commit
62c37e6d89
5 files changed
+158
-188
No files matched your search
+8
-1
@@ -32,6 +32,11 @@ Testing:
|
|||||||
→ `src/util/__tests__/lsp-completion.ts` (`#test-utils/…`) is the helper; the suite asserts its labels (see development/testing.md § Autocomplete)
|
→ `src/util/__tests__/lsp-completion.ts` (`#test-utils/…`) is the helper; the suite asserts its labels (see development/testing.md § Autocomplete)
|
||||||
→ wire it into `node --test` so a test asserts the offered labels
|
→ wire it into `node --test` so a test asserts the offered labels
|
||||||
→ note: `Parameters<typeof factory>[0]` resolves only the *last* overload; use `@ts-expect-error` call sites for factory negatives, not `not.toExtend<Parameters<…>>`
|
→ note: `Parameters<typeof factory>[0]` resolves only the *last* overload; use `@ts-expect-error` call sites for factory negatives, not `not.toExtend<Parameters<…>>`
|
||||||
|
✔ Drive the LSP completion helper through the LSP protocol library instead of a hand-rolled JSON-RPC client @medium @done
|
||||||
|
✔ Add `vscode-languageserver-protocol` and record the tooling decision @done
|
||||||
|
✔ Rewrite `src/util/__tests__/lsp-completion.ts` onto `createMessageConnection` and typed requests @done
|
||||||
|
✔ Update `development/testing.md § Autocomplete` for the new client @done
|
||||||
|
✔ Run `npm run verify` and check the task off @done
|
||||||
|
|
||||||
Matcher:
|
Matcher:
|
||||||
✔ Clean up: adopt the 3-overload matcher (`src/prototype-ac2.ts`) and delete the prototypes @high @done
|
✔ Clean up: adopt the 3-overload matcher (`src/prototype-ac2.ts`) and delete the prototypes @high @done
|
||||||
@@ -43,7 +48,9 @@ Matcher:
|
|||||||
→ `{ a, b, _ }` for `T = "a" | "b"` is accepted today; the redundant `_` should be rejected
|
→ `{ a, b, _ }` for `T = "a" | "b"` is accepted today; the redundant `_` should be rejected
|
||||||
|
|
||||||
Bugs:
|
Bugs:
|
||||||
|
✔ TS 7 LSP server logs `context canceled` on stderr at shutdown @done
|
||||||
|
→ `handleExit` returns `io.EOF`, cancelling the background context while `Session.updateWatches` is still in flight; the bare error is flushed to stderr and the server exits 1
|
||||||
|
→ close stdin after `shutdown` instead of sending `exit`; the server exits cleanly (code 0, no output), kill kept as a fallback
|
||||||
Enhancements:
|
Enhancements:
|
||||||
☐ Allow boolean literals in primitive union patterns (e.g. `true: () => "yes"`) @medium
|
☐ Allow boolean literals in primitive union patterns (e.g. `true: () => "yes"`) @medium
|
||||||
☐ Are there other primitive types that should be supported in union patterns? (e.g. `bigint`, `symbol`) @medium
|
☐ Are there other primitive types that should be supported in union patterns? (e.g. `bigint`, `symbol`) @medium
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ in [package.json](../package.json).
|
|||||||
- **@spences10/pi-lsp** — read-only LSP code intelligence for AI agents
|
- **@spences10/pi-lsp** — read-only LSP code intelligence for AI agents
|
||||||
(project-local `.pi/settings.json`); talks to this repo's TypeScript 7 via
|
(project-local `.pi/settings.json`); talks to this repo's TypeScript 7 via
|
||||||
`tsc --lsp --stdio`.
|
`tsc --lsp --stdio`.
|
||||||
|
- **vscode-languageserver-protocol** — LSP client and protocol types for the
|
||||||
|
autocomplete test helper (`src/util/__tests__/lsp-completion.ts`).
|
||||||
|
|
||||||
When each runs is in
|
When each runs is in
|
||||||
[CONTRIBUTING.md § Feedback tiers](../CONTRIBUTING.md#feedback-tiers).
|
[CONTRIBUTING.md § Feedback tiers](../CONTRIBUTING.md#feedback-tiers).
|
||||||
@@ -247,6 +249,38 @@ project.
|
|||||||
- The same script works by hand (whole project) and staged (scoped), so there is
|
- The same script works by hand (whole project) and staged (scoped), so there is
|
||||||
no second command to maintain.
|
no second command to maintain.
|
||||||
|
|
||||||
|
## Language server tooling
|
||||||
|
|
||||||
|
### `vscode-languageserver-protocol` backs the autocomplete helper
|
||||||
|
|
||||||
|
#### Decision (2026-09)
|
||||||
|
|
||||||
|
The autocomplete helper (`src/util/__tests__/lsp-completion.ts`) drives
|
||||||
|
`tsc --lsp --stdio` through `vscode-languageserver-protocol`'s
|
||||||
|
`createMessageConnection` and its typed request / notification objects, instead
|
||||||
|
of a hand-rolled JSON-RPC client.
|
||||||
|
|
||||||
|
#### Why
|
||||||
|
|
||||||
|
- Framing, `Content-Length` parsing, the pending-request map and server-request
|
||||||
|
dispatch are protocol plumbing the helper only reimplemented; the official
|
||||||
|
client owns them and tolerates the server's `string | number` ids.
|
||||||
|
- `InitializeRequest`, `CompletionRequest`, `DidOpenTextDocumentNotification`,
|
||||||
|
… carry their parameter and result types, so `CompletionList` / `CompletionItem`
|
||||||
|
replace the helper's ad-hoc shape guards.
|
||||||
|
- The `./node` entry re-exports `vscode-jsonrpc/node`, so one devDependency
|
||||||
|
supplies both the transport and the protocol types. It is test-only and never
|
||||||
|
ships (`files` publishes `dist/` only).
|
||||||
|
|
||||||
|
#### Rejected
|
||||||
|
|
||||||
|
- `vscode-languageclient`: the editor-side client with a full feature registry
|
||||||
|
— far more than a test helper needs.
|
||||||
|
- Generic JSON-RPC (`jsonrpc-lite`, `jayson`): still no LSP types, so they
|
||||||
|
replace framing only and leave the typed protocol surface unimplemented.
|
||||||
|
- Keeping the hand-rolled client: the low-level shape is the maintenance cost
|
||||||
|
the helper exists to remove, and it must be re-audited against the server.
|
||||||
|
|
||||||
## Editor and agent tooling
|
## Editor and agent tooling
|
||||||
|
|
||||||
### VSCode integration
|
### VSCode integration
|
||||||
|
|||||||
Generated
+30
-1
@@ -27,7 +27,8 @@
|
|||||||
"oxlint": "^1.83.0",
|
"oxlint": "^1.83.0",
|
||||||
"oxlint-tsgolint": "^7.0.2001",
|
"oxlint-tsgolint": "^7.0.2001",
|
||||||
"publint": "^0.3.24",
|
"publint": "^0.3.24",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2",
|
||||||
|
"vscode-languageserver-protocol": "^3.18.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=26"
|
"node": ">=26"
|
||||||
@@ -4822,6 +4823,27 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-SbQSV9yRemARxeXw6LU5sS6Zq0e9/DgCCX5yelH263ZQWukbTk8EF8fjTrr1dziasf4GwlJbvTwFnTrnQFWZXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol": {
|
||||||
|
"version": "3.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.3.tgz",
|
||||||
|
"integrity": "sha512-DF49+WeV5py4zO5hhobp60jjsDSK0lAqA0OuKBLBvp423HPWQcCbhZz3JgyfIewsEz2f8U+X75xNIFHdiXZm2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "9.0.2",
|
||||||
|
"vscode-languageserver-types": "3.18.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vscode-languageserver-textdocument": {
|
"node_modules/vscode-languageserver-textdocument": {
|
||||||
"version": "1.0.14",
|
"version": "1.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz",
|
||||||
@@ -4829,6 +4851,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
"version": "3.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.3.tgz",
|
||||||
|
"integrity": "sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vscode-uri": {
|
"node_modules/vscode-uri": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -87,7 +87,8 @@
|
|||||||
"oxlint": "^1.83.0",
|
"oxlint": "^1.83.0",
|
||||||
"oxlint-tsgolint": "^7.0.2001",
|
"oxlint-tsgolint": "^7.0.2001",
|
||||||
"publint": "^0.3.24",
|
"publint": "^0.3.24",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2",
|
||||||
|
"vscode-languageserver-protocol": "^3.18.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=26"
|
"node": ">=26"
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
|
// oxlint-disable no-magic-numbers unicorn/no-null - tolerable here, this is a helper
|
||||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CompletionItem,
|
||||||
|
type CompletionList,
|
||||||
|
CompletionRequest,
|
||||||
|
ConfigurationRequest,
|
||||||
|
createMessageConnection,
|
||||||
|
DidOpenTextDocumentNotification,
|
||||||
|
InitializedNotification,
|
||||||
|
InitializeRequest,
|
||||||
|
type MessageConnection,
|
||||||
|
ShutdownRequest,
|
||||||
|
StreamMessageReader,
|
||||||
|
StreamMessageWriter,
|
||||||
|
} from "vscode-languageserver-protocol/node";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print the completion labels TypeScript's language server offers at a marker
|
* Print the completion labels TypeScript's language server offers at a marker
|
||||||
* inside a source file.
|
* inside a source file.
|
||||||
@@ -48,71 +64,18 @@ export interface CompletionResult {
|
|||||||
readonly labels: readonly string[];
|
readonly labels: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompletionItem {
|
const completionLabels = (
|
||||||
readonly label: string;
|
result: CompletionItem[] | CompletionList | null,
|
||||||
}
|
): readonly string[] => {
|
||||||
|
if (result === null) {
|
||||||
interface RpcMessage {
|
|
||||||
readonly id?: string | number;
|
|
||||||
readonly method?: string;
|
|
||||||
readonly params?: unknown;
|
|
||||||
readonly result?: unknown;
|
|
||||||
readonly error?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Deferred {
|
|
||||||
readonly resolve: (message: RpcMessage) => void;
|
|
||||||
readonly reject: (error: Error) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
typeof value === "object" && value !== null;
|
|
||||||
|
|
||||||
const isRpcMessage = (value: unknown): value is RpcMessage => {
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const { id, method } = value;
|
|
||||||
return (
|
|
||||||
(id === undefined ||
|
|
||||||
typeof id === "number" ||
|
|
||||||
typeof id === "string") &&
|
|
||||||
(method === undefined || typeof method === "string")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCompletionItem = (value: unknown): value is CompletionItem =>
|
|
||||||
isRecord(value) && typeof value["label"] === "string";
|
|
||||||
|
|
||||||
const serverRequestResult = (message: RpcMessage): readonly null[] | null => {
|
|
||||||
if (message.method !== "workspace/configuration") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const items = isRecord(message.params)
|
|
||||||
? message.params["items"]
|
|
||||||
: undefined;
|
|
||||||
return Array.isArray(items) ? items.map(() => null) : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const completionLabels = (result: unknown): readonly string[] => {
|
|
||||||
if (!isRecord(result)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const { items } = result;
|
|
||||||
if (!Array.isArray(items)) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const items = Array.isArray(result) ? result : result.items;
|
||||||
return items
|
return items
|
||||||
.filter(isCompletionItem)
|
|
||||||
.map((item) => item.label)
|
.map((item) => item.label)
|
||||||
.toSorted((left, right) => left.localeCompare(right));
|
.toSorted((left, right) => left.localeCompare(right));
|
||||||
};
|
};
|
||||||
|
|
||||||
const frame = (message: object): string => {
|
|
||||||
const body = JSON.stringify(message);
|
|
||||||
return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const markerPosition = (source: string, marker: string): Position => {
|
const markerPosition = (source: string, marker: string): Position => {
|
||||||
const index = source.indexOf(marker);
|
const index = source.indexOf(marker);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
@@ -124,153 +87,86 @@ const markerPosition = (source: string, marker: string): Position => {
|
|||||||
return { line: lineIndex, character: line === undefined ? 0 : line.length };
|
return { line: lineIndex, character: line === undefined ? 0 : line.length };
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Minimal JSON-RPC client for `tsc --lsp --stdio`. */
|
|
||||||
class LspClient {
|
|
||||||
readonly #child: ChildProcessWithoutNullStreams;
|
|
||||||
readonly #pending = new Map<string | number, Deferred>();
|
|
||||||
#buffer = Buffer.alloc(0);
|
|
||||||
#nextId = 1;
|
|
||||||
|
|
||||||
public constructor(repoRoot: string) {
|
|
||||||
this.#child = spawn(
|
|
||||||
path.join(repoRoot, "node_modules", ".bin", "tsc"),
|
|
||||||
["--lsp", "--stdio"],
|
|
||||||
{ cwd: repoRoot },
|
|
||||||
);
|
|
||||||
this.#child.stdout.on("data", (chunk: Buffer) => {
|
|
||||||
this.#consume(chunk);
|
|
||||||
});
|
|
||||||
this.#child.stderr.on("data", (chunk: Buffer) => {
|
|
||||||
process.stderr.write(chunk);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send a request and settle when the server answers it. */
|
|
||||||
public request(method: string, params?: unknown): Promise<RpcMessage> {
|
|
||||||
const id = this.#nextId;
|
|
||||||
this.#nextId += 1;
|
|
||||||
const message = frame({ jsonrpc: "2.0", id, method, params });
|
|
||||||
return new Promise<RpcMessage>((resolve, reject) => {
|
|
||||||
this.#pending.set(id, { resolve, reject });
|
|
||||||
this.#child.stdin.write(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public notify(method: string, params: unknown): void {
|
|
||||||
this.#child.stdin.write(frame({ jsonrpc: "2.0", method, params }));
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
this.#child.stdin.end();
|
|
||||||
this.#child.kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
#consume(chunk: Buffer): void {
|
|
||||||
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
||||||
for (;;) {
|
|
||||||
const headerEnd = this.#buffer.indexOf("\r\n\r\n");
|
|
||||||
if (headerEnd === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const header = this.#buffer.subarray(0, headerEnd).toString();
|
|
||||||
const match = /Content-Length: (\d+)/i.exec(header);
|
|
||||||
const lengthText = match === null ? undefined : match[1];
|
|
||||||
if (lengthText === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bodyEnd = headerEnd + 4 + Number(lengthText);
|
|
||||||
if (this.#buffer.length < bodyEnd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const body = this.#buffer
|
|
||||||
.subarray(headerEnd + 4, bodyEnd)
|
|
||||||
.toString();
|
|
||||||
this.#buffer = this.#buffer.subarray(bodyEnd);
|
|
||||||
const parsed: unknown = JSON.parse(body);
|
|
||||||
if (isRpcMessage(parsed)) {
|
|
||||||
this.#handle(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#handle(message: RpcMessage): void {
|
|
||||||
// Server -> client request: answer so the server does not stall.
|
|
||||||
if (message.method !== undefined && message.id !== undefined) {
|
|
||||||
this.#child.stdin.write(
|
|
||||||
frame({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: message.id,
|
|
||||||
result: serverRequestResult(message),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Response to one of our requests.
|
|
||||||
const { id } = message;
|
|
||||||
if (id === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const deferred = this.#pending.get(id);
|
|
||||||
if (deferred === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#pending.delete(id);
|
|
||||||
if (message.error === undefined) {
|
|
||||||
deferred.resolve(message);
|
|
||||||
} else {
|
|
||||||
deferred.reject(new Error(JSON.stringify(message.error)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One language server, initialized on first use, shared across probes. Callers
|
* One language server, initialized on first use, shared across probes. Callers
|
||||||
* own the lifecycle and must `close()` it.
|
* own the lifecycle and must `close()` it.
|
||||||
*/
|
*/
|
||||||
export class LspSession {
|
export class LspSession {
|
||||||
readonly #client: LspClient;
|
readonly #child: ChildProcessWithoutNullStreams;
|
||||||
|
readonly #connection: MessageConnection;
|
||||||
readonly #repoRoot: string;
|
readonly #repoRoot: string;
|
||||||
#ready: Promise<void> | undefined;
|
#ready: Promise<void> | undefined;
|
||||||
|
|
||||||
public constructor(repoRoot: string) {
|
public constructor(repoRoot: string) {
|
||||||
this.#repoRoot = repoRoot;
|
this.#repoRoot = repoRoot;
|
||||||
this.#client = new LspClient(repoRoot);
|
this.#child = spawn(
|
||||||
|
path.join(repoRoot, "node_modules", ".bin", "tsc"),
|
||||||
|
["--lsp", "--stdio"],
|
||||||
|
{ cwd: repoRoot },
|
||||||
|
);
|
||||||
|
this.#child.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
process.stderr.write(chunk);
|
||||||
|
});
|
||||||
|
this.#connection = createMessageConnection(
|
||||||
|
new StreamMessageReader(this.#child.stdout),
|
||||||
|
new StreamMessageWriter(this.#child.stdin),
|
||||||
|
);
|
||||||
|
// Server -> client requests the server waits on: answer so it proceeds.
|
||||||
|
// `workspace/configuration` wants one reply per requested item; a
|
||||||
|
// catch-all covers the rest (e.g. `client/registerCapability`).
|
||||||
|
this.#connection.onRequest(ConfigurationRequest.type, ({ items }) =>
|
||||||
|
items.map(() => null),
|
||||||
|
);
|
||||||
|
this.#connection.onRequest(() => null);
|
||||||
|
this.#connection.listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
public completionLabelsAt(
|
public completionLabelsAt(
|
||||||
target: CompletionTarget,
|
target: CompletionTarget,
|
||||||
): Promise<CompletionResult> {
|
): Promise<CompletionResult> {
|
||||||
return this.#ensureInitialized().then(() => {
|
return this.#ensureInitialized()
|
||||||
const { uri, position } = this.#open(target);
|
.then(() => this.#open(target))
|
||||||
return this.#client
|
.then(({ uri, position }) =>
|
||||||
.request("textDocument/completion", {
|
this.#connection
|
||||||
|
.sendRequest(CompletionRequest.type, {
|
||||||
textDocument: { uri },
|
textDocument: { uri },
|
||||||
position,
|
position,
|
||||||
context: { triggerKind: 1 },
|
context: { triggerKind: 1 },
|
||||||
})
|
})
|
||||||
.then((response) => ({
|
.then((result) => ({
|
||||||
position,
|
position,
|
||||||
labels: completionLabels(response.result),
|
labels: completionLabels(result),
|
||||||
}));
|
})),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `shutdown` + `exit`, then kill the server; safe after a failed probe. */
|
/** `shutdown` + close stdin, then kill the server; safe after a failed probe. */
|
||||||
public close(): Promise<void> {
|
public close(): Promise<void> {
|
||||||
return this.#client
|
return (
|
||||||
.request("shutdown")
|
this.#connection
|
||||||
|
.sendRequest(ShutdownRequest.type)
|
||||||
|
// The TS 7 Go server logs a bare `context canceled` to stderr
|
||||||
|
// when it handles `exit`; EOF on stdin shuts it down cleanly
|
||||||
|
// (exit 0, no output) instead.
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.#client.notify("exit", null);
|
this.#child.stdin.end();
|
||||||
return delay(EXIT_DELAY_MS);
|
|
||||||
})
|
})
|
||||||
|
.then(() => delay(EXIT_DELAY_MS))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.#client.dispose();
|
this.#connection.dispose();
|
||||||
});
|
this.#child.kill();
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ensureInitialized(): Promise<void> {
|
#ensureInitialized(): Promise<void> {
|
||||||
this.#ready ??= this.#client
|
this.#ready ??= this.#initialize();
|
||||||
.request("initialize", {
|
return this.#ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize(): Promise<void> {
|
||||||
|
return this.#connection
|
||||||
|
.sendRequest(InitializeRequest.type, {
|
||||||
processId: process.pid,
|
processId: process.pid,
|
||||||
rootUri: pathToFileURL(this.#repoRoot).href,
|
rootUri: pathToFileURL(this.#repoRoot).href,
|
||||||
workspaceFolders: [
|
workspaceFolders: [
|
||||||
@@ -285,16 +181,18 @@ export class LspSession {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() =>
|
||||||
this.#client.notify("initialized", {});
|
this.#connection.sendNotification(
|
||||||
});
|
InitializedNotification.type,
|
||||||
return this.#ready;
|
{},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#open(target: CompletionTarget): {
|
#open(target: CompletionTarget): Promise<{
|
||||||
readonly uri: string;
|
readonly uri: string;
|
||||||
readonly position: Position;
|
readonly position: Position;
|
||||||
} {
|
}> {
|
||||||
const absolute = path.resolve(this.#repoRoot, target.file);
|
const absolute = path.resolve(this.#repoRoot, target.file);
|
||||||
const marker = target.marker ?? DEFAULT_MARKER;
|
const marker = target.marker ?? DEFAULT_MARKER;
|
||||||
const position = markerPosition(target.source, marker);
|
const position = markerPosition(target.source, marker);
|
||||||
@@ -302,15 +200,16 @@ export class LspSession {
|
|||||||
const uri = pathToFileURL(absolute).href;
|
const uri = pathToFileURL(absolute).href;
|
||||||
// The server handles `didOpen` in order before the completion request,
|
// The server handles `didOpen` in order before the completion request,
|
||||||
// so no settle delay is needed.
|
// so no settle delay is needed.
|
||||||
this.#client.notify("textDocument/didOpen", {
|
return this.#connection
|
||||||
|
.sendNotification(DidOpenTextDocumentNotification.type, {
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri,
|
uri,
|
||||||
languageId: "typescript",
|
languageId: "typescript",
|
||||||
version: 1,
|
version: 1,
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
return { uri, position };
|
.then(() => ({ uri, position }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user