Compare commits
7
Commits
ac04a87e53
...
241a5ba417
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
241a5ba417 | ||
|
|
f91996ddf6 | ||
|
|
6a9318568c | ||
|
|
12b2f73596 | ||
|
|
ad5c0fa91c | ||
|
|
b028ba8e03 | ||
|
|
942074cf85 |
No files matched your search
+2
-1
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
node_modules/
|
||||
dist/
|
||||
repos/
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
An MCP server that exposes one Language Server to agents as a headless editor.
|
||||
Effect 4 (`effect/unstable/ai` for MCP, `effect/unstable/cli` for the CLI) plus
|
||||
`vscode-languageserver-protocol` for LSP. TypeScript runs directly on Node 26
|
||||
(type stripping); there is no build step.
|
||||
`vscode-languageserver-protocol` for LSP. TypeScript is bundled for Node 26 with
|
||||
tsdown; source files can still run directly through Node's type stripping.
|
||||
|
||||
## Commands
|
||||
|
||||
- `pnpm tsc` — type-check (tsgo with the Effect language-service plugin)
|
||||
- `pnpm build` — bundle the CLI to `dist/main.mjs` with tsdown
|
||||
- `pnpm dev` — run the TypeScript source directly
|
||||
- `pnpm test` — vitest; integration tests spawn `test/fixtures/server.ts`, a real LSP server
|
||||
- `pnpm biome check --write --unsafe` — lint/format (run before committing; its rewrites are safe)
|
||||
- `node src/main.ts --help` — the CLI; smoke-test with `node src/main.ts --root <dir> -- <server> [args]`
|
||||
- `node dist/main.mjs --help` — the built CLI; smoke-test with `node dist/main.mjs --root <dir> -- <server> [args]`
|
||||
- Commit with `jj commit -m "..."`, never `git`
|
||||
|
||||
The shell may have an HTTP proxy configured; when curling the `--http` transport locally, pass `--noproxy '*'`.
|
||||
@@ -24,7 +26,8 @@ src/Resources.ts MCP resources (lsp://server, lsp://diagnostics[/{file}]) a
|
||||
src/Server.ts layer composition: LanguageServer → Documents+Diagnostics → transport → tools/resources
|
||||
src/main.ts CLI entry (Command/Flag/Argument), logs to stderr, NodeRuntime.runMain
|
||||
src/lsp/ the ONLY place vscode-* packages are used at runtime
|
||||
LanguageServer.ts spawn + JSON-RPC + initialize/shutdown; request() waits for $/progress idle and retries ContentModified
|
||||
LanguageServer.ts spawn + JSON-RPC + initialize/shutdown, supervised: restarts on crash per policy, reopens documents
|
||||
request() waits for $/progress idle and retries ContentModified / a server that is being restarted
|
||||
Documents.ts on-demand didOpen, LRU via ScopedCache, disk change detection → didChange+didSave, applyEdit
|
||||
Diagnostics.ts publishDiagnostics accumulation; pull (textDocument/diagnostic) or settled push per doc version
|
||||
Editor.ts the operations = MCP tool handlers; LSP results → Model values
|
||||
@@ -34,6 +37,9 @@ test/fixtures/workspace.ts temp workspace layer running the fixture server
|
||||
|
||||
Rule: outside `src/lsp/`, `vscode-*` may only be imported as types.
|
||||
|
||||
`EFFECT.md` lists the Effect 4 API shapes already verified against `repos/effect` (renames from Effect 3, Schedule,
|
||||
CLI, MCP, vitest). Read it before grepping the Effect source, and add to it when you verify something new.
|
||||
|
||||
## Design decisions worth knowing
|
||||
|
||||
- Positions handed to agents are 1-based; `Where` accepts `column` or a `symbol` name to find on the line.
|
||||
@@ -48,6 +54,12 @@ Rule: outside `src/lsp/`, `vscode-*` may only be imported as types.
|
||||
plus a 250ms quiet period, bounded by 5s, memoized per (uri, version).
|
||||
- Tests that talk to the fixture server use `it.layer(..., { excludeTestServices: true })` so the real
|
||||
clock is used (Stream.debounce / timeouts would hang under TestClock).
|
||||
- Crashes: `--restart never|always|N` (default 3). A crash is the process exiting or closing the connection after a
|
||||
successful initialize; a server that cannot initialize fails lsp-mcp immediately. Restarts back off from 500ms to 30s,
|
||||
a crash more than a minute after the previous one starts a new streak, and the streak limit `N` ends restarting for good.
|
||||
Requests in flight during a crash wait for the new process and retry; documents are re-`didOpen`ed with their current
|
||||
text; the capabilities and tool set stay those of the first process. Each restart and the final give-up are logged and
|
||||
sent to the MCP client as `notifications/message`. Once given up, every tool call fails with an `LspError` that says so.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -56,5 +68,4 @@ verified against the fixture server and `typescript-language-server`. `goal.md`
|
||||
no longer authoritative; the code is.
|
||||
|
||||
Ideas not done: inlay hints, type hierarchy, semantic tokens, partial results, workspace/didChangeWatchedFiles
|
||||
when a server insists on it, restarting a crashed language server (currently requests fail with a clear
|
||||
`LspError` until the MCP client restarts the process).
|
||||
when a server insists on it.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Effect 4 notes for this repo
|
||||
|
||||
Effect `4.0.0-rc.115` (pinned in `package.json`; `repos/effect` is the same version, check
|
||||
`repos/effect/packages/effect/package.json` if either is bumped). The API differs from Effect 3
|
||||
in places, and the type stripper gives no hints, so agents kept re-verifying the same shapes.
|
||||
This file records what has been verified against the source so the next agent does not have to.
|
||||
|
||||
When something is not listed here, grep the source, not `node_modules`:
|
||||
`grep -n "^export const NAME" repos/effect/packages/effect/src/MODULE.ts` shows the signature
|
||||
and implementation; `repos/effect/packages/effect/test/` has usage examples. Add what you learn.
|
||||
|
||||
## Renames from Effect 3 (the things that bite)
|
||||
|
||||
- `Effect.async` → `Effect.callback`; `Effect.catchAll` → `Effect.catch`; `Effect.catchAllCause` → `Effect.catchCause`.
|
||||
- Every `unsafeX` is `xUnsafe`: `Deferred.doneUnsafe`, `Deferred.isDoneUnsafe`, `PubSub.publishUnsafe`,
|
||||
`SubscriptionRef.getUnsafe`, `Latch.openUnsafe`, `Scope.makeUnsafe`.
|
||||
- `Context.Tag` classes are `Context.Service<Self, Shape>()("id")`; layers are built with `Layer.effect(Tag, make)`.
|
||||
- `Schema.Union([A, B])` and `Schema.Literals(["a", "b"])` take an array. Filters are `.check(Schema.isInt(),
|
||||
Schema.isGreaterThanOrEqualTo(0))`, not `.pipe(Schema.int())`. `.annotate({ description, message })`.
|
||||
`Schema.TaggedError<Self>()("Tag", fields)`; `Schema.NumberFromString`; `Schema.fromJsonString(Schema.Json)`.
|
||||
- `Duration.isGreaterThan` / `isLessThan` (not `greaterThan`); `Duration.format(d)` prints `1s 500ms`.
|
||||
- `Effect.timeout` / `timeoutOption` / `timeoutOrElse`; `Stream.runHead` returns an `Option`.
|
||||
- `Effect.forkScoped(effect, { startImmediately?, uninterruptible? })`; `Effect.forkIn(scope)`; `Effect.forkDetach`.
|
||||
- `Effect.tap` accepts an effect or a function; `Effect.when(condition)` returns an `Option`.
|
||||
- `Effect.fn("name")(function* (...) {})` for traced generator functions (used everywhere in `src/lsp/`).
|
||||
|
||||
## Schedule (reshaped in v4)
|
||||
|
||||
- Constructors: `exponential(base, factor)`, `spaced`, `fixed`, `recurs(n)`, `forever`, `jittered`, `cron`.
|
||||
- Limits: `Schedule.upTo({ times, duration })` (replaces `intersect(recurs(n))`); `Schedule.while(meta => boolean | Effect)`
|
||||
replaces `whileInput` / `whileOutput` / `recurWhile`; `Schedule.modifyDelay(meta => Duration.Input)`; `Schedule.tap(meta => ...)`;
|
||||
`Schedule.passthrough`. Combinators: `Schedule.max(a, b)` recurs while both do, with the slower delay (Effect 3
|
||||
`intersect`); `Schedule.min` recurs while either does, with the faster delay (`union`); `Schedule.concat` runs one
|
||||
then the other (`andThen`). There is no `resetAfter`: write a step with its own state instead (below).
|
||||
- `Metadata` given to those callbacks: `{ input, output, duration, attempt, start, now, elapsed, elapsedSincePrevious }`.
|
||||
`elapsedSincePrevious` is the time between consecutive steps (between crashes, for a restart policy).
|
||||
- Custom schedules: `Schedule.fromStepWithMetadata(Effect.sync(() => (meta: InputMetadata<Input>) => ...))`, where the step
|
||||
returns `Effect.succeed([output, delay])` to continue or `Cause.done(output)` to stop. `Effect.sync` runs once per `retry`,
|
||||
so closure state (a crash streak) is per run. See `restarts` in `src/lsp/LanguageServer.ts`.
|
||||
- `Effect.retry` takes `{ while, until, times, schedule }`, a `Schedule`, or a builder `($) => ...`. `Effect.retryOrElse(schedule,
|
||||
(lastError, output) => ...)` runs the fallback once the schedule stops. The effect under retry/repeat sees
|
||||
`Schedule.CurrentMetadata` as a service.
|
||||
|
||||
## Concurrency primitives (as expected, with v4 names)
|
||||
|
||||
- `Deferred.make<A, E>()`, `await`, `succeed`, `fail`, `done(exit)`, `isDone`; the `*Unsafe` variants are synchronous and return
|
||||
`boolean` (whether this call completed it). Waiters of `Deferred.await` resume synchronously inside `doneUnsafe`.
|
||||
- `PubSub.unbounded<A>({ replay? })`, `publish`, `publishUnsafe`, `subscribe`, `Stream.fromPubSub`, `Stream.fromSubscription`.
|
||||
- `SubscriptionRef.make/get/set/update/changes`; `changes` emits the current value first (replay 1).
|
||||
- `Latch.make(open?)` with `.open`, `.close`, `.await`, `.release`, `.whenOpen(effect)`.
|
||||
- `Semaphore.make(n)`, `.withPermits(n)(effect)`; `FiberSet.makeRuntime<R>()` / `makeRuntimePromise` to run effects from callbacks.
|
||||
- `ScopedCache.makeWith({ capacity, timeToLive: (exit) => Duration, lookup })`, `get`, `has`, `invalidate`, `keys`, `values`, `entries`.
|
||||
- `Cause.done(value)` is the halt signal used by `Pull`, `Schedule` and `Stream` internals; `Cause.hasInterruptsOnly(cause)`.
|
||||
|
||||
## `effect/unstable/cli`
|
||||
|
||||
- `Command.make(name, { flag: Flag..., arg: Argument... }, Effect.fn(function* (config) {}))`, `Command.withDescription`,
|
||||
`Command.withExamples([{ command, description }])`, `Command.run({ version })`.
|
||||
- `Flag.String/Int/Boolean/Directory(name, { mustExist })/KeyValuePair/Literals(name, [...])`; `Flag.withSchema(schema)`
|
||||
(decodes from the flag's type, so `Schema.NumberFromString` for a string flag), `withDescription`, `withDefault`, `withAlias`,
|
||||
`withMetavar("never|always|N")`, `optional` (→ `Option`), `map`, `orElse`. `Argument.String`, `Argument.variadic`.
|
||||
- Help does not print default values; put them in the description. Schema failures print `Invalid value for flag --x: "v".
|
||||
Expected: Schema validation failed: <message annotation or issue>`.
|
||||
|
||||
## `effect/unstable/ai` MCP (see `src/Server.ts`, `src/Tools.ts`, `src/Resources.ts`)
|
||||
|
||||
- `McpServer.layerStdio(info)` / `layerHttp({ ...info, path })` served with `HttpRouter.serve`; `McpServer.registerToolkit(toolkit)`
|
||||
after `Toolkit.toLayer(handlers)`; `McpServer.registerResource({ uri | template, name, description, mimeType, content, completion })`.
|
||||
- `Tool.make(name, { description, parameters, success, failure, dependencies }).annotate(Tool.Readonly, ...)`.
|
||||
- `McpServer.McpServer` service exposes `notifications["notifications/message" | "notifications/resources/updated"]`.
|
||||
|
||||
## `@effect/vitest`
|
||||
|
||||
- `it.effect` / `it.live` tests run inside a `Scope`; `it.layer(layer, { excludeTestServices: true })("name", (it) => ...)` shares the
|
||||
layer across the block and, with that option, uses the real clock (`Stream.debounce` and timeouts hang under `TestClock`).
|
||||
- Real processes and `Effect.sleep` need real time, so every fixture-server test uses `excludeTestServices` or `it.live`.
|
||||
|
||||
## `vscode-jsonrpc` (not Effect, but verified the same way)
|
||||
|
||||
- Loss of the peer: `connection.onClose` fires on transport close and on `dispose()`; pending requests reject with
|
||||
`ResponseError` codes `ConnectionInactive` (-32096), `PendingResponseRejected` (-32097) or `MessageWriteError` (-32099).
|
||||
- Writing to a dead child's stdin rejects a promise nobody handles (EPIPE crashes the process). `LanguageServer.ts` writes through
|
||||
a `PassThrough` piped into stdin and listens for stdin errors instead.
|
||||
- `connection.onRequest((method, params) => ...)` registers a catch-all ("star") handler; handlers registered per type are
|
||||
consulted first.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 EFLKumo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,132 @@
|
||||
# lsp-mcp
|
||||
|
||||
An [MCP](https://modelcontextprotocol.io/) server that exposes a single [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) server to agents as a headless editor.
|
||||
|
||||
`lsp-mcp` starts the language server you provide, keeps its document state in sync with the workspace, and exposes the language server's capabilities through MCP tools and resources. It is written in TypeScript and [Effect](https://effect.website).
|
||||
|
||||
## Features
|
||||
|
||||
- stdio and Streamable HTTP MCP transports
|
||||
- Diagnostics exposed both as a tool and subscribable MCP resources
|
||||
- Preview-by-default edits for rename and formatting, with explicit disk application
|
||||
- Language-server crash supervision, restart backoff, and document reopening
|
||||
- Initialization options, workspace settings, language-id mappings, and open-document capacity configuration
|
||||
|
||||
Underlying techniques:
|
||||
|
||||
- On-demand document opening, disk change detection, and an LRU cache of open documents
|
||||
- 1-based line and column positions designed for agent-facing use
|
||||
- Capability-aware tool registration: unsupported LSP features are not advertised
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 26+
|
||||
- pnpm 12.4.2+
|
||||
- An installed language server executable
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
From a checkout, invoke the built server with `node dist/main.mjs`. Use `pnpm dev` to run the TypeScript source directly during development.
|
||||
|
||||
## Usage
|
||||
|
||||
The general form is:
|
||||
|
||||
```sh
|
||||
node dist/main.mjs [lsp-mcp options] -- <language-server> [language-server args...]
|
||||
```
|
||||
|
||||
The `--` separates `lsp-mcp` options from the executable and arguments passed to the language server.
|
||||
|
||||
### stdio
|
||||
|
||||
stdio is the default transport and is suitable for MCP clients that launch a local server process:
|
||||
|
||||
```sh
|
||||
node dist/main.mjs --root /path/to/project -- typescript-language-server --stdio
|
||||
```
|
||||
|
||||
For example, an MCP client configuration can point at the checkout like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"typescript": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/path/to/lsp-mcp/dist/main.mjs",
|
||||
"--root",
|
||||
"/path/to/project",
|
||||
"--",
|
||||
"typescript-language-server",
|
||||
"--stdio"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Streamable HTTP
|
||||
|
||||
Run the MCP endpoint on a local port with `--http`:
|
||||
|
||||
```sh
|
||||
node dist/main.mjs --http 9010 --root ./crate -- rust-analyzer
|
||||
```
|
||||
|
||||
The endpoint is available at:
|
||||
|
||||
```text
|
||||
http://localhost:9010/mcp
|
||||
```
|
||||
|
||||
### Configuration examples
|
||||
|
||||
```sh
|
||||
# Pass JSON initialization options to the language server.
|
||||
node dist/main.mjs \
|
||||
--init-options '{"typescript":{"useInferredProjectPerProjectRoot":true}}' \
|
||||
-- typescript-language-server --stdio
|
||||
|
||||
# Serve workspace/configuration and didChangeConfiguration with JSON settings.
|
||||
node dist/main.mjs \
|
||||
--settings '{"gopls":{"staticcheck":true}}' \
|
||||
-- gopls
|
||||
|
||||
# Add a file-extension to language-id mapping.
|
||||
node dist/main.mjs --language vue=vue --root ./web -- vue-language-server --stdio
|
||||
|
||||
# Keep more documents open, and always restart a crashed language server.
|
||||
node dist/main.mjs --open-documents 64 --restart always -- clangd
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
pnpm test # Run the Vitest suite, including the fixture LSP integration tests
|
||||
pnpm check # Type-check without emitting JavaScript
|
||||
pnpm lint # Format and lint with Biome
|
||||
```
|
||||
|
||||
The main source layout is:
|
||||
|
||||
```text
|
||||
src/Model.ts Agent-facing schemas and 1-based positions
|
||||
src/Tools.ts MCP tool definitions and capability gating
|
||||
src/Resources.ts MCP resources and notifications
|
||||
src/Server.ts Effect layer composition and transports
|
||||
src/main.ts CLI entry point
|
||||
src/lsp/ LSP process, document, diagnostic, and editor bridge
|
||||
test/ Unit and integration tests with a fixture language server
|
||||
```
|
||||
|
||||
The `vscode-*` packages are intentionally confined to `src/lsp/`; the rest of the application uses the agent-facing schemas and Effect services.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+16
-12
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "lsp-mcp",
|
||||
"version": "0.1.0",
|
||||
"name": "@opticlm/lsp-mcp",
|
||||
"version": "0.1.2",
|
||||
"description": "MCP server exposing a Language Server's capabilities to agents as a headless editor",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"bin": {
|
||||
"lsp-mcp": "./src/main.ts"
|
||||
"lsp-mcp": "./dist/main.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/main.ts",
|
||||
"build": "tsdown",
|
||||
"start": "node dist/main.mjs",
|
||||
"dev": "node src/main.ts",
|
||||
"check": "tsc",
|
||||
"test": "vitest run",
|
||||
"lint": "biome check --write --unsafe",
|
||||
@@ -22,20 +27,19 @@
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@12.4.2",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-rc.115",
|
||||
"effect": "4.0.0-rc.115",
|
||||
"vscode-languageserver-protocol": "^3.18.3",
|
||||
"vscode-languageserver-textdocument": "^1.0.14",
|
||||
"vscode-uri": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.14",
|
||||
"@effect/platform-node": "4.0.0-rc.115",
|
||||
"@effect/tsgo": "0.45.0",
|
||||
"@effect/vitest": "4.0.0-rc.115",
|
||||
"@types/node": "^26.6.1",
|
||||
"effect": "4.0.0-rc.115",
|
||||
"tsdown": "0.23.0",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^5.0.1",
|
||||
"vscode-languageserver": "^10.1.1"
|
||||
"vscode-languageserver": "^10.1.1",
|
||||
"vscode-languageserver-protocol": "^3.18.3",
|
||||
"vscode-languageserver-textdocument": "^1.0.14",
|
||||
"vscode-uri": "^3.2.0"
|
||||
}
|
||||
}
|
||||
Generated
+466
-16
@@ -165,26 +165,13 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@effect/platform-node':
|
||||
specifier: 4.0.0-rc.115
|
||||
version: 4.0.0-rc.115(effect@4.0.0-rc.115)(redis@6.2.1)
|
||||
effect:
|
||||
specifier: 4.0.0-rc.115
|
||||
version: 4.0.0-rc.115
|
||||
vscode-languageserver-protocol:
|
||||
specifier: ^3.18.3
|
||||
version: 3.18.3
|
||||
vscode-languageserver-textdocument:
|
||||
specifier: ^1.0.14
|
||||
version: 1.0.14
|
||||
vscode-uri:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.5.14
|
||||
version: 2.5.14
|
||||
'@effect/platform-node':
|
||||
specifier: 4.0.0-rc.115
|
||||
version: 4.0.0-rc.115(effect@4.0.0-rc.115)(redis@6.2.1)
|
||||
'@effect/tsgo':
|
||||
specifier: 0.45.0
|
||||
version: 0.45.0
|
||||
@@ -194,6 +181,12 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^26.6.1
|
||||
version: 26.6.1
|
||||
effect:
|
||||
specifier: 4.0.0-rc.115
|
||||
version: 4.0.0-rc.115
|
||||
tsdown:
|
||||
specifier: 0.23.0
|
||||
version: 0.23.0(typescript@7.0.2)
|
||||
typescript:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
@@ -203,6 +196,15 @@ importers:
|
||||
vscode-languageserver:
|
||||
specifier: ^10.1.1
|
||||
version: 10.1.1
|
||||
vscode-languageserver-protocol:
|
||||
specifier: ^3.18.3
|
||||
version: 3.18.3
|
||||
vscode-languageserver-textdocument:
|
||||
specifier: ^1.0.14
|
||||
version: 1.0.14
|
||||
vscode-uri:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -334,6 +336,9 @@ packages:
|
||||
'@oxc-project/types@0.150.0':
|
||||
resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
|
||||
|
||||
'@quansync/fs@1.1.0':
|
||||
resolution: {integrity: sha512-qAPG/t3HqML1TlN7sY/pTbEjzFVAKsMjNNMGheyDosro+kT4iw2KCUoHcVdmliwWjorm4elZbgNQyU2eD97eDg==}
|
||||
|
||||
'@redis/bloom@6.2.1':
|
||||
resolution: {integrity: sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag==}
|
||||
engines: {node: '>= 20.0.0'}
|
||||
@@ -618,10 +623,149 @@ packages:
|
||||
'@vitest/spy@5.0.1':
|
||||
resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==}
|
||||
|
||||
'@yuku-codegen/binding-android-arm64@0.10.2':
|
||||
resolution: {integrity: sha512-Oc2KInVkPfUjEB4PeV6X0NvIoyYTzDe/WmqFrTwBqKs6YKnz4A7XwWQvJTb7M0rLz9a6WgpUUiiS5QKaCi4O/g==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@yuku-codegen/binding-darwin-arm64@0.10.2':
|
||||
resolution: {integrity: sha512-3H7eNPIHJndUJXZ4QUGBPq1zC6RGcUZfm7bymJes+/N7wTVWUn1bxZ9JcozYHpkWQNm9f23G8YWbaHD9aYH72A==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@yuku-codegen/binding-darwin-x64@0.10.2':
|
||||
resolution: {integrity: sha512-AJOUpR2s3LF9AWiiub/Uyc/UoXNTWcq8hK3cVI6fUSXtwh34dG21J6hRuNlO6JVJFTo4o3ScVvZOrh/YFfUEAA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@yuku-codegen/binding-freebsd-x64@0.10.2':
|
||||
resolution: {integrity: sha512-ms7DcZu87u5CiK/wMwvpKAvAmtaqKjCQIXNweMLypG6qbuiaOMQf1jpbB5+j46xBcgCovo8TQMcv0bCQLKIL9w==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@yuku-codegen/binding-linux-arm-gnu@0.10.2':
|
||||
resolution: {integrity: sha512-xJDpYAsKV5+eaiBhTYl05fvT6sst4PsCeuIFMRu9b0/WCSrNQPfCDtAQ5/MS6xNpsdujdZEPR+gmfWk3ZG5i3A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@yuku-codegen/binding-linux-arm-musl@0.10.2':
|
||||
resolution: {integrity: sha512-qfTkgd7AEx61l4K9VtXWCGTxc/fmXJ4ZheDz3i+/AAJUtg4sa0bPrc0VBxPggB/rayR8Z3+JfrBItuyanBJJ9Q==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@yuku-codegen/binding-linux-arm64-gnu@0.10.2':
|
||||
resolution: {integrity: sha512-4e6Mifm/4UdjtU3D8mATIrvgP+xEiH8xtQOjd0zpd72XKWT7ug3sdyhq2utkkZFP6BvYp7SgZrI+aR4VeIOHdw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@yuku-codegen/binding-linux-arm64-musl@0.10.2':
|
||||
resolution: {integrity: sha512-RdsJrUfDYFVV3JOmWFUWwSu31fa1APn12xDeIKxgl/YWxrabwtxsDBNjF2561c7tmcbiewdCUvngqRs8AyGVLA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@yuku-codegen/binding-linux-x64-gnu@0.10.2':
|
||||
resolution: {integrity: sha512-mVzWimEPPreaPyXIUvxsHoJzvO7ckZ5j0LgQFHz04zQIRwt9A9T2hA7K5/jYjJKkMxWG/U2VjhGNwVhtyU+uqg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@yuku-codegen/binding-linux-x64-musl@0.10.2':
|
||||
resolution: {integrity: sha512-Y77fyISurmr2mvO1yeq3u+x/WJbACgPR4w+lzEVx30ViCxZzIGrZPRN1yEdZ9xUNPNsIO5thBHIdRNG+UGUG+Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@yuku-codegen/binding-win32-arm64@0.10.2':
|
||||
resolution: {integrity: sha512-1+tGLyG0u5YYy0lev4ck7/0sd8xJ9SZde0dLut7qLDPZV5WVTV8x6OxNXUne/PGuHZyO1vK+txPQzzHOyXHGSw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@yuku-codegen/binding-win32-x64@0.10.2':
|
||||
resolution: {integrity: sha512-8a2SExRohRbwC0MY4VpOF0/RSckrJ2ASZqAor5/RYSJJaeadR93n10gZzcKT6pY9ukiSOIZCRKVRD2tH73T0qA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@yuku-parser/binding-android-arm64@0.10.2':
|
||||
resolution: {integrity: sha512-2VPBU9fRGRAQ2xPAvghnec3oou5Nrxm2Bezhf+13UMspAxQSkF/32r+ySKXSwIPA5/RivumDJDxAxc301Sgicw==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@yuku-parser/binding-darwin-arm64@0.10.2':
|
||||
resolution: {integrity: sha512-LD+PMZE51tCYTOss5HBkm3/AE39MvcMDBfWJx7A4yDcjfNbAQDnHZKtzSOuqrswx+TQHY0ws5xn6+fWOwtmfBA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@yuku-parser/binding-darwin-x64@0.10.2':
|
||||
resolution: {integrity: sha512-mmZ8cND+AoIIMRERyMinlg5ApHxP23B9jH2B5wT7T+dliPa9rubLxneB/SUjFwyUjGaFnB5G7t4YvpfbO5zbkQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@yuku-parser/binding-freebsd-x64@0.10.2':
|
||||
resolution: {integrity: sha512-gVIjaaIddbRfAhHlC8N809wQWml7mxfSVnzaLtzGXObTeFTPAg/YVnQXt1UOhPM1ah5eG/8Y0RUlNpB4GVe7eQ==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@yuku-parser/binding-linux-arm-gnu@0.10.2':
|
||||
resolution: {integrity: sha512-q/XPPQQAPdlw05aPj30ygBhekmQryGOwxVraBgApjKK8yY1kNQgqq6XCYLF1WHSee+lhxclrTO7W0/bt7dR1GA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@yuku-parser/binding-linux-arm-musl@0.10.2':
|
||||
resolution: {integrity: sha512-A+Cb0I1hFF4wilTQpWs79k1aNnj4B2tkrHx3zsuUNF9BtVY2zPZ4yeQEM/zjXrS/qJlNMZVK9NrWJjwdpnTrfg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@yuku-parser/binding-linux-arm64-gnu@0.10.2':
|
||||
resolution: {integrity: sha512-aGNSzqIqqphFwAdIFwVvKIyXD1Iy3CxrEJVIZAT87Ecyi4vCmmDD2v2l9h+Gd3/wSy3JZlOI792LJbKAjyy9rQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@yuku-parser/binding-linux-arm64-musl@0.10.2':
|
||||
resolution: {integrity: sha512-Ddl1sF0rtuCXyHGSOV6dcmdx+ETv1iD+IVFqQuOjzkJZWclxJxdBWX6ZJMRtTiGqGUTbqLEKiTXxpR/Bvqk+2A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@yuku-parser/binding-linux-x64-gnu@0.10.2':
|
||||
resolution: {integrity: sha512-/nlcpR6IF5U+0m5L9wvAIXeQa2DT+BXuvqKDsTcOTlcc0B4dHNyqE5hTXsS4hAyimpy2ZK8A1zMNF5hTrjg2cg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@yuku-parser/binding-linux-x64-musl@0.10.2':
|
||||
resolution: {integrity: sha512-GX80dxTQD/M/OryyuxJcFzFENzry3cDFPvCFTyWogNWQH3fSHfMrNfpwpl1YzMos1eV9NygK5GSDG7VArQlb4w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@yuku-parser/binding-win32-arm64@0.10.2':
|
||||
resolution: {integrity: sha512-agePQBV4VHewiGU0ACSjscZ/hJd283f9JfAF19Gf1rI5+wy5tTgx4GS36i0b3xim15AQ4RCpDRosEvhLZ4zAOw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@yuku-parser/binding-win32-x64@0.10.2':
|
||||
resolution: {integrity: sha512-s8//CMpgL5+y1lvDCdyh1rwGI5+ytDJywFJRe1vnhI7n0j+caxshNucurikYLLVeQ2SYFex6aPrzgQxkabBQRA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@yuku-toolchain/types@0.10.2':
|
||||
resolution: {integrity: sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
cac@7.0.0:
|
||||
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -630,13 +774,29 @@ packages:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
defu@6.1.7:
|
||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dts-resolver@3.0.0:
|
||||
resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
|
||||
engines: {node: ^22.18.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
oxc-resolver: '>=11.0.0'
|
||||
peerDependenciesMeta:
|
||||
oxc-resolver:
|
||||
optional: true
|
||||
|
||||
effect@4.0.0-rc.115:
|
||||
resolution: {integrity: sha512-ogYulZ5ffeOzrJqQrG0XOkDO5lKn2s9KNHhoxJy/6wKUkF0i12dlVXWA9TgsgQ+zV/JzizyG0+XMPepZEQX6vw==}
|
||||
|
||||
empathic@2.0.1:
|
||||
resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
es-module-lexer@2.3.2:
|
||||
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||
|
||||
@@ -661,6 +821,17 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-tsconfig@5.0.0-beta.6:
|
||||
resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==}
|
||||
engines: {node: '>=20.20.0'}
|
||||
|
||||
hookable@6.1.2:
|
||||
resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==}
|
||||
|
||||
import-without-cache@0.4.1:
|
||||
resolution: {integrity: sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A==}
|
||||
engines: {node: ^22.18.0 || >=24.0.0}
|
||||
|
||||
lightningcss-android-arm64@1.33.0:
|
||||
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -747,6 +918,10 @@ packages:
|
||||
resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
obug@3.0.0:
|
||||
resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -758,10 +933,38 @@ packages:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
quansync@1.0.0:
|
||||
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
|
||||
|
||||
redis@6.2.1:
|
||||
resolution: {integrity: sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg==}
|
||||
engines: {node: '>= 20.0.0'}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
rolldown-plugin-dts@0.28.6:
|
||||
resolution: {integrity: sha512-qKrFtBfRfR2hP233m7Ic9zf3wv6MSYdZghWKMdEO6dRYZGlNfINJAa1P5ZVvyHh3mrcd7IJQvIY8L9vnm+v5rQ==}
|
||||
engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0}
|
||||
peerDependencies:
|
||||
'@typescript/native-preview': '*'
|
||||
'@volar/typescript': ~2.4.0
|
||||
'@vue/language-core': ~3.2.0 || ~3.3.0
|
||||
rolldown: ^1.2.0
|
||||
typescript: ^5.0.0 || ^6.0.0 || ~7.0.0
|
||||
vue-tsc: ~3.2.0 || ~3.3.0
|
||||
peerDependenciesMeta:
|
||||
'@typescript/native-preview':
|
||||
optional: true
|
||||
'@volar/typescript':
|
||||
optional: true
|
||||
'@vue/language-core':
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
vue-tsc:
|
||||
optional: true
|
||||
|
||||
rolldown@1.2.9:
|
||||
resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -788,15 +991,60 @@ packages:
|
||||
resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyexec@1.3.1:
|
||||
resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
tsdown@0.23.0:
|
||||
resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==}
|
||||
engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@arethetypeswrong/core': ^0.18.1
|
||||
'@tsdown/css': 0.23.0
|
||||
'@tsdown/exe': 0.23.0
|
||||
'@vitejs/devtools': '*'
|
||||
publint: ^0.3.8
|
||||
tsx: '*'
|
||||
typescript: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
unplugin-unused: '>=0.5.0'
|
||||
unrun: '*'
|
||||
peerDependenciesMeta:
|
||||
'@arethetypeswrong/core':
|
||||
optional: true
|
||||
'@tsdown/css':
|
||||
optional: true
|
||||
'@tsdown/exe':
|
||||
optional: true
|
||||
'@vitejs/devtools':
|
||||
optional: true
|
||||
publint:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
unplugin-unused:
|
||||
optional: true
|
||||
unrun:
|
||||
optional: true
|
||||
|
||||
typescript@7.0.2:
|
||||
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
hasBin: true
|
||||
|
||||
unconfig-core@7.5.0:
|
||||
resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
|
||||
|
||||
undici-types@8.9.0:
|
||||
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
||||
|
||||
@@ -804,6 +1052,10 @@ packages:
|
||||
resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
verkit@0.4.0:
|
||||
resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
vite@8.3.0:
|
||||
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -925,6 +1177,15 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
yuku-ast@0.10.2:
|
||||
resolution: {integrity: sha512-UnG9mA6giglCvSErft2/40TVFX750Sj1xgwddPLpG7J7rlr/P1wPADg9G2RK+d/1tNLtRVoLdMMOMVBB9561TQ==}
|
||||
|
||||
yuku-codegen@0.10.2:
|
||||
resolution: {integrity: sha512-hentl2dtrF6cPjiAirtkfxfFZBA6Hdm61UqRJ7cA0qrlDNMk9PZGOjw5w1mx3E0hu/6pHcJF4dJW+D0SXHPZOA==}
|
||||
|
||||
yuku-parser@0.10.2:
|
||||
resolution: {integrity: sha512-CgaU0/PPjCAIEZ3WQroosOxTY3eeKldAN3h+vk8pMNz4+jl1CZzBR4pW+K/rRPF112VooCL5FdjJoiMNjDrL2A==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@biomejs/biome@2.5.14':
|
||||
@@ -1028,6 +1289,10 @@ snapshots:
|
||||
|
||||
'@oxc-project/types@0.150.0': {}
|
||||
|
||||
'@quansync/fs@1.1.0':
|
||||
dependencies:
|
||||
quansync: 1.0.0
|
||||
|
||||
'@redis/bloom@6.2.1(@redis/client@6.2.1)':
|
||||
dependencies:
|
||||
'@redis/client': 6.2.1
|
||||
@@ -1183,16 +1448,98 @@ snapshots:
|
||||
|
||||
'@vitest/spy@5.0.1': {}
|
||||
|
||||
'@yuku-codegen/binding-android-arm64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-darwin-arm64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-darwin-x64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-freebsd-x64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-linux-arm-gnu@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-linux-arm-musl@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-linux-arm64-gnu@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-linux-arm64-musl@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-linux-x64-gnu@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-linux-x64-musl@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-win32-arm64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-codegen/binding-win32-x64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-android-arm64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-darwin-arm64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-darwin-x64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-freebsd-x64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-linux-arm-gnu@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-linux-arm-musl@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-linux-arm64-gnu@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-linux-arm64-musl@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-linux-x64-gnu@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-linux-x64-musl@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-win32-arm64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-parser/binding-win32-x64@0.10.2':
|
||||
optional: true
|
||||
|
||||
'@yuku-toolchain/types@0.10.2': {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
cac@7.0.0: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
defu@6.1.7: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
dts-resolver@3.0.0: {}
|
||||
|
||||
effect@4.0.0-rc.115: {}
|
||||
|
||||
empathic@2.0.1: {}
|
||||
|
||||
es-module-lexer@2.3.2: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
@@ -1208,6 +1555,14 @@ snapshots:
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-tsconfig@5.0.0-beta.6:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
hookable@6.1.2: {}
|
||||
|
||||
import-without-cache@0.4.1: {}
|
||||
|
||||
lightningcss-android-arm64@1.33.0:
|
||||
optional: true
|
||||
|
||||
@@ -1265,6 +1620,8 @@ snapshots:
|
||||
|
||||
obug@2.2.1: {}
|
||||
|
||||
obug@3.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
@@ -1275,6 +1632,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
quansync@1.0.0: {}
|
||||
|
||||
redis@6.2.1:
|
||||
dependencies:
|
||||
'@redis/bloom': 6.2.1(@redis/client@6.2.1)
|
||||
@@ -1286,6 +1645,22 @@ snapshots:
|
||||
- '@node-rs/xxhash'
|
||||
- '@opentelemetry/api'
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
rolldown-plugin-dts@0.28.6(rolldown@1.2.9)(typescript@7.0.2):
|
||||
dependencies:
|
||||
dts-resolver: 3.0.0
|
||||
get-tsconfig: 5.0.0-beta.6
|
||||
obug: 3.0.0
|
||||
rolldown: 1.2.9
|
||||
yuku-ast: 0.10.2
|
||||
yuku-codegen: 0.10.2
|
||||
yuku-parser: 0.10.2
|
||||
optionalDependencies:
|
||||
typescript: 7.0.2
|
||||
transitivePeerDependencies:
|
||||
- oxc-resolver
|
||||
|
||||
rolldown@1.2.9:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.150.0
|
||||
@@ -1319,11 +1694,40 @@ snapshots:
|
||||
|
||||
tinyexec@1.3.0: {}
|
||||
|
||||
tinyexec@1.3.1: {}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.7)
|
||||
picomatch: 4.0.7
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
tsdown@0.23.0(typescript@7.0.2):
|
||||
dependencies:
|
||||
cac: 7.0.0
|
||||
defu: 6.1.7
|
||||
empathic: 2.0.1
|
||||
hookable: 6.1.2
|
||||
import-without-cache: 0.4.1
|
||||
obug: 2.2.1
|
||||
picomatch: 4.0.7
|
||||
rolldown: 1.2.9
|
||||
rolldown-plugin-dts: 0.28.6(rolldown@1.2.9)(typescript@7.0.2)
|
||||
tinyexec: 1.3.1
|
||||
tinyglobby: 0.2.17
|
||||
tree-kill: 1.2.2
|
||||
unconfig-core: 7.5.0
|
||||
verkit: 0.4.0
|
||||
optionalDependencies:
|
||||
typescript: 7.0.2
|
||||
transitivePeerDependencies:
|
||||
- '@typescript/native-preview'
|
||||
- '@volar/typescript'
|
||||
- '@vue/language-core'
|
||||
- oxc-resolver
|
||||
- vue-tsc
|
||||
|
||||
typescript@7.0.2:
|
||||
optionalDependencies:
|
||||
'@typescript/typescript-aix-ppc64': 7.0.2
|
||||
@@ -1347,10 +1751,17 @@ snapshots:
|
||||
'@typescript/typescript-win32-arm64': 7.0.2
|
||||
'@typescript/typescript-win32-x64': 7.0.2
|
||||
|
||||
unconfig-core@7.5.0:
|
||||
dependencies:
|
||||
'@quansync/fs': 1.1.0
|
||||
quansync: 1.0.0
|
||||
|
||||
undici-types@8.9.0: {}
|
||||
|
||||
undici@8.10.2: {}
|
||||
|
||||
verkit@0.4.0: {}
|
||||
|
||||
vite@8.3.0(@types/node@26.6.1):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
@@ -1406,3 +1817,42 @@ snapshots:
|
||||
stackback: 0.0.2
|
||||
|
||||
ws@8.21.3: {}
|
||||
|
||||
yuku-ast@0.10.2:
|
||||
dependencies:
|
||||
'@yuku-toolchain/types': 0.10.2
|
||||
|
||||
yuku-codegen@0.10.2:
|
||||
dependencies:
|
||||
'@yuku-toolchain/types': 0.10.2
|
||||
optionalDependencies:
|
||||
'@yuku-codegen/binding-android-arm64': 0.10.2
|
||||
'@yuku-codegen/binding-darwin-arm64': 0.10.2
|
||||
'@yuku-codegen/binding-darwin-x64': 0.10.2
|
||||
'@yuku-codegen/binding-freebsd-x64': 0.10.2
|
||||
'@yuku-codegen/binding-linux-arm-gnu': 0.10.2
|
||||
'@yuku-codegen/binding-linux-arm-musl': 0.10.2
|
||||
'@yuku-codegen/binding-linux-arm64-gnu': 0.10.2
|
||||
'@yuku-codegen/binding-linux-arm64-musl': 0.10.2
|
||||
'@yuku-codegen/binding-linux-x64-gnu': 0.10.2
|
||||
'@yuku-codegen/binding-linux-x64-musl': 0.10.2
|
||||
'@yuku-codegen/binding-win32-arm64': 0.10.2
|
||||
'@yuku-codegen/binding-win32-x64': 0.10.2
|
||||
|
||||
yuku-parser@0.10.2:
|
||||
dependencies:
|
||||
'@yuku-toolchain/types': 0.10.2
|
||||
yuku-ast: 0.10.2
|
||||
optionalDependencies:
|
||||
'@yuku-parser/binding-android-arm64': 0.10.2
|
||||
'@yuku-parser/binding-darwin-arm64': 0.10.2
|
||||
'@yuku-parser/binding-darwin-x64': 0.10.2
|
||||
'@yuku-parser/binding-freebsd-x64': 0.10.2
|
||||
'@yuku-parser/binding-linux-arm-gnu': 0.10.2
|
||||
'@yuku-parser/binding-linux-arm-musl': 0.10.2
|
||||
'@yuku-parser/binding-linux-arm64-gnu': 0.10.2
|
||||
'@yuku-parser/binding-linux-arm64-musl': 0.10.2
|
||||
'@yuku-parser/binding-linux-x64-gnu': 0.10.2
|
||||
'@yuku-parser/binding-linux-x64-musl': 0.10.2
|
||||
'@yuku-parser/binding-win32-arm64': 0.10.2
|
||||
'@yuku-parser/binding-win32-x64': 0.10.2
|
||||
+1
-1
@@ -28,7 +28,7 @@ export interface Options {
|
||||
|
||||
const info = {
|
||||
name: "lsp-mcp",
|
||||
version: "0.1.0",
|
||||
version: "0.1.2",
|
||||
protocols: [
|
||||
McpProtocol.v2025_11_25,
|
||||
McpProtocol.v2025_06_18,
|
||||
|
||||
+313
-86
@@ -5,12 +5,20 @@
|
||||
* `request` waits for the server to become idle (no `$/progress` work in
|
||||
* flight) and retries transient `ContentModified` / `ServerCancelled`
|
||||
* failures, so callers can treat the server as if it were always ready.
|
||||
*
|
||||
* The process is supervised: when it exits unexpectedly it is started again
|
||||
* according to the restart policy, the documents that were open are opened in
|
||||
* the new process, and requests caught by the crash wait for it and retry.
|
||||
* Once the policy gives up, every request fails with an `LspError` saying so.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { NodeStream } from "@effect/platform-node";
|
||||
import {
|
||||
Cause,
|
||||
Context,
|
||||
Deferred,
|
||||
Duration,
|
||||
Effect,
|
||||
FiberSet,
|
||||
HashSet,
|
||||
@@ -25,14 +33,23 @@ import {
|
||||
import {
|
||||
CancellationTokenSource,
|
||||
type ClientCapabilities,
|
||||
type CompletionItemKind,
|
||||
type ConfigurationParams,
|
||||
ConfigurationRequest,
|
||||
createMessageConnection,
|
||||
DidChangeConfigurationNotification,
|
||||
DidChangeTextDocumentNotification,
|
||||
type DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentNotification,
|
||||
type DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentNotification,
|
||||
type DidOpenTextDocumentParams,
|
||||
ErrorCodes,
|
||||
ExitNotification,
|
||||
type HandlerResult,
|
||||
InitializedNotification,
|
||||
InitializeRequest,
|
||||
type InitializeResult,
|
||||
LogMessageNotification,
|
||||
type LSPAny,
|
||||
LSPErrorCodes,
|
||||
@@ -46,12 +63,21 @@ import {
|
||||
ShutdownRequest,
|
||||
StreamMessageReader,
|
||||
StreamMessageWriter,
|
||||
type SymbolKind,
|
||||
type TextDocumentItem,
|
||||
UnregistrationRequest,
|
||||
WorkDoneProgressCreateRequest,
|
||||
WorkspaceFoldersRequest,
|
||||
} from "vscode-languageserver-protocol/node";
|
||||
import { URI } from "vscode-uri";
|
||||
|
||||
/**
|
||||
* What to do when the server exits unexpectedly: never start it again, always
|
||||
* start it again, or start it again but give up after this many crashes in a
|
||||
* row (less than a minute apart).
|
||||
*/
|
||||
export type Restart = "never" | "always" | number;
|
||||
|
||||
export interface Options {
|
||||
readonly command: string;
|
||||
readonly args: ReadonlyArray<string>;
|
||||
@@ -60,18 +86,26 @@ export interface Options {
|
||||
readonly initializationOptions?: unknown;
|
||||
/** Served as `workspace/configuration` and pushed via `didChangeConfiguration`. */
|
||||
readonly settings?: unknown;
|
||||
/** Defaults to giving up after 3 crashes in a row. */
|
||||
readonly restart?: Restart;
|
||||
}
|
||||
|
||||
/** Failures worth retrying: the server was busy, or it went away and may come back. */
|
||||
const transient: ReadonlySet<number> = new Set([
|
||||
LSPErrorCodes.ContentModified,
|
||||
LSPErrorCodes.ServerCancelled,
|
||||
ErrorCodes.ConnectionInactive,
|
||||
ErrorCodes.PendingResponseRejected,
|
||||
ErrorCodes.MessageWriteError,
|
||||
]);
|
||||
|
||||
export class LspError extends Schema.TaggedError<LspError>()("LspError", {
|
||||
method: Schema.String,
|
||||
message: Schema.String,
|
||||
code: Schema.optional(Schema.Int),
|
||||
}) {
|
||||
get retryable() {
|
||||
return (
|
||||
this.code === LSPErrorCodes.ContentModified ||
|
||||
this.code === LSPErrorCodes.ServerCancelled
|
||||
);
|
||||
return this.code !== undefined && transient.has(this.code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +137,7 @@ export class LanguageServer extends Context.Service<
|
||||
type: RequestType<P, R, unknown>,
|
||||
handler: (params: P) => Effect.Effect<R, unknown>,
|
||||
) => Effect.Effect<void, never, Scope.Scope>;
|
||||
/** `window/logMessage` and `window/showMessage` traffic. */
|
||||
/** `window/logMessage` and `window/showMessage` traffic, plus restart announcements. */
|
||||
readonly logs: Stream.Stream<LogMessage>;
|
||||
}
|
||||
>()("lsp-mcp/lsp/LanguageServer") {
|
||||
@@ -111,22 +145,97 @@ export class LanguageServer extends Context.Service<
|
||||
Layer.effect(LanguageServer, make(options));
|
||||
}
|
||||
|
||||
/** One process and its connection; replaced on crash. */
|
||||
interface Session {
|
||||
readonly initialized: InitializeResult;
|
||||
readonly exited: Deferred.Deferred<never, LspError>;
|
||||
readonly send: <P, R>(
|
||||
type: RequestType<P, R, unknown>,
|
||||
params: P,
|
||||
) => Effect.Effect<R, LspError>;
|
||||
readonly notify: <P>(
|
||||
type: NotificationType<P>,
|
||||
params: P,
|
||||
) => Effect.Effect<void, LspError>;
|
||||
}
|
||||
|
||||
const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
const run = yield* FiberSet.makeRuntime<never>();
|
||||
const runPromise = yield* FiberSet.makeRuntimePromise<never>();
|
||||
const exited = yield* Deferred.make<never, LspError>();
|
||||
const events = yield* PubSub.unbounded<{
|
||||
readonly method: string;
|
||||
readonly params: unknown;
|
||||
}>();
|
||||
const lifecycle = yield* PubSub.unbounded<LogMessage>();
|
||||
const progress = yield* SubscriptionRef.make(
|
||||
HashSet.empty<string | number>(),
|
||||
);
|
||||
const exit = (message: string) =>
|
||||
const initialized = yield* Deferred.make<InitializeResult, LspError>();
|
||||
/** The live session, or the one being started; failed once the policy gives up. */
|
||||
let current = Deferred.makeUnsafe<Session, LspError>();
|
||||
const session = Effect.suspend(() => Deferred.await(current));
|
||||
/** What the server has been told is open, to open it again after a restart. */
|
||||
const documents = new Map<string, TextDocumentItem>();
|
||||
const limit =
|
||||
options.restart === "never"
|
||||
? 0
|
||||
: options.restart === "always"
|
||||
? Number.POSITIVE_INFINITY
|
||||
: (options.restart ?? 3);
|
||||
|
||||
const rootUri = URI.file(options.root).toString();
|
||||
const workspaceFolders = [
|
||||
{ uri: rootUri, name: options.root.split(/[\\/]/).pop() ?? options.root },
|
||||
];
|
||||
const handlers = new Map<
|
||||
string,
|
||||
(params: unknown) => HandlerResult<unknown, unknown>
|
||||
>([
|
||||
[WorkDoneProgressCreateRequest.method, () => undefined],
|
||||
[RegistrationRequest.method, () => undefined],
|
||||
[UnregistrationRequest.method, () => undefined],
|
||||
[
|
||||
ConfigurationRequest.method,
|
||||
(params) =>
|
||||
(params as ConfigurationParams).items.map((item) =>
|
||||
section(options.settings, item.section),
|
||||
),
|
||||
],
|
||||
[WorkspaceFoldersRequest.method, () => workspaceFolders],
|
||||
]);
|
||||
|
||||
const failure = (method: string, error: unknown) =>
|
||||
error instanceof ResponseError
|
||||
? new LspError({ method, message: error.message, code: error.code })
|
||||
: new LspError({
|
||||
method,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
const announce = (level: LogLevel, message: string) =>
|
||||
Effect.andThen(
|
||||
level === "error" ? Effect.logError(message) : Effect.logWarning(message),
|
||||
PubSub.publish(lifecycle, { level, message }),
|
||||
);
|
||||
|
||||
const start = Effect.fn("LanguageServer.start")(function* () {
|
||||
const exited = yield* Deferred.make<never, LspError>();
|
||||
const exit = (message: string) => {
|
||||
if (Deferred.isDoneUnsafe(exited)) return;
|
||||
// Whoever was using this session now waits for the next one; swap before
|
||||
// signalling, as waiters resume synchronously and may give up right away.
|
||||
if (Deferred.isDoneUnsafe(current)) current = Deferred.makeUnsafe();
|
||||
Deferred.doneUnsafe(
|
||||
exited,
|
||||
Effect.fail(new LspError({ method: "connection", message })),
|
||||
Effect.fail(
|
||||
new LspError({
|
||||
method: "connection",
|
||||
message,
|
||||
code: ErrorCodes.ConnectionInactive,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const child = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
@@ -155,9 +264,16 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
Effect.forkScoped,
|
||||
);
|
||||
|
||||
// Writes go through a buffer: a broken pipe must end the session, not
|
||||
// surface as a write failure (vscode-jsonrpc leaks those as unhandled rejections).
|
||||
const input = new PassThrough();
|
||||
input.pipe(child.stdin);
|
||||
child.stdin.on("error", (error) =>
|
||||
exit(`${options.command}: ${error.message}`),
|
||||
);
|
||||
const connection = createMessageConnection(
|
||||
new StreamMessageReader(child.stdout),
|
||||
new StreamMessageWriter(child.stdin),
|
||||
new StreamMessageWriter(input),
|
||||
{
|
||||
error: (m) => run(Effect.logError(m)),
|
||||
warn: (m) => run(Effect.logWarning(m)),
|
||||
@@ -181,32 +297,20 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
),
|
||||
),
|
||||
);
|
||||
connection.onRequest(WorkDoneProgressCreateRequest.type, () => undefined);
|
||||
connection.onRequest(RegistrationRequest.type, () => undefined);
|
||||
connection.onRequest(UnregistrationRequest.type, () => undefined);
|
||||
connection.onRequest(ConfigurationRequest.type, ({ items }) =>
|
||||
items.map((item) => section(options.settings, item.section)),
|
||||
connection.onRequest(
|
||||
(method, params) =>
|
||||
handlers.get(method)?.(params) ??
|
||||
new ResponseError(ErrorCodes.MethodNotFound, `unhandled ${method}`),
|
||||
);
|
||||
const rootUri = URI.file(options.root).toString();
|
||||
const workspaceFolders = [
|
||||
{ uri: rootUri, name: options.root.split(/[\\/]/).pop() ?? options.root },
|
||||
];
|
||||
connection.onRequest(WorkspaceFoldersRequest.type, () => workspaceFolders);
|
||||
connection.listen();
|
||||
|
||||
const failure = (method: string, error: unknown) =>
|
||||
error instanceof ResponseError
|
||||
? new LspError({ method, message: error.message, code: error.code })
|
||||
: new LspError({
|
||||
method,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
const send = <P, R>(type: RequestType<P, R, unknown>, params: P) =>
|
||||
Effect.callback<R, LspError>((resume) => {
|
||||
const cancellation = new CancellationTokenSource();
|
||||
try {
|
||||
connection.sendRequest(type, params as never, cancellation.token).then(
|
||||
connection
|
||||
.sendRequest(type, params as never, cancellation.token)
|
||||
.then(
|
||||
(result) => resume(Effect.succeed(result)),
|
||||
(error) => resume(Effect.fail(failure(type.method, error))),
|
||||
);
|
||||
@@ -216,65 +320,16 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
return Effect.ignore(Effect.try(() => cancellation.cancel()));
|
||||
}).pipe(Effect.raceFirst(Deferred.await(exited)));
|
||||
|
||||
const idle = SubscriptionRef.changes(progress).pipe(
|
||||
Stream.filter(HashSet.isEmpty),
|
||||
Stream.runHead,
|
||||
Effect.timeoutOption("15 seconds"),
|
||||
Effect.asVoid,
|
||||
);
|
||||
|
||||
const request = <P, R>(type: RequestType<P, R, unknown>, params: P) =>
|
||||
idle.pipe(
|
||||
Effect.andThen(send(type, params)),
|
||||
Effect.retry({
|
||||
while: (error) => error.retryable,
|
||||
schedule: Schedule.exponential("100 millis").pipe(
|
||||
Schedule.upTo({ times: 5 }),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const notify = <P>(type: NotificationType<P>, params: P) =>
|
||||
Effect.tryPromise({
|
||||
try: () => connection.sendNotification(type, params as never),
|
||||
catch: (error) => failure(type.method, error),
|
||||
}).pipe(Effect.catch((error) => Effect.logWarning(error.message)));
|
||||
|
||||
const notifications = <P>(type: NotificationType<P>): Stream.Stream<P> =>
|
||||
Stream.fromPubSub(events).pipe(
|
||||
Stream.filter((event) => event.method === type.method),
|
||||
Stream.map((event) => event.params as P),
|
||||
);
|
||||
|
||||
const onRequest = <P, R>(
|
||||
type: RequestType<P, R, unknown>,
|
||||
handler: (params: P) => Effect.Effect<R, unknown>,
|
||||
) =>
|
||||
Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
connection.onRequest(
|
||||
type.method,
|
||||
(params: P): HandlerResult<R, unknown> =>
|
||||
runPromise(
|
||||
handler(params).pipe(
|
||||
Effect.mapError((error) =>
|
||||
error instanceof ResponseError
|
||||
? error
|
||||
: new ResponseError(
|
||||
ErrorCodes.InternalError,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
),
|
||||
),
|
||||
) as Promise<never>,
|
||||
),
|
||||
),
|
||||
(disposable) => Effect.sync(() => disposable.dispose()),
|
||||
).pipe(Effect.asVoid);
|
||||
});
|
||||
|
||||
yield* SubscriptionRef.set(progress, HashSet.empty());
|
||||
const initialized = yield* send(InitializeRequest.type, {
|
||||
processId: process.pid,
|
||||
clientInfo: { name: "lsp-mcp", version: "0.1.0" },
|
||||
clientInfo: { name: "lsp-mcp", version: "0.1.2" },
|
||||
locale: "en",
|
||||
rootPath: options.root,
|
||||
rootUri,
|
||||
@@ -288,8 +343,15 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
settings: options.settings as LSPAny,
|
||||
});
|
||||
}
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.tryPromise(() => connection.sendRequest(ShutdownRequest.type)).pipe(
|
||||
yield* Effect.forEach(
|
||||
documents.values(),
|
||||
(textDocument) =>
|
||||
notify(DidOpenTextDocumentNotification.type, { textDocument }),
|
||||
{ discard: true },
|
||||
);
|
||||
const shutdown = Effect.tryPromise(() =>
|
||||
connection.sendRequest(ShutdownRequest.type),
|
||||
).pipe(
|
||||
Effect.timeout("3 seconds"),
|
||||
Effect.andThen(
|
||||
Effect.tryPromise(() =>
|
||||
@@ -298,8 +360,114 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
),
|
||||
Effect.andThen(Deferred.await(exited).pipe(Effect.timeout("2 seconds"))),
|
||||
Effect.ignore,
|
||||
);
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.suspend(() =>
|
||||
Deferred.isDoneUnsafe(exited) ? Effect.void : shutdown,
|
||||
),
|
||||
);
|
||||
return { initialized, exited, send, notify } satisfies Session;
|
||||
});
|
||||
|
||||
/** Runs one session until its process exits, and fails with the reason. */
|
||||
const serve = Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const session = yield* start().pipe(
|
||||
Effect.tapError((error) => Deferred.fail(initialized, error)),
|
||||
);
|
||||
yield* Deferred.succeed(current, session);
|
||||
yield* Deferred.succeed(initialized, session.initialized);
|
||||
return yield* Deferred.await(session.exited);
|
||||
}),
|
||||
);
|
||||
|
||||
const policy = restarts(limit).pipe(
|
||||
// Only crashes are worth retrying; a server that fails to initialize is broken.
|
||||
Schedule.while(({ input }) => input.code === ErrorCodes.ConnectionInactive),
|
||||
Schedule.tap(({ input, output, duration }) =>
|
||||
announce(
|
||||
"warning",
|
||||
`${input.message}; restarting in ${Duration.format(duration)}${
|
||||
Number.isFinite(limit) ? ` (crash ${output} of ${limit})` : ""
|
||||
}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const giveUp = (error: LspError) => {
|
||||
const reason =
|
||||
error.code !== ErrorCodes.ConnectionInactive
|
||||
? `restarting ${options.command} failed (${error.method}: ${error.message})`
|
||||
: limit === 0
|
||||
? `${error.message}; restarting is disabled`
|
||||
: `${error.message}; that is ${limit} crash${limit === 1 ? "" : "es"} in a row`;
|
||||
const message = `${reason}. Restart lsp-mcp to recover`;
|
||||
return Effect.andThen(
|
||||
announce("error", message),
|
||||
Deferred.fail(current, new LspError({ method: "connection", message })),
|
||||
);
|
||||
};
|
||||
|
||||
yield* serve.pipe(Effect.retryOrElse(policy, giveUp), Effect.forkScoped);
|
||||
const first = yield* Deferred.await(initialized);
|
||||
|
||||
const idle = SubscriptionRef.changes(progress).pipe(
|
||||
Stream.filter(HashSet.isEmpty),
|
||||
Stream.runHead,
|
||||
Effect.timeoutOption("15 seconds"),
|
||||
Effect.asVoid,
|
||||
);
|
||||
|
||||
const request = <P, R>(type: RequestType<P, R, unknown>, params: P) =>
|
||||
session.pipe(
|
||||
Effect.tap(() => idle),
|
||||
Effect.flatMap((session) => session.send(type, params)),
|
||||
Effect.retry({
|
||||
while: (error) => error.retryable,
|
||||
schedule: Schedule.exponential("100 millis").pipe(
|
||||
Schedule.upTo({ times: 5 }),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const notify = <P>(type: NotificationType<P>, params: P) =>
|
||||
Effect.sync(() => track(documents, type.method, params)).pipe(
|
||||
Effect.andThen(session),
|
||||
Effect.flatMap((session) => session.notify(type, params)),
|
||||
Effect.catch((error) => Effect.logWarning(error.message)),
|
||||
);
|
||||
|
||||
const notifications = <P>(type: NotificationType<P>): Stream.Stream<P> =>
|
||||
Stream.fromPubSub(events).pipe(
|
||||
Stream.filter((event) => event.method === type.method),
|
||||
Stream.map((event) => event.params as P),
|
||||
);
|
||||
|
||||
const onRequest = <P, R>(
|
||||
type: RequestType<P, R, unknown>,
|
||||
handler: (params: P) => Effect.Effect<R, unknown>,
|
||||
) =>
|
||||
Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
handlers.set(
|
||||
type.method,
|
||||
(params) =>
|
||||
runPromise(
|
||||
handler(params as P).pipe(
|
||||
Effect.mapError((error) =>
|
||||
error instanceof ResponseError
|
||||
? error
|
||||
: new ResponseError(
|
||||
ErrorCodes.InternalError,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
),
|
||||
),
|
||||
) as Promise<never>,
|
||||
),
|
||||
),
|
||||
() => Effect.sync(() => handlers.delete(type.method)),
|
||||
).pipe(Effect.asVoid);
|
||||
|
||||
const logs = Stream.merge(
|
||||
notifications(LogMessageNotification.type),
|
||||
@@ -311,12 +479,13 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
message,
|
||||
}),
|
||||
),
|
||||
Stream.merge(Stream.fromPubSub(lifecycle)),
|
||||
);
|
||||
|
||||
return LanguageServer.of({
|
||||
root: options.root,
|
||||
capabilities: initialized.capabilities,
|
||||
serverInfo: initialized.serverInfo,
|
||||
capabilities: first.capabilities,
|
||||
serverInfo: first.serverInfo,
|
||||
request,
|
||||
notify,
|
||||
notifications,
|
||||
@@ -325,6 +494,61 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
|
||||
});
|
||||
});
|
||||
|
||||
/** A crash this long after the previous one starts a fresh streak. */
|
||||
const quiet = Duration.minutes(1);
|
||||
|
||||
/**
|
||||
* Restart delays: exponential from 500ms, capped at 30s, giving up once
|
||||
* `limit` crashes happened in a row. The output is the length of the streak.
|
||||
*/
|
||||
const restarts = (limit: number) =>
|
||||
Schedule.fromStepWithMetadata(
|
||||
Effect.sync(() => {
|
||||
let streak = 0;
|
||||
return ({ elapsedSincePrevious }: Schedule.InputMetadata<LspError>) => {
|
||||
streak =
|
||||
elapsedSincePrevious > Duration.toMillis(quiet) ? 1 : streak + 1;
|
||||
return streak > limit
|
||||
? Cause.done(streak)
|
||||
: Effect.succeed<[number, Duration.Duration]>([
|
||||
streak,
|
||||
Duration.millis(Math.min(500 * 2 ** (streak - 1), 30_000)),
|
||||
]);
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
/** Keep the open-document bookkeeping in step with what we tell the server. */
|
||||
const track = (
|
||||
documents: Map<string, TextDocumentItem>,
|
||||
method: string,
|
||||
params: unknown,
|
||||
) => {
|
||||
switch (method) {
|
||||
case DidOpenTextDocumentNotification.method: {
|
||||
const { textDocument } = params as DidOpenTextDocumentParams;
|
||||
documents.set(textDocument.uri, textDocument);
|
||||
break;
|
||||
}
|
||||
case DidChangeTextDocumentNotification.method: {
|
||||
const { textDocument, contentChanges } =
|
||||
params as DidChangeTextDocumentParams;
|
||||
const open = documents.get(textDocument.uri);
|
||||
const last = contentChanges.at(-1);
|
||||
if (open && last && !("range" in last)) {
|
||||
documents.set(textDocument.uri, {
|
||||
...open,
|
||||
version: textDocument.version,
|
||||
text: last.text,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DidCloseTextDocumentNotification.method:
|
||||
documents.delete((params as DidCloseTextDocumentParams).textDocument.uri);
|
||||
}
|
||||
};
|
||||
|
||||
const logLevels: Record<MessageType, LogLevel> = {
|
||||
1: "error",
|
||||
2: "warning",
|
||||
@@ -393,7 +617,10 @@ const clientCapabilities: ClientCapabilities = {
|
||||
deprecatedSupport: true,
|
||||
},
|
||||
completionItemKind: {
|
||||
valueSet: Array.from({ length: 25 }, (_, i) => (i + 1) as never),
|
||||
valueSet: Array.from(
|
||||
{ length: 25 },
|
||||
(_, i) => (i + 1) as CompletionItemKind,
|
||||
),
|
||||
},
|
||||
contextSupport: true,
|
||||
},
|
||||
@@ -440,5 +667,5 @@ const clientCapabilities: ClientCapabilities = {
|
||||
};
|
||||
|
||||
function allSymbolKinds() {
|
||||
return Array.from({ length: 26 }, (_, i) => (i + 1) as never);
|
||||
return Array.from({ length: 26 }, (_, i) => (i + 1) as SymbolKind);
|
||||
}
|
||||
+23
-1
@@ -10,6 +10,14 @@ import { Argument, Command, Flag } from "effect/unstable/cli";
|
||||
import * as Server from "./Server.ts";
|
||||
|
||||
const Json = Flag.withSchema(Schema.fromJsonString(Schema.Json));
|
||||
const crashes = { message: "expected a whole number of crashes" };
|
||||
const Restart = Schema.Union([
|
||||
Schema.Literals(["never", "always"]),
|
||||
Schema.NumberFromString.check(
|
||||
Schema.isInt(crashes),
|
||||
Schema.isGreaterThanOrEqualTo(0, crashes),
|
||||
),
|
||||
]);
|
||||
|
||||
const command = Command.make(
|
||||
"lsp-mcp",
|
||||
@@ -58,6 +66,14 @@ const command = Command.make(
|
||||
),
|
||||
Flag.withDefault(32),
|
||||
),
|
||||
restart: Flag.String("restart").pipe(
|
||||
Flag.withSchema(Restart),
|
||||
Flag.withMetavar("never|always|N"),
|
||||
Flag.withDescription(
|
||||
"When the language server crashes: never start it again, always start it again, or give up after N crashes in a row (default 3)",
|
||||
),
|
||||
Flag.withDefault(3),
|
||||
),
|
||||
command: Argument.String("server").pipe(
|
||||
Argument.withDescription("Language server executable"),
|
||||
),
|
||||
@@ -75,6 +91,7 @@ const command = Command.make(
|
||||
settings,
|
||||
languages,
|
||||
capacity,
|
||||
restart,
|
||||
command,
|
||||
args,
|
||||
}) {
|
||||
@@ -89,6 +106,7 @@ const command = Command.make(
|
||||
root: path.resolve(root),
|
||||
initializationOptions: Option.getOrUndefined(initOptions),
|
||||
settings: Option.getOrUndefined(settings),
|
||||
restart,
|
||||
},
|
||||
documents: { capacity, languages },
|
||||
}),
|
||||
@@ -111,11 +129,15 @@ const command = Command.make(
|
||||
command: `lsp-mcp --settings '{"gopls":{"staticcheck":true}}' -- gopls`,
|
||||
description: "Go with workspace settings",
|
||||
},
|
||||
{
|
||||
command: "lsp-mcp --restart always -- clangd",
|
||||
description: "Keep bringing the server back, however often it crashes",
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
command.pipe(
|
||||
Command.run({ version: "0.1.0" }),
|
||||
Command.run({ version: "0.1.2" }),
|
||||
Effect.tapCause((cause) =>
|
||||
Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logError(cause),
|
||||
),
|
||||
|
||||
+37
-1
@@ -1,7 +1,7 @@
|
||||
import { NodeServices } from "@effect/platform-node";
|
||||
import { assert, describe, it } from "@effect/vitest";
|
||||
import { Effect, FileSystem, Layer } from "effect";
|
||||
import { RequestType } from "vscode-languageserver-protocol";
|
||||
import { NotificationType, RequestType } from "vscode-languageserver-protocol";
|
||||
import { Diagnostics } from "../src/lsp/Diagnostics.ts";
|
||||
import { Documents } from "../src/lsp/Documents.ts";
|
||||
import * as Editor from "../src/lsp/Editor.ts";
|
||||
@@ -12,6 +12,7 @@ const State = new RequestType<null, { open: number; closes: number }, void>(
|
||||
"fixture/state",
|
||||
);
|
||||
const Progress = new RequestType<number, string, void>("fixture/progress");
|
||||
const Crash = new NotificationType<null>("fixture/crash");
|
||||
|
||||
const TestLayer = Layer.mergeAll(
|
||||
Documents.layer({ capacity: 2, languages: {} }),
|
||||
@@ -168,4 +169,39 @@ describe("Editor", () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.layer(
|
||||
Layer.mergeAll(
|
||||
Documents.layer({ capacity: 2, languages: {} }),
|
||||
Diagnostics.layer,
|
||||
).pipe(
|
||||
Layer.provideMerge(
|
||||
workspace({ "a.ts": "const alpha = 1\n" }, { restart: 1 }),
|
||||
),
|
||||
Layer.provideMerge(NodeServices.layer),
|
||||
),
|
||||
{ excludeTestServices: true },
|
||||
)("when the server crashes", (it) => {
|
||||
it.effect(
|
||||
"starts it again with the same documents open, until the policy gives up",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LanguageServer;
|
||||
const hover = Editor.hover({
|
||||
file: "a.ts",
|
||||
line: 1,
|
||||
symbol: "alpha",
|
||||
});
|
||||
yield* hover;
|
||||
|
||||
yield* lsp.notify(Crash, null);
|
||||
assert.strictEqual((yield* hover)?.contents, "word **alpha**");
|
||||
assert.strictEqual((yield* lsp.request(State, null)).open, 1);
|
||||
|
||||
yield* lsp.notify(Crash, null);
|
||||
const error = yield* Effect.flip(hover);
|
||||
assert.match(error.message, /1 crash in a row/);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Vendored
+3
-1
@@ -2,7 +2,8 @@
|
||||
* A miniature language server used by the tests. Words are symbols: hover
|
||||
* describes the word under the cursor, definition is its first occurrence,
|
||||
* references and rename cover every occurrence, and each line containing
|
||||
* "TODO" yields a warning. `fixture/state` reports open/close counts.
|
||||
* "TODO" yields a warning. `fixture/state` reports open/close counts and
|
||||
* `fixture/crash` kills the process.
|
||||
*/
|
||||
import {
|
||||
createConnection,
|
||||
@@ -136,6 +137,7 @@ connection.onDocumentSymbol(({ textDocument }) => {
|
||||
);
|
||||
});
|
||||
|
||||
connection.onNotification("fixture/crash", () => process.exit(1));
|
||||
connection.onRequest("fixture/state", () => ({
|
||||
open: documents.all().length,
|
||||
closes,
|
||||
|
||||
Vendored
+6
-2
@@ -1,11 +1,14 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Effect, FileSystem, Layer, Path } from "effect";
|
||||
import { LanguageServer } from "../../src/lsp/LanguageServer.ts";
|
||||
import { LanguageServer, type Options } from "../../src/lsp/LanguageServer.ts";
|
||||
|
||||
const server = fileURLToPath(new URL("./server.ts", import.meta.url));
|
||||
|
||||
/** A temporary workspace with the given files, served by the fixture language server. */
|
||||
export const workspace = (files: Record<string, string>) =>
|
||||
export const workspace = (
|
||||
files: Record<string, string>,
|
||||
options?: Pick<Options, "restart">,
|
||||
) =>
|
||||
Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem;
|
||||
@@ -18,6 +21,7 @@ export const workspace = (files: Record<string, string>) =>
|
||||
command: process.execPath,
|
||||
args: [server],
|
||||
root,
|
||||
...options,
|
||||
});
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
|
||||
export default defineConfig({
|
||||
entry: "src/main.ts",
|
||||
format: "esm",
|
||||
outDir: "dist",
|
||||
|
||||
deps: {
|
||||
alwaysBundle: ["*"],
|
||||
},
|
||||
|
||||
treeshake: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
});
|
||||
Reference in new issue
Block a user