@theupsider/lsp-mcp 1.0.1 → 1.0.8
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/README.md +2 -6
- 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 +346 -151
- package/dist/mcp/__tests__/server.test.js +6 -6
- package/dist/mcp/__tests__/write-tools.test.js +190 -126
- package/dist/mcp/formatters.js +109 -91
- package/dist/mcp/server.js +30 -14
- package/dist/mcp/tools/read-tools.js +82 -78
- package/dist/mcp/tools/shared.js +19 -11
- package/dist/mcp/tools/write-tools.js +0 -13
- 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,436 @@ 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,
|
|
94
|
+
});
|
|
95
|
+
promises_1.stat.mockResolvedValueOnce({
|
|
96
|
+
isDirectory: () => false,
|
|
69
97
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 and returns formatted definition results", async () => {
|
|
89
130
|
const registrar = new FakeRegistrar();
|
|
90
|
-
const client = createClient({
|
|
131
|
+
const client = createClient({
|
|
132
|
+
uri: "file:///workspace/src/defs.ts",
|
|
133
|
+
range: {
|
|
134
|
+
start: { line: 3, character: 1 },
|
|
135
|
+
end: { line: 3, character: 2 },
|
|
136
|
+
},
|
|
137
|
+
});
|
|
91
138
|
const lifecycle = createLifecycle({ fileClient: client });
|
|
92
139
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
93
|
-
const
|
|
94
|
-
await getHandler(registrar,
|
|
140
|
+
const definition = await getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
|
|
141
|
+
await getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
|
|
95
142
|
expect(client.ensureDidOpen).toHaveBeenCalledTimes(2);
|
|
96
|
-
expect(client.ensureDidOpen).toHaveBeenCalledWith(
|
|
97
|
-
expect(client.request).toHaveBeenCalledWith(
|
|
98
|
-
textDocument: { uri:
|
|
99
|
-
position: { line: 2, character: 4 }
|
|
143
|
+
expect(client.ensureDidOpen).toHaveBeenCalledWith("/workspace/src/index.ts");
|
|
144
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/definition", {
|
|
145
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
146
|
+
position: { line: 2, character: 4 },
|
|
100
147
|
}, 5000);
|
|
101
|
-
expect(
|
|
102
|
-
content: [
|
|
103
|
-
|
|
148
|
+
expect(definition).toEqual({
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
raw: [
|
|
156
|
+
{
|
|
157
|
+
path: "/workspace/src/defs.ts",
|
|
158
|
+
range: {
|
|
159
|
+
start: { line: 3, character: 1 },
|
|
160
|
+
end: { line: 3, character: 2 },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
],
|
|
104
164
|
});
|
|
105
165
|
});
|
|
106
|
-
it(
|
|
166
|
+
it("formats definitions and converts URIs back to paths", async () => {
|
|
107
167
|
const registrar = new FakeRegistrar();
|
|
108
168
|
const lifecycle = createLifecycle({
|
|
109
169
|
fileClient: createClient([
|
|
110
170
|
{
|
|
111
|
-
uri:
|
|
112
|
-
range: {
|
|
113
|
-
|
|
114
|
-
|
|
171
|
+
uri: "file:///workspace/src/defs.ts",
|
|
172
|
+
range: {
|
|
173
|
+
start: { line: 3, character: 1 },
|
|
174
|
+
end: { line: 3, character: 2 },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
]),
|
|
115
178
|
});
|
|
116
179
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
117
|
-
await expect(getHandler(registrar,
|
|
118
|
-
content: [
|
|
180
|
+
await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 1, character: 1 })).resolves.toEqual({
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
119
187
|
raw: [
|
|
120
188
|
{
|
|
121
|
-
path:
|
|
122
|
-
range: {
|
|
123
|
-
|
|
124
|
-
|
|
189
|
+
path: "/workspace/src/defs.ts",
|
|
190
|
+
range: {
|
|
191
|
+
start: { line: 3, character: 1 },
|
|
192
|
+
end: { line: 3, character: 2 },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
],
|
|
125
196
|
});
|
|
126
197
|
});
|
|
127
|
-
it(
|
|
198
|
+
it("uses the declaration flag when requesting references", async () => {
|
|
128
199
|
const registrar = new FakeRegistrar();
|
|
129
200
|
const client = createClient([]);
|
|
130
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
201
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
202
|
+
initializeManager: jest.fn(),
|
|
203
|
+
});
|
|
204
|
+
await getHandler(registrar, "lsp_references")({
|
|
205
|
+
file: "/workspace/src/index.ts",
|
|
206
|
+
line: 0,
|
|
207
|
+
character: 0,
|
|
208
|
+
includeDeclaration: true,
|
|
209
|
+
});
|
|
210
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/references", {
|
|
211
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
134
212
|
position: { line: 0, character: 0 },
|
|
135
|
-
context: { includeDeclaration: true }
|
|
213
|
+
context: { includeDeclaration: true },
|
|
136
214
|
}, 15000);
|
|
137
215
|
});
|
|
138
|
-
it(
|
|
216
|
+
it("merges workspace symbols across ready clients", async () => {
|
|
139
217
|
const registrar = new FakeRegistrar();
|
|
140
|
-
const firstClient = createClient([
|
|
141
|
-
|
|
142
|
-
|
|
218
|
+
const firstClient = createClient([
|
|
219
|
+
{
|
|
220
|
+
name: "UserService",
|
|
221
|
+
kind: 5,
|
|
222
|
+
location: {
|
|
223
|
+
uri: "file:///workspace/src/user.ts",
|
|
224
|
+
range: {
|
|
225
|
+
start: { line: 0, character: 0 },
|
|
226
|
+
end: { line: 0, character: 4 },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
const secondClient = createClient([
|
|
232
|
+
{
|
|
233
|
+
name: "login",
|
|
234
|
+
kind: 12,
|
|
235
|
+
location: {
|
|
236
|
+
uri: "file:///workspace/src/auth.ts",
|
|
237
|
+
range: {
|
|
238
|
+
start: { line: 1, character: 0 },
|
|
239
|
+
end: { line: 1, character: 3 },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
const lifecycle = createLifecycle({
|
|
245
|
+
workspaceClients: [firstClient, secondClient],
|
|
246
|
+
});
|
|
143
247
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
144
|
-
const result = await getHandler(registrar,
|
|
248
|
+
const result = await getHandler(registrar, "lsp_workspace_symbols")({ query: "log" });
|
|
145
249
|
expect(lifecycle.ensureSeedFilesOpen).toHaveBeenCalledTimes(1);
|
|
146
|
-
expect(firstClient.request).toHaveBeenCalledWith(
|
|
147
|
-
expect(secondClient.request).toHaveBeenCalledWith(
|
|
250
|
+
expect(firstClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
|
|
251
|
+
expect(secondClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
|
|
148
252
|
expect(result).toEqual({
|
|
149
|
-
content: [{ type:
|
|
253
|
+
content: [{ type: "text", text: expect.stringContaining("UserService") }],
|
|
150
254
|
raw: [
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
255
|
+
{
|
|
256
|
+
name: "UserService",
|
|
257
|
+
kind: 5,
|
|
258
|
+
path: "/workspace/src/user.ts",
|
|
259
|
+
range: {
|
|
260
|
+
start: { line: 0, character: 0 },
|
|
261
|
+
end: { line: 0, character: 4 },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "login",
|
|
266
|
+
kind: 12,
|
|
267
|
+
path: "/workspace/src/auth.ts",
|
|
268
|
+
range: {
|
|
269
|
+
start: { line: 1, character: 0 },
|
|
270
|
+
end: { line: 1, character: 3 },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
],
|
|
154
274
|
});
|
|
155
275
|
});
|
|
156
|
-
it(
|
|
276
|
+
it("returns cached file diagnostics and aggregates workspace diagnostics", async () => {
|
|
157
277
|
const registrar = new FakeRegistrar();
|
|
158
|
-
const diagnostics = [
|
|
278
|
+
const diagnostics = [
|
|
279
|
+
{
|
|
280
|
+
uri: "file:///workspace/src/index.ts",
|
|
281
|
+
message: "Boom",
|
|
282
|
+
severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Error,
|
|
283
|
+
range: {
|
|
284
|
+
start: { line: 0, character: 0 },
|
|
285
|
+
end: { line: 0, character: 1 },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
];
|
|
159
289
|
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ diagnostics, workspaceClients: [createClient([])] }), { initializeManager: jest.fn() });
|
|
160
|
-
await expect(getHandler(registrar,
|
|
161
|
-
content: [
|
|
162
|
-
|
|
290
|
+
await expect(getHandler(registrar, "lsp_diagnostics")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: expect.stringContaining("File diagnostics: 1 issue(s)"),
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
raw: diagnostics,
|
|
163
298
|
});
|
|
164
|
-
await expect(getHandler(registrar,
|
|
165
|
-
content: [
|
|
166
|
-
|
|
299
|
+
await expect(getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" })).resolves.toEqual({
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: expect.stringContaining("Workspace diagnostics: 1 issue(s)"),
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
raw: diagnostics,
|
|
167
307
|
});
|
|
168
308
|
});
|
|
169
|
-
it(
|
|
309
|
+
it("returns health instantly without LSP requests", async () => {
|
|
170
310
|
const registrar = new FakeRegistrar();
|
|
171
|
-
const lifecycle = createLifecycle({
|
|
311
|
+
const lifecycle = createLifecycle({
|
|
312
|
+
health: [{ language: "typescript", status: "ready" }],
|
|
313
|
+
});
|
|
172
314
|
(0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
|
|
173
|
-
await expect(getHandler(registrar,
|
|
174
|
-
content: [
|
|
175
|
-
|
|
315
|
+
await expect(getHandler(registrar, "lsp_health")({})).resolves.toEqual({
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: "| Language | Status | Error |\n| --- | --- | --- |\n| typescript | ready | |",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
raw: [{ language: "typescript", status: "ready" }],
|
|
176
323
|
});
|
|
177
324
|
});
|
|
178
|
-
it(
|
|
325
|
+
it("supports document symbols", async () => {
|
|
179
326
|
const registrar = new FakeRegistrar();
|
|
180
|
-
const client = createClient(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
327
|
+
const client = createClient([
|
|
328
|
+
{
|
|
329
|
+
name: "DocSymbol",
|
|
330
|
+
kind: 5,
|
|
331
|
+
range: {
|
|
332
|
+
start: { line: 0, character: 0 },
|
|
333
|
+
end: { line: 0, character: 1 },
|
|
334
|
+
},
|
|
335
|
+
selectionRange: {
|
|
336
|
+
start: { line: 0, character: 0 },
|
|
337
|
+
end: { line: 0, character: 1 },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
342
|
+
initializeManager: jest.fn(),
|
|
190
343
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
344
|
+
await expect(getHandler(registrar, "lsp_document_symbols")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
|
|
345
|
+
content: [{ type: "text", text: "- 📦 `DocSymbol`" }],
|
|
346
|
+
raw: [
|
|
347
|
+
{
|
|
348
|
+
name: "DocSymbol",
|
|
349
|
+
kind: 5,
|
|
350
|
+
range: {
|
|
351
|
+
start: { line: 0, character: 0 },
|
|
352
|
+
end: { line: 0, character: 1 },
|
|
353
|
+
},
|
|
354
|
+
selectionRange: {
|
|
355
|
+
start: { line: 0, character: 0 },
|
|
356
|
+
end: { line: 0, character: 1 },
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
],
|
|
195
360
|
});
|
|
196
361
|
});
|
|
197
|
-
it(
|
|
362
|
+
it("supports type and implementation lookups", async () => {
|
|
198
363
|
const registrar = new FakeRegistrar();
|
|
199
364
|
const client = createClient({
|
|
200
|
-
uri:
|
|
201
|
-
range: {
|
|
365
|
+
uri: "file:///workspace/src/types.ts",
|
|
366
|
+
range: {
|
|
367
|
+
start: { line: 1, character: 2 },
|
|
368
|
+
end: { line: 1, character: 6 },
|
|
369
|
+
},
|
|
202
370
|
});
|
|
203
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
204
|
-
|
|
205
|
-
content: [{ type: 'text', text: 'Found 1 definition: `/workspace/src/types.ts:2:3`' }],
|
|
206
|
-
raw: [{ path: '/workspace/src/types.ts', range: { start: { line: 1, character: 2 }, end: { line: 1, character: 6 } } }]
|
|
371
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
372
|
+
initializeManager: jest.fn(),
|
|
207
373
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
374
|
+
await expect(getHandler(registrar, "lsp_type_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
375
|
+
content: [
|
|
376
|
+
{
|
|
377
|
+
type: "text",
|
|
378
|
+
text: "Found 1 definition: `/workspace/src/types.ts:2:3`",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
raw: [
|
|
382
|
+
{
|
|
383
|
+
path: "/workspace/src/types.ts",
|
|
384
|
+
range: {
|
|
385
|
+
start: { line: 1, character: 2 },
|
|
386
|
+
end: { line: 1, character: 6 },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
],
|
|
212
390
|
});
|
|
213
391
|
client.request.mockResolvedValueOnce(null);
|
|
214
|
-
await expect(getHandler(registrar,
|
|
215
|
-
content: [{ type:
|
|
216
|
-
raw: null
|
|
217
|
-
});
|
|
218
|
-
client.request.mockResolvedValueOnce({ signatures: [{ label: 'fn(x: string)' }] });
|
|
219
|
-
await expect(getHandler(registrar, 'lsp_signature_help')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
|
|
220
|
-
content: [{ type: 'text', text: JSON.stringify({ signatures: [{ label: 'fn(x: string)' }] }, null, 2) }],
|
|
221
|
-
raw: { signatures: [{ label: 'fn(x: string)' }] }
|
|
392
|
+
await expect(getHandler(registrar, "lsp_implementation")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
393
|
+
content: [{ type: "text", text: "No result" }],
|
|
394
|
+
raw: null,
|
|
222
395
|
});
|
|
223
396
|
});
|
|
224
|
-
it(
|
|
397
|
+
it("turns LSP timeouts into retry guidance", async () => {
|
|
225
398
|
const registrar = new FakeRegistrar();
|
|
226
|
-
const client = createClient(new Error(
|
|
227
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
228
|
-
|
|
229
|
-
|
|
399
|
+
const client = createClient(new Error("LSP request timed out: textDocument/definition"));
|
|
400
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
401
|
+
initializeManager: jest.fn(),
|
|
402
|
+
});
|
|
403
|
+
await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: "Operation timed out after 5s — try a more specific query or check the LSP server health",
|
|
408
|
+
},
|
|
409
|
+
],
|
|
230
410
|
error: true,
|
|
231
|
-
raw: null
|
|
411
|
+
raw: null,
|
|
232
412
|
});
|
|
233
413
|
});
|
|
234
|
-
it(
|
|
414
|
+
it("returns a no-server error when no language server matches the file", async () => {
|
|
235
415
|
const registrar = new FakeRegistrar();
|
|
236
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
237
|
-
|
|
238
|
-
|
|
416
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
|
|
417
|
+
initializeManager: jest.fn(),
|
|
418
|
+
});
|
|
419
|
+
await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
|
|
420
|
+
content: [
|
|
421
|
+
{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: "No language server available for .md files. Run lsp_health for details.",
|
|
424
|
+
},
|
|
425
|
+
],
|
|
239
426
|
error: true,
|
|
240
|
-
raw: null
|
|
427
|
+
raw: null,
|
|
241
428
|
});
|
|
242
429
|
});
|
|
243
|
-
it(
|
|
430
|
+
it("returns restart guidance when the LSP crashed", async () => {
|
|
244
431
|
const registrar = new FakeRegistrar();
|
|
245
|
-
const client = createClient(new Error(
|
|
246
|
-
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
247
|
-
|
|
248
|
-
|
|
432
|
+
const client = createClient(new Error("LSP server exited unexpectedly (code: 1, signal: null)"));
|
|
433
|
+
(0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
|
|
434
|
+
initializeManager: jest.fn(),
|
|
435
|
+
});
|
|
436
|
+
await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
437
|
+
content: [
|
|
438
|
+
{
|
|
439
|
+
type: "text",
|
|
440
|
+
text: "Der Language Server ist neu gestartet, bitte versuche es erneut.",
|
|
441
|
+
},
|
|
442
|
+
],
|
|
249
443
|
error: true,
|
|
250
|
-
raw: null
|
|
444
|
+
raw: null,
|
|
251
445
|
});
|
|
252
446
|
});
|
|
253
447
|
});
|
|
@@ -262,11 +456,12 @@ function createLifecycle(options) {
|
|
|
262
456
|
return {
|
|
263
457
|
getClientForFile: jest.fn((_) => options.fileClient ?? null),
|
|
264
458
|
getReadyClients: jest.fn((_) => options.workspaceClients ?? []),
|
|
265
|
-
getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri ===
|
|
459
|
+
getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === "file:///workspace/src/index.ts")),
|
|
266
460
|
getWorkspaceDiagnostics: jest.fn((_) => options.diagnostics ?? []),
|
|
267
461
|
getHealth: jest.fn(() => options.health ?? []),
|
|
268
462
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
269
|
-
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
|
|
463
|
+
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
464
|
+
analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
|
|
270
465
|
};
|
|
271
466
|
}
|
|
272
467
|
function createClient(result) {
|
|
@@ -278,6 +473,6 @@ function createClient(result) {
|
|
|
278
473
|
getCapabilities: jest.fn(() => ({ renameProvider: true })),
|
|
279
474
|
ensureDidOpen: jest.fn().mockResolvedValue(undefined),
|
|
280
475
|
waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
|
|
281
|
-
ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
|
|
476
|
+
ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined),
|
|
282
477
|
};
|
|
283
478
|
}
|