Skip to content

Commit 1b609bc

Browse files
committed
🎉initial commit
0 parents  commit 1b609bc

File tree

7 files changed

+385
-0
lines changed

7 files changed

+385
-0
lines changed

.github/workflows/verify.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Verify
2+
3+
on:
4+
push:
5+
branches: main
6+
pull_request:
7+
branches: main
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
fmt:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: checkout
18+
uses: actions/checkout@v3
19+
20+
- name: setup deno
21+
uses: denoland/setup-deno@v2
22+
with:
23+
deno-version: v2.x
24+
25+
- name: format
26+
run: deno fmt --check
27+
lint:
28+
runs-on: ubuntu-latest
29+
30+
steps:
31+
- name: checkout
32+
uses: actions/checkout@v3
33+
34+
- name: setup deno
35+
uses: denoland/setup-deno@v2
36+
with:
37+
deno-version: v2.x
38+
39+
- name: lint
40+
run: deno lint
41+
42+
test:
43+
runs-on: ubuntu-latest
44+
45+
steps:
46+
- name: checkout
47+
uses: actions/checkout@v3
48+
49+
- name: setup deno
50+
uses: denoland/setup-deno@v2
51+
with:
52+
deno-version: v2.x
53+
54+
- name: test
55+
run: deno task test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/lspx

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# lspx
2+
3+
`lspx` is a language server multiplexer, supervisor, and interactive shell.
4+
5+
```
6+
Usage: lspx [options]
7+
8+
Options:
9+
-h, --help Show help
10+
-i, --interactive start an interactive session with a multiplexed system (default: false)
11+
--lsp <string ...> start and muliplex a server with specified command string [required]
12+
```
13+
14+
## multiplexer
15+
16+
There are often many language servers active for a given file. For example, if
17+
you are editing some TypeScript for the web, you might want to run some
18+
combination of the following:
19+
20+
- __typescript_ (ts) resolve symbols, provide refactorings
21+
- _tailwind_ (css) provides completion for tailwind utility clases
22+
- _eslint_ (ts) highlight warnings and errors based on project linting settings
23+
24+
In order to provide this union of functionality, IDEs like VSCode must manage
25+
four separate language server processes and then handle the dispatch and
26+
synchronization of all edits and user inputs to each one. What this means in
27+
practice is that in the example above, if you hover over a symbol, that hover
28+
should be sent to each of the typescript, tailwind, htmx, and eslint servers.
29+
Then any hints, overlays that any of them have should be collated and displayed
30+
at that point. This is a complex process, and furthermore it is required that it
31+
be duplicated inside every single IDE that wants to use more than one language
32+
server per buffer.
33+
34+
`lspx` combines the capabilities of any number language servers into one, so
35+
that each IDE only needs to interact with a single LSP connection.
36+
37+
To run the three language servers above in unison:
38+
39+
```
40+
lspx --lsp "typescript-language-server --stdio" --lsp "tailwindcss-intellisense" --lsp "eslint-lsp --stdio"
41+
```
42+
43+
## supervisor
44+
45+
`lspx` manages the language server processes that it proxies and will attempt to
46+
restart them if they fail. How many times and at what interval is configurable.
47+
48+
## shell
49+
50+
`lspx` allows you to send commands by to the set of lsp, and print their
51+
responses
52+
53+
```
54+
lspx --lsp "deno lsp" --lsp "tailwindcss-intellisense"
55+
56+
lspx
57+
|
58+
+-> deno lsp
59+
+-> tailwindcss-intellisense
60+
61+
LSP> initialize({ "capabilities": {} })
62+
```

completions.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
[
2+
"textDocument/implementation",
3+
"textDocument/typeDefinition",
4+
"workspace/workspaceFolders",
5+
"workspace/configuration",
6+
"textDocument/documentColor",
7+
"textDocument/colorPresentation",
8+
"textDocument/foldingRange",
9+
"workspace/foldingRange/refresh",
10+
"textDocument/declaration",
11+
"textDocument/selectionRange",
12+
"window/workDoneProgress/create",
13+
"textDocument/prepareCallHierarchy",
14+
"callHierarchy/incomingCalls",
15+
"callHierarchy/outgoingCalls",
16+
"textDocument/semanticTokens/full",
17+
"textDocument/semanticTokens/full/delta",
18+
"textDocument/semanticTokens/range",
19+
"workspace/semanticTokens/refresh",
20+
"window/showDocument",
21+
"textDocument/linkedEditingRange",
22+
"workspace/willCreateFiles",
23+
"workspace/willRenameFiles",
24+
"workspace/willDeleteFiles",
25+
"textDocument/moniker",
26+
"textDocument/prepareTypeHierarchy",
27+
"typeHierarchy/supertypes",
28+
"typeHierarchy/subtypes",
29+
"textDocument/inlineValue",
30+
"workspace/inlineValue/refresh",
31+
"textDocument/inlayHint",
32+
"inlayHint/resolve",
33+
"workspace/inlayHint/refresh",
34+
"textDocument/diagnostic",
35+
"workspace/diagnostic",
36+
"workspace/diagnostic/refresh",
37+
"textDocument/inlineCompletion",
38+
"client/registerCapability",
39+
"client/unregisterCapability",
40+
"initialize",
41+
"shutdown",
42+
"window/showMessageRequest",
43+
"textDocument/willSaveWaitUntil",
44+
"textDocument/completion",
45+
"completionItem/resolve",
46+
"textDocument/hover",
47+
"textDocument/signatureHelp",
48+
"textDocument/definition",
49+
"textDocument/references",
50+
"textDocument/documentHighlight",
51+
"textDocument/documentSymbol",
52+
"textDocument/codeAction",
53+
"codeAction/resolve",
54+
"workspace/symbol",
55+
"workspaceSymbol/resolve",
56+
"textDocument/codeLens",
57+
"codeLens/resolve",
58+
"workspace/codeLens/refresh",
59+
"textDocument/documentLink",
60+
"documentLink/resolve",
61+
"textDocument/formatting",
62+
"textDocument/rangeFormatting",
63+
"textDocument/rangesFormatting",
64+
"textDocument/onTypeFormatting",
65+
"textDocument/rename",
66+
"textDocument/prepareRename",
67+
"workspace/executeCommand",
68+
"workspace/applyEdit"
69+
]

deno.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@frontside/lspx",
3+
"tasks": {
4+
"dev": "deno run --allow-env --allow-run main.ts",
5+
"compile": "deno compile -o lspx --allow-env --allow-run main.ts"
6+
},
7+
"imports": {
8+
"effection": "jsr:@effection/effection@^4.0.0-alpha.7",
9+
"@std/assert": "jsr:@std/assert@1",
10+
"vscode-jsonrpc": "npm:vscode-jsonrpc@^8.2.1",
11+
"zod": "npm:zod@^3.24.2",
12+
"zod-opts": "npm:zod-opts@^0.1.8"
13+
},
14+
"exports": "./mod.ts",
15+
"lint": {
16+
"rules": {
17+
"exclude": ["prefer-const", "require-yield"]
18+
}
19+
}
20+
}

deno.lock

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

main.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
call,
3+
createSignal,
4+
main,
5+
type Operation,
6+
resource,
7+
type Stream,
8+
} from "effection";
9+
10+
import * as rpc from "vscode-jsonrpc/node.js";
11+
import { Readable, Writable } from "node:stream";
12+
import { createInterface } from "node:readline";
13+
import * as z from "zod";
14+
import { parser } from "zod-opts";
15+
16+
await main(function* (argv) {
17+
let opts = parser()
18+
.name("lspx")
19+
.options({
20+
"interactive": {
21+
type: z.boolean().default(false),
22+
alias: "i",
23+
description: "start an interactive session with a multiplexed system",
24+
},
25+
"lsp": {
26+
type: z.array(z.string()),
27+
description: "start and muliplex a server with specified command string",
28+
},
29+
}).parse(argv);
30+
console.log(opts);
31+
});
32+
33+
export function useCommand(
34+
...params: ConstructorParameters<typeof Deno.Command>
35+
): Operation<Deno.ChildProcess> {
36+
let [name, options] = params;
37+
return resource(function* (provide) {
38+
let controller = new AbortController();
39+
let { signal } = controller;
40+
let command = new Deno.Command(name, {
41+
...options,
42+
signal,
43+
});
44+
let process = command.spawn();
45+
try {
46+
yield* provide(process);
47+
} finally {
48+
controller.abort();
49+
yield* call(() => process.status);
50+
}
51+
});
52+
}
53+
54+
export interface ConnectionOptions {
55+
write: WritableStream<Uint8Array>;
56+
read: ReadableStream<Uint8Array>;
57+
}
58+
59+
export function useConnection(
60+
options: ConnectionOptions,
61+
): Operation<rpc.MessageConnection> {
62+
return resource(function* (provide) {
63+
const connection = rpc.createMessageConnection(
64+
//@ts-expect-error 🤷
65+
new rpc.StreamMessageReader(Readable.fromWeb(options.read)),
66+
new rpc.StreamMessageWriter(Writable.fromWeb(options.write)),
67+
);
68+
connection.listen();
69+
try {
70+
yield* provide(connection);
71+
} finally {
72+
connection.dispose();
73+
}
74+
});
75+
}
76+
77+
export function useReadline(
78+
...args: Parameters<typeof createInterface>
79+
): Stream<string, never> {
80+
return resource(function* (provide) {
81+
let rl = createInterface(...args);
82+
let lines = createSignal<string, never>();
83+
84+
let subscription = yield* lines;
85+
try {
86+
rl.on("line", lines.send);
87+
yield* provide({
88+
*next() {
89+
rl.prompt();
90+
return yield* subscription.next();
91+
},
92+
});
93+
} finally {
94+
rl.off("line", lines.send);
95+
rl.close();
96+
}
97+
});
98+
}

0 commit comments

Comments
 (0)