@dreki-gg/pi-lsp 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -0
- package/extensions/lsp/config.ts +133 -111
- package/extensions/lsp/effects/command.ts +42 -0
- package/extensions/lsp/effects/filesystem.ts +50 -0
- package/extensions/lsp/effects/runtime.ts +21 -0
- package/extensions/lsp/errors.ts +113 -0
- package/extensions/lsp/index.ts +24 -8
- package/extensions/lsp/tools/programs.ts +293 -0
- package/extensions/lsp/tools.ts +19 -231
- package/package.json +4 -1
- package/test/config.test.ts +34 -0
- package/test/effects.test.ts +107 -0
- package/test/index.test.ts +3 -2
- package/test/mock-lsp-server.ts +2 -8
- package/test/tools.test.ts +10 -10
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Effect, Layer } from 'effect';
|
|
3
|
+
|
|
4
|
+
import { loadConfigEffect, scaffoldGlobalConfigEffect } from '../extensions/lsp/config';
|
|
5
|
+
import { CommandResolver, type CommandAvailability } from '../extensions/lsp/effects/command';
|
|
6
|
+
import { FileSystem, type FileSystemService } from '../extensions/lsp/effects/filesystem';
|
|
7
|
+
import { ConfigReadError } from '../extensions/lsp/errors';
|
|
8
|
+
|
|
9
|
+
/** Build an in-memory FileSystem service over a path→content map. */
|
|
10
|
+
function fakeFs(files: Record<string, string>, writes?: Record<string, string>): FileSystemService {
|
|
11
|
+
return {
|
|
12
|
+
readTextFile: (path) =>
|
|
13
|
+
path in files
|
|
14
|
+
? Effect.succeed(files[path])
|
|
15
|
+
: Effect.fail(new ConfigReadError({ path, cause: new Error('ENOENT') })),
|
|
16
|
+
fileExists: (path) => Effect.succeed(path in files),
|
|
17
|
+
writeTextFile: (path, content) =>
|
|
18
|
+
Effect.sync(() => {
|
|
19
|
+
if (writes) writes[path] = content;
|
|
20
|
+
files[path] = content;
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fakeResolver(availability: CommandAvailability) {
|
|
26
|
+
return Layer.succeed(CommandResolver, { resolve: () => Effect.succeed(availability) });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('effect services', () => {
|
|
30
|
+
test('loadConfigEffect resolves servers via injected services (no disk, no shell)', async () => {
|
|
31
|
+
const home = process.env.HOME ?? '';
|
|
32
|
+
const globalPath = `${home}/.pi/agent/extensions/lsp/config.json`;
|
|
33
|
+
const fs = fakeFs({
|
|
34
|
+
[globalPath]: JSON.stringify({
|
|
35
|
+
lsp: { ts: { command: ['ts-ls', '--stdio'], extensions: ['.ts'] } },
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const layer = Layer.merge(Layer.succeed(FileSystem, fs), fakeResolver('global'));
|
|
40
|
+
const config = await Effect.runPromise(
|
|
41
|
+
loadConfigEffect('/workspace').pipe(Effect.provide(layer)),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(config.globalDisabled).toBe(false);
|
|
45
|
+
expect(config.servers).toEqual([
|
|
46
|
+
{
|
|
47
|
+
name: 'ts',
|
|
48
|
+
command: 'ts-ls',
|
|
49
|
+
args: ['--stdio'],
|
|
50
|
+
extensions: ['.ts'],
|
|
51
|
+
env: {},
|
|
52
|
+
initializationOptions: {},
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('loadConfigEffect rewrites command to npx when only available via npx', async () => {
|
|
58
|
+
const home = process.env.HOME ?? '';
|
|
59
|
+
const globalPath = `${home}/.pi/agent/extensions/lsp/config.json`;
|
|
60
|
+
const fs = fakeFs({
|
|
61
|
+
[globalPath]: JSON.stringify({
|
|
62
|
+
lsp: { ts: { command: ['ts-ls', '--stdio'], extensions: ['.ts'] } },
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const layer = Layer.merge(Layer.succeed(FileSystem, fs), fakeResolver('npx'));
|
|
67
|
+
const config = await Effect.runPromise(
|
|
68
|
+
loadConfigEffect('/workspace').pipe(Effect.provide(layer)),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(config.servers[0]?.command).toBe('npx');
|
|
72
|
+
expect(config.servers[0]?.args).toEqual(['--yes', 'ts-ls', '--stdio']);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('loadConfigEffect drops servers whose command cannot be resolved', async () => {
|
|
76
|
+
const home = process.env.HOME ?? '';
|
|
77
|
+
const globalPath = `${home}/.pi/agent/extensions/lsp/config.json`;
|
|
78
|
+
const fs = fakeFs({
|
|
79
|
+
[globalPath]: JSON.stringify({
|
|
80
|
+
lsp: { ts: { command: ['missing-ls'], extensions: ['.ts'] } },
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const layer = Layer.merge(Layer.succeed(FileSystem, fs), fakeResolver(null));
|
|
85
|
+
const config = await Effect.runPromise(
|
|
86
|
+
loadConfigEffect('/workspace').pipe(Effect.provide(layer)),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(config.servers).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('scaffoldGlobalConfigEffect writes the starter template when nothing exists', async () => {
|
|
93
|
+
const writes: Record<string, string> = {};
|
|
94
|
+
const fs = fakeFs({}, writes);
|
|
95
|
+
|
|
96
|
+
const created = await Effect.runPromise(
|
|
97
|
+
scaffoldGlobalConfigEffect('/workspace').pipe(Effect.provideService(FileSystem, fs)),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(created).toBe(true);
|
|
101
|
+
const written = Object.values(writes)[0] ?? '';
|
|
102
|
+
const parsed = JSON.parse(written);
|
|
103
|
+
for (const server of Object.values(parsed.lsp as Record<string, { disabled?: boolean }>)) {
|
|
104
|
+
expect(server.disabled).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
package/test/index.test.ts
CHANGED
|
@@ -107,8 +107,9 @@ describe('extension/session layer', () => {
|
|
|
107
107
|
|
|
108
108
|
const globalConfigPath = join(home, '.pi', 'agent', 'extensions', 'lsp', 'config.json');
|
|
109
109
|
expect(await fileExists(globalConfigPath)).toBe(true);
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
const parsed = JSON.parse(await readFile(globalConfigPath, 'utf8'));
|
|
111
|
+
// typescript is present only as a disabled example — never auto-enabled.
|
|
112
|
+
expect(parsed.lsp.typescript.disabled).toBe(true);
|
|
112
113
|
expect(ui.notifications.some((n) => n.includes('created starter config'))).toBe(true);
|
|
113
114
|
});
|
|
114
115
|
|
package/test/mock-lsp-server.ts
CHANGED
|
@@ -251,10 +251,7 @@ function handleDocSync(uri: string, text: string) {
|
|
|
251
251
|
function handleNotification(msg: JsonRpcNotification) {
|
|
252
252
|
switch (msg.method) {
|
|
253
253
|
case 'textDocument/didOpen':
|
|
254
|
-
handleDocSync(
|
|
255
|
-
msg.params?.textDocument?.uri ?? lastUri,
|
|
256
|
-
msg.params?.textDocument?.text ?? '',
|
|
257
|
-
);
|
|
254
|
+
handleDocSync(msg.params?.textDocument?.uri ?? lastUri, msg.params?.textDocument?.text ?? '');
|
|
258
255
|
break;
|
|
259
256
|
case 'textDocument/didChange':
|
|
260
257
|
handleDocSync(
|
|
@@ -263,10 +260,7 @@ function handleNotification(msg: JsonRpcNotification) {
|
|
|
263
260
|
);
|
|
264
261
|
break;
|
|
265
262
|
case 'textDocument/didSave':
|
|
266
|
-
handleDocSync(
|
|
267
|
-
msg.params?.textDocument?.uri ?? lastUri,
|
|
268
|
-
msg.params?.text ?? '',
|
|
269
|
-
);
|
|
263
|
+
handleDocSync(msg.params?.textDocument?.uri ?? lastUri, msg.params?.text ?? '');
|
|
270
264
|
break;
|
|
271
265
|
case 'exit':
|
|
272
266
|
process.exit(0);
|
package/test/tools.test.ts
CHANGED
|
@@ -139,17 +139,17 @@ describe('unified lsp tool dispatch', () => {
|
|
|
139
139
|
emptyManager({
|
|
140
140
|
anyClient: () => ({
|
|
141
141
|
workspaceSymbol: async (query: string) => [
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
{
|
|
143
|
+
name: `${query}Service`,
|
|
144
|
+
kind: 5,
|
|
145
|
+
location: {
|
|
146
|
+
uri: 'file:///repo/src/service.ts',
|
|
147
|
+
range: { start: { line: 9, character: 0 }, end: { line: 9, character: 5 } },
|
|
148
|
+
},
|
|
149
|
+
containerName: 'services',
|
|
148
150
|
},
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
],
|
|
152
|
-
}),
|
|
151
|
+
],
|
|
152
|
+
}),
|
|
153
153
|
}),
|
|
154
154
|
);
|
|
155
155
|
|