@theupsider/lsp-mcp 1.0.1 → 1.0.7
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/dist/lsp/lifecycle-manager.js +22 -7
- package/dist/lsp/lsp-client.js +57 -4
- package/dist/lsp/server-supervisor.js +17 -18
- package/dist/mcp/__tests__/read-tools.test.js +354 -146
- package/dist/mcp/__tests__/write-tools.test.js +218 -104
- package/dist/mcp/formatters.js +109 -91
- package/dist/mcp/server.js +30 -14
- package/dist/mcp/tools/read-tools.js +104 -58
- package/dist/mcp/tools/shared.js +19 -11
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const promises_1 = require("node:fs/promises");
|
|
4
4
|
const read_tools_1 = require("../tools/read-tools");
|
|
5
5
|
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
|
|
6
|
-
jest.mock(
|
|
7
|
-
stat: jest.fn()
|
|
6
|
+
jest.mock("node:fs/promises", () => ({
|
|
7
|
+
stat: jest.fn(),
|
|
8
8
|
}));
|
|
9
9
|
class FakeRegistrar {
|
|
10
10
|
tools = new Map();
|
|
@@ -12,242 +12,449 @@ class FakeRegistrar {
|
|
|
12
12
|
this.tools.set(name, { description: config.description, handler });
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
describe(
|
|
15
|
+
describe("registerReadTools", () => {
|
|
16
16
|
beforeEach(() => {
|
|
17
17
|
jest.clearAllMocks();
|
|
18
|
-
promises_1.stat.mockResolvedValue({
|
|
18
|
+
promises_1.stat.mockResolvedValue({
|
|
19
|
+
isDirectory: () => true,
|
|
20
|
+
});
|
|
19
21
|
});
|
|
20
|
-
it(
|
|
22
|
+
it("initializes LSP with a valid root and reports health", async () => {
|
|
21
23
|
const registrar = new FakeRegistrar();
|
|
22
24
|
const initializeManager = jest.fn().mockResolvedValue({
|
|
23
|
-
root:
|
|
25
|
+
root: "/workspace",
|
|
24
26
|
health: [
|
|
25
|
-
{ language:
|
|
26
|
-
{ language:
|
|
27
|
-
]
|
|
28
|
-
});
|
|
29
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
{ language: "typescript", status: "ready" },
|
|
28
|
+
{ language: "python", status: "error", error: "missing pylsp" },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
32
|
+
initializeManager,
|
|
33
|
+
});
|
|
34
|
+
await expect(getHandler(registrar, "lsp_init")({ root: "/workspace" })).resolves.toEqual({
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: "Initialized LSP for /workspace. Detected languages: Typescript, Python. LSP servers: 1 started, 1 errors.",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
text: "Initialized LSP for /workspace. Detected languages: Typescript, Python. LSP servers: 1 started, 1 errors.",
|
|
33
42
|
raw: {
|
|
34
|
-
root:
|
|
35
|
-
languages: [
|
|
43
|
+
root: "/workspace",
|
|
44
|
+
languages: ["Typescript", "Python"],
|
|
36
45
|
health: [
|
|
37
|
-
{ language:
|
|
38
|
-
{ language:
|
|
39
|
-
]
|
|
40
|
-
}
|
|
46
|
+
{ language: "typescript", status: "ready" },
|
|
47
|
+
{ language: "python", status: "error", error: "missing pylsp" },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
41
50
|
});
|
|
42
|
-
expect(initializeManager).toHaveBeenCalledWith(
|
|
51
|
+
expect(initializeManager).toHaveBeenCalledWith("/workspace", undefined);
|
|
43
52
|
});
|
|
44
|
-
it(
|
|
53
|
+
it("rejects invalid lsp_init roots clearly", async () => {
|
|
45
54
|
const registrar = new FakeRegistrar();
|
|
46
55
|
const initializeManager = jest.fn();
|
|
47
|
-
promises_1.stat.mockRejectedValueOnce(new Error(
|
|
48
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
promises_1.stat.mockRejectedValueOnce(new Error("ENOENT"));
|
|
57
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
58
|
+
initializeManager,
|
|
59
|
+
});
|
|
60
|
+
await expect(getHandler(registrar, "lsp_init")({ root: "/missing/project" })).resolves.toEqual({
|
|
61
|
+
content: [
|
|
62
|
+
{ type: "text", text: "Project root does not exist: /missing/project" },
|
|
63
|
+
],
|
|
51
64
|
error: true,
|
|
52
|
-
raw: null
|
|
65
|
+
raw: null,
|
|
53
66
|
});
|
|
54
67
|
expect(initializeManager).not.toHaveBeenCalled();
|
|
55
68
|
});
|
|
56
|
-
it(
|
|
69
|
+
it("rejects missing, relative, and non-directory lsp_init roots", async () => {
|
|
57
70
|
const registrar = new FakeRegistrar();
|
|
58
71
|
const initializeManager = jest.fn();
|
|
59
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
73
|
+
initializeManager,
|
|
74
|
+
});
|
|
75
|
+
await expect(getHandler(registrar, "lsp_init")({})).resolves.toEqual({
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: "Project root is required. Provide lsp_init({ root: '/absolute/path' }).",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
62
82
|
error: true,
|
|
63
|
-
raw: null
|
|
83
|
+
raw: null,
|
|
64
84
|
});
|
|
65
|
-
await expect(getHandler(registrar,
|
|
66
|
-
content: [
|
|
85
|
+
await expect(getHandler(registrar, "lsp_init")({ root: "relative/path" })).resolves.toEqual({
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: "Project root must be an absolute path: relative/path",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
67
92
|
error: true,
|
|
68
|
-
raw: null
|
|
93
|
+
raw: null,
|
|
69
94
|
});
|
|
70
|
-
promises_1.stat.mockResolvedValueOnce({
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
promises_1.stat.mockResolvedValueOnce({
|
|
96
|
+
isDirectory: () => false,
|
|
97
|
+
});
|
|
98
|
+
await expect(getHandler(registrar, "lsp_init")({ root: "/workspace/file.ts" })).resolves.toEqual({
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: "Project root is not a directory: /workspace/file.ts",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
73
105
|
error: true,
|
|
74
|
-
raw: null
|
|
106
|
+
raw: null,
|
|
75
107
|
});
|
|
76
108
|
expect(initializeManager).not.toHaveBeenCalled();
|
|
77
109
|
});
|
|
78
|
-
it(
|
|
110
|
+
it("maps lsp_init startup failures into tool errors", async () => {
|
|
79
111
|
const registrar = new FakeRegistrar();
|
|
80
|
-
const initializeManager = jest
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
const initializeManager = jest
|
|
113
|
+
.fn()
|
|
114
|
+
.mockRejectedValue(new Error("Lifecycle start timed out"));
|
|
115
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
116
|
+
initializeManager,
|
|
117
|
+
});
|
|
118
|
+
await expect(getHandler(registrar, "lsp_init")({ root: "/workspace" })).resolves.toEqual({
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: "Operation timed out after 30s — try a more specific query or check the LSP server health",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
84
125
|
error: true,
|
|
85
|
-
raw: null
|
|
126
|
+
raw: null,
|
|
86
127
|
});
|
|
87
128
|
});
|
|
88
|
-
it(
|
|
129
|
+
it("sends didOpen once and returns formatted hover results", async () => {
|
|
89
130
|
const registrar = new FakeRegistrar();
|
|
90
|
-
const client = createClient({ contents:
|
|
131
|
+
const client = createClient({ contents: "hover docs" });
|
|
91
132
|
const lifecycle = createLifecycle({ fileClient: client });
|
|
92
133
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
93
|
-
const hover = await getHandler(registrar,
|
|
94
|
-
await getHandler(registrar,
|
|
134
|
+
const hover = await getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
|
|
135
|
+
await getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
|
|
95
136
|
expect(client.ensureDidOpen).toHaveBeenCalledTimes(2);
|
|
96
|
-
expect(client.ensureDidOpen).toHaveBeenCalledWith(
|
|
97
|
-
expect(client.request).toHaveBeenCalledWith(
|
|
98
|
-
textDocument: { uri:
|
|
99
|
-
position: { line: 2, character: 4 }
|
|
137
|
+
expect(client.ensureDidOpen).toHaveBeenCalledWith("/workspace/src/index.ts");
|
|
138
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/hover", {
|
|
139
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
140
|
+
position: { line: 2, character: 4 },
|
|
100
141
|
}, 5000);
|
|
101
142
|
expect(hover).toEqual({
|
|
102
|
-
content: [{ type:
|
|
103
|
-
raw: { contents:
|
|
143
|
+
content: [{ type: "text", text: "hover docs" }],
|
|
144
|
+
raw: { contents: "hover docs" },
|
|
104
145
|
});
|
|
105
146
|
});
|
|
106
|
-
it(
|
|
147
|
+
it("formats definitions and converts URIs back to paths", async () => {
|
|
107
148
|
const registrar = new FakeRegistrar();
|
|
108
149
|
const lifecycle = createLifecycle({
|
|
109
150
|
fileClient: createClient([
|
|
110
151
|
{
|
|
111
|
-
uri:
|
|
112
|
-
range: {
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
uri: "file:///workspace/src/defs.ts",
|
|
153
|
+
range: {
|
|
154
|
+
start: { line: 3, character: 1 },
|
|
155
|
+
end: { line: 3, character: 2 },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
]),
|
|
115
159
|
});
|
|
116
160
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
117
|
-
await expect(getHandler(registrar,
|
|
118
|
-
content: [
|
|
161
|
+
await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 1, character: 1 })).resolves.toEqual({
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
119
168
|
raw: [
|
|
120
169
|
{
|
|
121
|
-
path:
|
|
122
|
-
range: {
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
path: "/workspace/src/defs.ts",
|
|
171
|
+
range: {
|
|
172
|
+
start: { line: 3, character: 1 },
|
|
173
|
+
end: { line: 3, character: 2 },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
],
|
|
125
177
|
});
|
|
126
178
|
});
|
|
127
|
-
it(
|
|
179
|
+
it("uses the declaration flag when requesting references", async () => {
|
|
128
180
|
const registrar = new FakeRegistrar();
|
|
129
181
|
const client = createClient([]);
|
|
130
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
183
|
+
initializeManager: jest.fn(),
|
|
184
|
+
});
|
|
185
|
+
await getHandler(registrar, "lsp_references")({
|
|
186
|
+
file: "/workspace/src/index.ts",
|
|
187
|
+
line: 0,
|
|
188
|
+
character: 0,
|
|
189
|
+
includeDeclaration: true,
|
|
190
|
+
});
|
|
191
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/references", {
|
|
192
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
134
193
|
position: { line: 0, character: 0 },
|
|
135
|
-
context: { includeDeclaration: true }
|
|
194
|
+
context: { includeDeclaration: true },
|
|
136
195
|
}, 15000);
|
|
137
196
|
});
|
|
138
|
-
it(
|
|
197
|
+
it("merges workspace symbols across ready clients", async () => {
|
|
139
198
|
const registrar = new FakeRegistrar();
|
|
140
|
-
const firstClient = createClient([
|
|
141
|
-
|
|
142
|
-
|
|
199
|
+
const firstClient = createClient([
|
|
200
|
+
{
|
|
201
|
+
name: "UserService",
|
|
202
|
+
kind: 5,
|
|
203
|
+
location: {
|
|
204
|
+
uri: "file:///workspace/src/user.ts",
|
|
205
|
+
range: {
|
|
206
|
+
start: { line: 0, character: 0 },
|
|
207
|
+
end: { line: 0, character: 4 },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
const secondClient = createClient([
|
|
213
|
+
{
|
|
214
|
+
name: "login",
|
|
215
|
+
kind: 12,
|
|
216
|
+
location: {
|
|
217
|
+
uri: "file:///workspace/src/auth.ts",
|
|
218
|
+
range: {
|
|
219
|
+
start: { line: 1, character: 0 },
|
|
220
|
+
end: { line: 1, character: 3 },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
const lifecycle = createLifecycle({
|
|
226
|
+
workspaceClients: [firstClient, secondClient],
|
|
227
|
+
});
|
|
143
228
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
144
|
-
const result = await getHandler(registrar,
|
|
229
|
+
const result = await getHandler(registrar, "lsp_workspace_symbols")({ query: "log" });
|
|
145
230
|
expect(lifecycle.ensureSeedFilesOpen).toHaveBeenCalledTimes(1);
|
|
146
|
-
expect(firstClient.request).toHaveBeenCalledWith(
|
|
147
|
-
expect(secondClient.request).toHaveBeenCalledWith(
|
|
231
|
+
expect(firstClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
|
|
232
|
+
expect(secondClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
|
|
148
233
|
expect(result).toEqual({
|
|
149
|
-
content: [{ type:
|
|
234
|
+
content: [{ type: "text", text: expect.stringContaining("UserService") }],
|
|
150
235
|
raw: [
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
236
|
+
{
|
|
237
|
+
name: "UserService",
|
|
238
|
+
kind: 5,
|
|
239
|
+
path: "/workspace/src/user.ts",
|
|
240
|
+
range: {
|
|
241
|
+
start: { line: 0, character: 0 },
|
|
242
|
+
end: { line: 0, character: 4 },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "login",
|
|
247
|
+
kind: 12,
|
|
248
|
+
path: "/workspace/src/auth.ts",
|
|
249
|
+
range: {
|
|
250
|
+
start: { line: 1, character: 0 },
|
|
251
|
+
end: { line: 1, character: 3 },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
],
|
|
154
255
|
});
|
|
155
256
|
});
|
|
156
|
-
it(
|
|
257
|
+
it("returns cached file diagnostics and aggregates workspace diagnostics", async () => {
|
|
157
258
|
const registrar = new FakeRegistrar();
|
|
158
|
-
const diagnostics = [
|
|
259
|
+
const diagnostics = [
|
|
260
|
+
{
|
|
261
|
+
uri: "file:///workspace/src/index.ts",
|
|
262
|
+
message: "Boom",
|
|
263
|
+
severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Error,
|
|
264
|
+
range: {
|
|
265
|
+
start: { line: 0, character: 0 },
|
|
266
|
+
end: { line: 0, character: 1 },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
];
|
|
159
270
|
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ diagnostics, workspaceClients: [createClient([])] }), { initializeManager: jest.fn() });
|
|
160
|
-
await expect(getHandler(registrar,
|
|
161
|
-
content: [
|
|
162
|
-
|
|
271
|
+
await expect(getHandler(registrar, "lsp_diagnostics")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: expect.stringContaining("File diagnostics: 1 issue(s)"),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
raw: diagnostics,
|
|
163
279
|
});
|
|
164
|
-
await expect(getHandler(registrar,
|
|
165
|
-
content: [
|
|
166
|
-
|
|
280
|
+
await expect(getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" })).resolves.toEqual({
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: expect.stringContaining("Workspace diagnostics: 1 issue(s)"),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
raw: diagnostics,
|
|
167
288
|
});
|
|
168
289
|
});
|
|
169
|
-
it(
|
|
290
|
+
it("returns health instantly without LSP requests", async () => {
|
|
170
291
|
const registrar = new FakeRegistrar();
|
|
171
|
-
const lifecycle = createLifecycle({
|
|
292
|
+
const lifecycle = createLifecycle({
|
|
293
|
+
health: [{ language: "typescript", status: "ready" }],
|
|
294
|
+
});
|
|
172
295
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
173
|
-
await expect(getHandler(registrar,
|
|
174
|
-
content: [
|
|
175
|
-
|
|
296
|
+
await expect(getHandler(registrar, "lsp_health")({})).resolves.toEqual({
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: "| Language | Status | Error |\n| --- | --- | --- |\n| typescript | ready | |",
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
raw: [{ language: "typescript", status: "ready" }],
|
|
176
304
|
});
|
|
177
305
|
});
|
|
178
|
-
it(
|
|
306
|
+
it("supports document symbols, completion lists, and signature help fallbacks", async () => {
|
|
179
307
|
const registrar = new FakeRegistrar();
|
|
180
|
-
const client = createClient({ items: [{ label:
|
|
181
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
308
|
+
const client = createClient({ items: [{ label: "x", kind: 3 }] });
|
|
309
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
310
|
+
initializeManager: jest.fn(),
|
|
311
|
+
});
|
|
312
|
+
await expect(getHandler(registrar, "lsp_completion")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: "Showing 1 of 1 completion item(s)\n\n### Functions\n- `x`",
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
raw: [{ label: "x", kind: 3 }],
|
|
185
320
|
});
|
|
186
|
-
client.request.mockResolvedValueOnce([
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
321
|
+
client.request.mockResolvedValueOnce([
|
|
322
|
+
{
|
|
323
|
+
name: "DocSymbol",
|
|
324
|
+
kind: 5,
|
|
325
|
+
range: {
|
|
326
|
+
start: { line: 0, character: 0 },
|
|
327
|
+
end: { line: 0, character: 1 },
|
|
328
|
+
},
|
|
329
|
+
selectionRange: {
|
|
330
|
+
start: { line: 0, character: 0 },
|
|
331
|
+
end: { line: 0, character: 1 },
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
]);
|
|
335
|
+
await expect(getHandler(registrar, "lsp_document_symbols")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
|
|
336
|
+
content: [{ type: "text", text: "- 📦 `DocSymbol`" }],
|
|
337
|
+
raw: [
|
|
338
|
+
{
|
|
339
|
+
name: "DocSymbol",
|
|
340
|
+
kind: 5,
|
|
341
|
+
range: {
|
|
342
|
+
start: { line: 0, character: 0 },
|
|
343
|
+
end: { line: 0, character: 1 },
|
|
344
|
+
},
|
|
345
|
+
selectionRange: {
|
|
346
|
+
start: { line: 0, character: 0 },
|
|
347
|
+
end: { line: 0, character: 1 },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
],
|
|
190
351
|
});
|
|
191
352
|
client.request.mockResolvedValueOnce(null);
|
|
192
|
-
await expect(getHandler(registrar,
|
|
193
|
-
content: [{ type:
|
|
194
|
-
raw: null
|
|
353
|
+
await expect(getHandler(registrar, "lsp_signature_help")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
354
|
+
content: [{ type: "text", text: "No result" }],
|
|
355
|
+
raw: null,
|
|
195
356
|
});
|
|
196
357
|
});
|
|
197
|
-
it(
|
|
358
|
+
it("supports type and implementation lookups plus empty completion results", async () => {
|
|
198
359
|
const registrar = new FakeRegistrar();
|
|
199
360
|
const client = createClient({
|
|
200
|
-
uri:
|
|
201
|
-
range: {
|
|
361
|
+
uri: "file:///workspace/src/types.ts",
|
|
362
|
+
range: {
|
|
363
|
+
start: { line: 1, character: 2 },
|
|
364
|
+
end: { line: 1, character: 6 },
|
|
365
|
+
},
|
|
202
366
|
});
|
|
203
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
367
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
368
|
+
initializeManager: jest.fn(),
|
|
369
|
+
});
|
|
370
|
+
await expect(getHandler(registrar, "lsp_type_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: "text",
|
|
374
|
+
text: "Found 1 definition: `/workspace/src/types.ts:2:3`",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
raw: [
|
|
378
|
+
{
|
|
379
|
+
path: "/workspace/src/types.ts",
|
|
380
|
+
range: {
|
|
381
|
+
start: { line: 1, character: 2 },
|
|
382
|
+
end: { line: 1, character: 6 },
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
],
|
|
207
386
|
});
|
|
208
387
|
client.request.mockResolvedValueOnce(null);
|
|
209
|
-
await expect(getHandler(registrar,
|
|
210
|
-
content: [{ type:
|
|
211
|
-
raw: null
|
|
388
|
+
await expect(getHandler(registrar, "lsp_implementation")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
389
|
+
content: [{ type: "text", text: "No result" }],
|
|
390
|
+
raw: null,
|
|
212
391
|
});
|
|
213
392
|
client.request.mockResolvedValueOnce(null);
|
|
214
|
-
await expect(getHandler(registrar,
|
|
215
|
-
content: [{ type:
|
|
216
|
-
raw: null
|
|
393
|
+
await expect(getHandler(registrar, "lsp_completion")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
394
|
+
content: [{ type: "text", text: "No result" }],
|
|
395
|
+
raw: null,
|
|
217
396
|
});
|
|
218
|
-
client.request.mockResolvedValueOnce({
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
397
|
+
client.request.mockResolvedValueOnce({
|
|
398
|
+
signatures: [{ label: "fn(x: string)" }],
|
|
399
|
+
});
|
|
400
|
+
await expect(getHandler(registrar, "lsp_signature_help")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: "text",
|
|
404
|
+
text: JSON.stringify({ signatures: [{ label: "fn(x: string)" }] }, null, 2),
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
raw: { signatures: [{ label: "fn(x: string)" }] },
|
|
222
408
|
});
|
|
223
409
|
});
|
|
224
|
-
it(
|
|
410
|
+
it("turns LSP timeouts into retry guidance", async () => {
|
|
225
411
|
const registrar = new FakeRegistrar();
|
|
226
|
-
const client = createClient(new Error(
|
|
227
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
228
|
-
|
|
229
|
-
|
|
412
|
+
const client = createClient(new Error("LSP request timed out: textDocument/hover"));
|
|
413
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
414
|
+
initializeManager: jest.fn(),
|
|
415
|
+
});
|
|
416
|
+
await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
417
|
+
content: [
|
|
418
|
+
{
|
|
419
|
+
type: "text",
|
|
420
|
+
text: "Operation timed out after 5s — try a more specific query or check the LSP server health",
|
|
421
|
+
},
|
|
422
|
+
],
|
|
230
423
|
error: true,
|
|
231
|
-
raw: null
|
|
424
|
+
raw: null,
|
|
232
425
|
});
|
|
233
426
|
});
|
|
234
|
-
it(
|
|
427
|
+
it("returns a no-server error when no language server matches the file", async () => {
|
|
235
428
|
const registrar = new FakeRegistrar();
|
|
236
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
237
|
-
|
|
238
|
-
|
|
429
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
430
|
+
initializeManager: jest.fn(),
|
|
431
|
+
});
|
|
432
|
+
await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: "text",
|
|
436
|
+
text: "No language server available for .md files. Run lsp_health for details.",
|
|
437
|
+
},
|
|
438
|
+
],
|
|
239
439
|
error: true,
|
|
240
|
-
raw: null
|
|
440
|
+
raw: null,
|
|
241
441
|
});
|
|
242
442
|
});
|
|
243
|
-
it(
|
|
443
|
+
it("returns restart guidance when the LSP crashed", async () => {
|
|
244
444
|
const registrar = new FakeRegistrar();
|
|
245
|
-
const client = createClient(new Error(
|
|
246
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
247
|
-
|
|
248
|
-
|
|
445
|
+
const client = createClient(new Error("LSP server exited unexpectedly (code: 1, signal: null)"));
|
|
446
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
447
|
+
initializeManager: jest.fn(),
|
|
448
|
+
});
|
|
449
|
+
await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: "Der Language Server ist neu gestartet, bitte versuche es erneut.",
|
|
454
|
+
},
|
|
455
|
+
],
|
|
249
456
|
error: true,
|
|
250
|
-
raw: null
|
|
457
|
+
raw: null,
|
|
251
458
|
});
|
|
252
459
|
});
|
|
253
460
|
});
|
|
@@ -262,11 +469,12 @@ function createLifecycle(options) {
|
|
|
262
469
|
return {
|
|
263
470
|
getClientForFile: jest.fn((_) => options.fileClient ?? null),
|
|
264
471
|
getReadyClients: jest.fn((_) => options.workspaceClients ?? []),
|
|
265
|
-
getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri ===
|
|
472
|
+
getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === "file:///workspace/src/index.ts")),
|
|
266
473
|
getWorkspaceDiagnostics: jest.fn((_) => options.diagnostics ?? []),
|
|
267
474
|
getHealth: jest.fn(() => options.health ?? []),
|
|
268
475
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
269
|
-
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
|
|
476
|
+
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
477
|
+
analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
|
|
270
478
|
};
|
|
271
479
|
}
|
|
272
480
|
function createClient(result) {
|
|
@@ -278,6 +486,6 @@ function createClient(result) {
|
|
|
278
486
|
getCapabilities: jest.fn(() => ({ renameProvider: true })),
|
|
279
487
|
ensureDidOpen: jest.fn().mockResolvedValue(undefined),
|
|
280
488
|
waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
|
|
281
|
-
ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
|
|
489
|
+
ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined),
|
|
282
490
|
};
|
|
283
491
|
}
|