Skip to content

Commit 410c5f7

Browse files
committed
🧪 Add first failing test
1 parent 540d38e commit 410c5f7

File tree

11 files changed

+413
-25
lines changed

11 files changed

+413
-25
lines changed

cancellation-token.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type Operation, useAbortSignal } from "effection";
2+
import type { CancellationToken } from "vscode-jsonrpc";
3+
4+
export function* useCancellationToken(): Operation<CancellationToken> {
5+
let signal = yield* useAbortSignal();
6+
7+
return {
8+
get isCancellationRequested() {
9+
return signal.aborted;
10+
},
11+
onCancellationRequested(listener) {
12+
signal.addEventListener("abort", listener);
13+
return {
14+
dispose: () => signal.removeEventListener("abort", listener),
15+
};
16+
},
17+
};
18+
}

deno.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
},
88
"imports": {
99
"@effection-contrib/test-adapter": "jsr:@effection-contrib/test-adapter@^0.1.0",
10+
"@std/expect": "jsr:@std/expect@^1.0.13",
1011
"@std/testing": "jsr:@std/testing@^1.0.9",
1112
"effection": "jsr:@effection/effection@^4.0.0-alpha.7",
1213
"@std/assert": "jsr:@std/assert@1",
1314
"vscode-jsonrpc": "npm:vscode-jsonrpc@^8.2.1",
15+
"vscode-languageserver-protocol": "npm:vscode-languageserver-protocol@^3.17.5",
1416
"zod": "npm:zod@^3.24.2",
1517
"zod-opts": "npm:zod-opts@^0.1.8"
1618
},

deno.lock

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

repl.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readline } from "./readline.ts";
33
import process from "node:process";
44
import completions from "./completions.json" with { type: "json" };
55
import { each, spawn } from "effection";
6+
import { useCancellationToken } from "./cancellation-token.ts";
67

78
export function* repl(server: LSPXServer) {
89
yield* spawn(function* () {
@@ -29,13 +30,17 @@ export function* repl(server: LSPXServer) {
2930
let match = pattern.exec(line);
3031
if (match) {
3132
try {
32-
const method = match[1];
33-
let params = JSON.parse(`[${match[2]}]`);
33+
let method = match[1];
34+
let args = JSON.parse(`[${match[2]}]`) as unknown[];
3435
console.log(
35-
`Sending request ${method} with params ${JSON.stringify(params)}...`,
36+
`Sending request ${method} with params ${JSON.stringify(args)}...`,
3637
);
3738

38-
let response = yield* server.request(method, ...params);
39+
let params = args.length === 1 ? args[0] : args;
40+
41+
let token = yield* useCancellationToken();
42+
43+
let response = yield* server.request(method, params as object, token);
3944

4045
console.log(JSON.stringify(response, null, 2));
4146
} catch (e) {

server.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1-
import { call, createSignal, type Operation, resource } from "effection";
2-
import type { Disposable, LSPXServer, Notification } from "./types.ts";
1+
import {
2+
call,
3+
createSignal,
4+
type Operation,
5+
resource,
6+
useScope,
7+
} from "effection";
8+
import type {
9+
Disposable,
10+
LSPXServer,
11+
Notification,
12+
RequestParams,
13+
} from "./types.ts";
314

4-
import { useCommand } from "./use-command.ts";
15+
import { useCommand, useDaemon } from "./use-command.ts";
516
import { useConnection } from "./json-rpc-connection.ts";
6-
import type { MessageConnection } from "vscode-jsonrpc";
17+
import {
18+
ErrorCodes,
19+
type MessageConnection,
20+
ResponseError,
21+
type StarRequestHandler,
22+
} from "vscode-jsonrpc";
723

8-
export interface Options {
9-
interactive: boolean;
24+
export interface LSPXOptions {
25+
interactive?: boolean;
26+
input?: ReadableStream<Uint8Array>;
27+
output?: WritableStream<Uint8Array>;
1028
commands: string[];
1129
}
1230

13-
export function start(opts: Options): Operation<LSPXServer> {
31+
export function start(opts: LSPXOptions): Operation<LSPXServer> {
1432
return resource(function* (provide) {
1533
let notifications = createSignal<Notification, never>();
1634
let connections: MessageConnection[] = [];
1735
let disposables: Disposable[] = [];
36+
1837
try {
1938
for (let command of opts.commands) {
2039
let [exe, ...args] = command.split(/\s/g);
21-
let process = yield* useCommand(exe, {
40+
let process = yield* useDaemon(exe, {
2241
args,
2342
stdin: "piped",
2443
stdout: "piped",
@@ -36,14 +55,28 @@ export function start(opts: Options): Operation<LSPXServer> {
3655
),
3756
);
3857
}
58+
59+
let client = yield* useConnection({
60+
read: opts.input ?? ReadableStream.from([]),
61+
write: opts.output ?? new WritableStream(),
62+
});
63+
64+
let scope = yield* useScope();
65+
let dispatch = createDispatch(connections);
66+
67+
let handler: StarRequestHandler = (...params) =>
68+
scope.run(() => dispatch(...params));
69+
70+
disposables.push(client.onRequest(handler));
71+
3972
yield* provide({
4073
notifications,
41-
*request<T>(method: string, ...params: unknown[]) {
42-
for (let connection of connections) {
43-
return (yield* call(() =>
44-
connection.sendRequest(method, ...params)
45-
)) as T;
74+
*request<T>(...params: RequestParams): Operation<T> {
75+
const result = yield* dispatch<T, unknown>(...params);
76+
if (result instanceof ResponseError) {
77+
throw result;
4678
}
79+
return result;
4780
},
4881
});
4982
} finally {
@@ -53,3 +86,17 @@ export function start(opts: Options): Operation<LSPXServer> {
5386
}
5487
});
5588
}
89+
90+
function createDispatch(
91+
connections: MessageConnection[],
92+
): <T, E>(...params: RequestParams) => Operation<T | ResponseError<E>> {
93+
return function* dispatch(...params) {
94+
for (let connection of connections) {
95+
return (yield* call(() => connection.sendRequest(...params)));
96+
}
97+
return new ResponseError(
98+
ErrorCodes.ServerNotInitialized,
99+
`no active server connections`,
100+
);
101+
};
102+
}

test/bdd.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,91 @@ export function beforeEach(...args: BeforeEachArgs): void {
3434

3535
export type ItBody = Parameters<TestAdapter["runTest"]>[0];
3636

37-
export function it(name: string, body: ItBody): void {
38-
bdd.it<EffectionTestContext>(name, function () {
37+
export function it(name: string, body?: ItBody): void {
38+
if (body) {
39+
bdd.it<EffectionTestContext>(name, function () {
40+
return this["@effectionx/test-adapter"].runTest(body);
41+
});
42+
} else {
43+
bdd.it.skip(name, () => {});
44+
}
45+
}
46+
47+
it.only = (name: string, body: ItBody) => {
48+
bdd.it.only<EffectionTestContext>(name, function () {
3949
return this["@effectionx/test-adapter"].runTest(body);
4050
});
51+
};
52+
53+
import { type Async, expect as $expect, type Expected } from "@std/expect";
54+
import type { Result } from "effection";
55+
56+
interface ExtendedExpected<IsAsync = false> extends Expected<IsAsync> {
57+
toBeErr: (expected?: unknown) => unknown;
58+
toBeOk: (expected?: unknown) => unknown;
59+
60+
// NOTE: You also need to overrides the following typings to allow modifiers to correctly infer typing
61+
not: IsAsync extends true ? Async<ExtendedExpected<true>>
62+
: ExtendedExpected<false>;
63+
resolves: Async<ExtendedExpected<true>>;
64+
rejects: Async<ExtendedExpected<true>>;
65+
}
66+
67+
export const expect = $expect<ExtendedExpected>;
68+
69+
$expect.extend({
70+
toBeOk(context, options?: unknown) {
71+
let { value } = context;
72+
let pass = isOk(value) &&
73+
(typeof options === "undefined" || options === value.value);
74+
75+
if (context.isNot) {
76+
return {
77+
pass,
78+
message: () => `Expected NOT to be ok, but was Ok(${value})`,
79+
};
80+
} else {
81+
let actually = isErr(value)
82+
? `was Err(${value.error})`
83+
: `${value} is not a Result`;
84+
return {
85+
pass,
86+
message: () => `Expected to be Ok(${options ?? ""}) but ${actually}`,
87+
};
88+
}
89+
},
90+
toBeErr(context, options?: string) {
91+
let { value } = context;
92+
let pass = isErr(value) &&
93+
(typeof options === "undefined" || options === value.error.message);
94+
if (context.isNot) {
95+
return {
96+
pass,
97+
message: () =>
98+
`Expected NOT to be an error, but was Err(${
99+
(value as { error: Error }).error
100+
})`,
101+
};
102+
} else {
103+
let actually = isOk(value)
104+
? `was Ok(${value.value})`
105+
: isErr(value)
106+
? `but was Err(${value.error})`
107+
: `${value} is not a Result`;
108+
return {
109+
pass,
110+
message: () => `Expected to be Err(${options}), but ${actually}`,
111+
};
112+
}
113+
},
114+
});
115+
116+
function isOk<T>(value: unknown): value is Result<T> & { ok: true } {
117+
let result = value as Result<T>;
118+
return result.ok;
119+
}
120+
121+
function isErr<T>(value: unknown): value is { error: Error } {
122+
let result = value as Result<T>;
123+
return !result.ok && typeof result.error !== "undefined";
41124
}

0 commit comments

Comments
 (0)