@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
|
@@ -46,7 +46,7 @@ describe('McpServer', () => {
|
|
|
46
46
|
const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
|
|
47
47
|
readTools.mockImplementationOnce((registrar) => {
|
|
48
48
|
registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
|
|
49
|
-
registrar.registerTool('
|
|
49
|
+
registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'definition' }], raw: null }));
|
|
50
50
|
});
|
|
51
51
|
writeTools.mockImplementationOnce(() => undefined);
|
|
52
52
|
const { ListToolsRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
|
|
@@ -55,14 +55,14 @@ describe('McpServer', () => {
|
|
|
55
55
|
const listHandler = getHandler(ListToolsRequestSchema);
|
|
56
56
|
const result = await listHandler({});
|
|
57
57
|
expect(result.tools.map((t) => t.name)).toContain('lsp_init');
|
|
58
|
-
expect(result.tools.map((t) => t.name)).toContain('
|
|
58
|
+
expect(result.tools.map((t) => t.name)).toContain('lsp_definition');
|
|
59
59
|
});
|
|
60
60
|
it('lists all tools after initialization too', async () => {
|
|
61
61
|
const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
|
|
62
62
|
const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
|
|
63
63
|
readTools.mockImplementationOnce((registrar) => {
|
|
64
64
|
registrar.registerTool('lsp_init', { description: 'init' }, async () => ({ content: [{ type: 'text', text: 'ok' }], raw: null }));
|
|
65
|
-
registrar.registerTool('
|
|
65
|
+
registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'definition' }], raw: null }));
|
|
66
66
|
});
|
|
67
67
|
writeTools.mockImplementationOnce(() => undefined);
|
|
68
68
|
const { ListToolsRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
|
|
@@ -72,20 +72,20 @@ describe('McpServer', () => {
|
|
|
72
72
|
const listHandler = getHandler(ListToolsRequestSchema);
|
|
73
73
|
const result = await listHandler({});
|
|
74
74
|
expect(result.tools.map((t) => t.name)).toContain('lsp_init');
|
|
75
|
-
expect(result.tools.map((t) => t.name)).toContain('
|
|
75
|
+
expect(result.tools.map((t) => t.name)).toContain('lsp_definition');
|
|
76
76
|
});
|
|
77
77
|
it('returns no-root error for non-init tools before lsp_init', async () => {
|
|
78
78
|
const readTools = jest.requireMock('../tools/read-tools').registerReadTools;
|
|
79
79
|
const writeTools = jest.requireMock('../tools/write-tools').registerWriteTools;
|
|
80
80
|
readTools.mockImplementationOnce((registrar) => {
|
|
81
|
-
registrar.registerTool('
|
|
81
|
+
registrar.registerTool('lsp_definition', { description: 'definition' }, async () => ({ content: [{ type: 'text', text: 'reachable' }], raw: null }));
|
|
82
82
|
});
|
|
83
83
|
writeTools.mockImplementationOnce(() => undefined);
|
|
84
84
|
const { CallToolRequestSchema } = jest.requireActual('@modelcontextprotocol/sdk/types.js');
|
|
85
85
|
const server = new server_1.McpServer('info');
|
|
86
86
|
await server.start();
|
|
87
87
|
const callHandler = getHandler(CallToolRequestSchema);
|
|
88
|
-
const result = await callHandler({ params: { name: '
|
|
88
|
+
const result = await callHandler({ params: { name: 'lsp_definition', arguments: {} } });
|
|
89
89
|
expect(result.content[0].text).toMatch(/No project root set/);
|
|
90
90
|
});
|
|
91
91
|
it('re-initializes with new root when lsp_init called again', async () => {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const promises_1 = require("node:fs/promises");
|
|
4
4
|
const write_tools_1 = require("../tools/write-tools");
|
|
5
|
-
jest.mock(
|
|
5
|
+
jest.mock("node:fs/promises", () => ({
|
|
6
6
|
access: jest.fn(),
|
|
7
7
|
readFile: jest.fn(),
|
|
8
|
-
writeFile: jest.fn()
|
|
8
|
+
writeFile: jest.fn(),
|
|
9
9
|
}));
|
|
10
10
|
class FakeRegistrar {
|
|
11
11
|
tools = new Map();
|
|
@@ -13,166 +13,203 @@ class FakeRegistrar {
|
|
|
13
13
|
this.tools.set(name, { handler });
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
describe(
|
|
16
|
+
describe("registerWriteTools", () => {
|
|
17
17
|
beforeEach(() => {
|
|
18
18
|
jest.clearAllMocks();
|
|
19
19
|
promises_1.access.mockResolvedValue(undefined);
|
|
20
20
|
promises_1.readFile.mockImplementation(async (filePath) => {
|
|
21
21
|
const pathText = String(filePath);
|
|
22
|
-
if (pathText.endsWith(
|
|
23
|
-
return
|
|
22
|
+
if (pathText.endsWith(".editorconfig")) {
|
|
23
|
+
return "root = true\n[*]\nindent_size = 2\nindent_style = space\n";
|
|
24
24
|
}
|
|
25
|
-
return
|
|
25
|
+
return "const foo = oldName\n";
|
|
26
26
|
});
|
|
27
27
|
promises_1.writeFile.mockResolvedValue(undefined);
|
|
28
28
|
});
|
|
29
|
-
it(
|
|
29
|
+
it("renames symbols when the server supports rename and saves the changed file", async () => {
|
|
30
30
|
const registrar = new FakeRegistrar();
|
|
31
31
|
const client = createClient({
|
|
32
32
|
changes: {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
"file:///workspace/src/index.ts": [
|
|
34
|
+
{
|
|
35
|
+
range: {
|
|
36
|
+
start: { line: 0, character: 12 },
|
|
37
|
+
end: { line: 0, character: 19 },
|
|
38
|
+
},
|
|
39
|
+
newText: "newName",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
35
43
|
});
|
|
36
44
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
37
|
-
const result = await getHandler(registrar,
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
const result = await getHandler(registrar, "lsp_rename")({
|
|
46
|
+
file: "/workspace/src/index.ts",
|
|
47
|
+
line: 0,
|
|
48
|
+
character: 12,
|
|
49
|
+
newName: "newName",
|
|
50
|
+
});
|
|
51
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/rename", {
|
|
52
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
40
53
|
position: { line: 0, character: 12 },
|
|
41
|
-
newName:
|
|
54
|
+
newName: "newName",
|
|
42
55
|
}, 15000);
|
|
43
|
-
expect(promises_1.writeFile).toHaveBeenCalledWith(
|
|
44
|
-
expect(client.notify).toHaveBeenCalledWith(
|
|
56
|
+
expect(promises_1.writeFile).toHaveBeenCalledWith("/workspace/src/index.ts", "const foo = newName\n", "utf8");
|
|
57
|
+
expect(client.notify).toHaveBeenCalledWith("textDocument/didSave", {
|
|
58
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
59
|
+
});
|
|
45
60
|
expect(result).toEqual({
|
|
46
|
-
content: [{ type:
|
|
47
|
-
raw: { changedFiles: [
|
|
61
|
+
content: [{ type: "text", text: "Applied workspace edit to 1 file(s)" }],
|
|
62
|
+
raw: { changedFiles: ["/workspace/src/index.ts"] },
|
|
48
63
|
});
|
|
49
64
|
});
|
|
50
|
-
it(
|
|
65
|
+
it("returns an error when rename is unsupported", async () => {
|
|
51
66
|
const registrar = new FakeRegistrar();
|
|
52
67
|
const client = createClient(null, { renameProvider: false });
|
|
53
68
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
54
|
-
await expect(getHandler(registrar,
|
|
55
|
-
|
|
69
|
+
await expect(getHandler(registrar, "lsp_rename")({
|
|
70
|
+
file: "/workspace/src/index.ts",
|
|
71
|
+
line: 0,
|
|
72
|
+
character: 0,
|
|
73
|
+
newName: "x",
|
|
74
|
+
})).resolves.toEqual({
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: "Rename is not supported by the active language server.",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
56
81
|
error: true,
|
|
57
|
-
raw: null
|
|
82
|
+
raw: null,
|
|
58
83
|
});
|
|
59
84
|
});
|
|
60
|
-
it(
|
|
85
|
+
it("lists code actions without applying them", async () => {
|
|
61
86
|
const registrar = new FakeRegistrar();
|
|
62
|
-
const actions = [{ title:
|
|
87
|
+
const actions = [{ title: "Fix import", kind: "quickfix" }];
|
|
63
88
|
const client = createClient(actions);
|
|
64
89
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
65
|
-
await expect(getHandler(registrar,
|
|
66
|
-
content: [
|
|
67
|
-
|
|
90
|
+
await expect(getHandler(registrar, "lsp_code_action")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
91
|
+
content: [
|
|
92
|
+
{ type: "text", text: "Available code actions:\n- [0] Fix import" },
|
|
93
|
+
],
|
|
94
|
+
raw: actions,
|
|
68
95
|
});
|
|
69
96
|
expect(promises_1.writeFile).not.toHaveBeenCalled();
|
|
70
97
|
});
|
|
71
|
-
it(
|
|
98
|
+
it("applies the selected code action edit and command", async () => {
|
|
72
99
|
const registrar = new FakeRegistrar();
|
|
73
100
|
const client = createClient([
|
|
74
101
|
{
|
|
75
|
-
title:
|
|
102
|
+
title: "Fix import",
|
|
76
103
|
edit: {
|
|
77
104
|
changes: {
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
"file:///workspace/src/index.ts": [
|
|
106
|
+
{
|
|
107
|
+
range: {
|
|
108
|
+
start: { line: 0, character: 0 },
|
|
109
|
+
end: { line: 0, character: 5 },
|
|
110
|
+
},
|
|
111
|
+
newText: "let",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
80
115
|
},
|
|
81
|
-
command: { command:
|
|
82
|
-
}
|
|
116
|
+
command: { command: "workspace.applyFix", arguments: ["x"] },
|
|
117
|
+
},
|
|
83
118
|
]);
|
|
84
119
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
85
|
-
const result = await getHandler(registrar,
|
|
86
|
-
expect(client.request).toHaveBeenCalledWith(
|
|
120
|
+
const result = await getHandler(registrar, "lsp_code_action")({ file: "/workspace/src/index.ts", line: 0, character: 0, apply: true });
|
|
121
|
+
expect(client.request).toHaveBeenCalledWith("workspace/executeCommand", { command: "workspace.applyFix", arguments: ["x"] }, 15000);
|
|
87
122
|
expect(result).toEqual({
|
|
88
|
-
content: [{ type:
|
|
89
|
-
raw: { title:
|
|
123
|
+
content: [{ type: "text", text: "Applied code action: Fix import" }],
|
|
124
|
+
raw: { title: "Fix import", changedFiles: ["/workspace/src/index.ts"] },
|
|
90
125
|
});
|
|
91
126
|
});
|
|
92
|
-
it(
|
|
127
|
+
it("formats a file using editorconfig defaults", async () => {
|
|
93
128
|
const registrar = new FakeRegistrar();
|
|
94
|
-
const client = createClient([
|
|
129
|
+
const client = createClient([
|
|
130
|
+
{
|
|
131
|
+
range: {
|
|
132
|
+
start: { line: 0, character: 0 },
|
|
133
|
+
end: { line: 0, character: 21 },
|
|
134
|
+
},
|
|
135
|
+
newText: "const foo = oldName;\n",
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
95
138
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
96
|
-
await getHandler(registrar,
|
|
97
|
-
expect(client.request).toHaveBeenCalledWith(
|
|
98
|
-
textDocument: { uri:
|
|
99
|
-
options: { tabSize: 2, insertSpaces: true }
|
|
139
|
+
await getHandler(registrar, "lsp_formatting")({ file: "/workspace/src/index.ts" });
|
|
140
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/formatting", {
|
|
141
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
142
|
+
options: { tabSize: 2, insertSpaces: true },
|
|
100
143
|
}, 15000);
|
|
101
144
|
});
|
|
102
|
-
it(
|
|
145
|
+
it("formats a range with explicit options", async () => {
|
|
103
146
|
const registrar = new FakeRegistrar();
|
|
104
|
-
const client = createClient([
|
|
147
|
+
const client = createClient([
|
|
148
|
+
{
|
|
149
|
+
range: {
|
|
150
|
+
start: { line: 0, character: 0 },
|
|
151
|
+
end: { line: 0, character: 21 },
|
|
152
|
+
},
|
|
153
|
+
newText: "const foo = oldName;\n",
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
105
156
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
106
|
-
await getHandler(registrar,
|
|
107
|
-
file:
|
|
108
|
-
range: {
|
|
109
|
-
|
|
157
|
+
await getHandler(registrar, "lsp_range_formatting")({
|
|
158
|
+
file: "/workspace/src/index.ts",
|
|
159
|
+
range: {
|
|
160
|
+
start: { line: 0, character: 0 },
|
|
161
|
+
end: { line: 0, character: 21 },
|
|
162
|
+
},
|
|
163
|
+
options: { tabSize: 4, insertSpaces: false },
|
|
110
164
|
});
|
|
111
|
-
expect(client.request).toHaveBeenCalledWith(
|
|
112
|
-
textDocument: { uri:
|
|
113
|
-
range: {
|
|
114
|
-
|
|
165
|
+
expect(client.request).toHaveBeenCalledWith("textDocument/rangeFormatting", {
|
|
166
|
+
textDocument: { uri: "file:///workspace/src/index.ts" },
|
|
167
|
+
range: {
|
|
168
|
+
start: { line: 0, character: 0 },
|
|
169
|
+
end: { line: 0, character: 21 },
|
|
170
|
+
},
|
|
171
|
+
options: { tabSize: 4, insertSpaces: false },
|
|
115
172
|
}, 15000);
|
|
116
173
|
});
|
|
117
|
-
it(
|
|
118
|
-
const registrar = new FakeRegistrar();
|
|
119
|
-
const client = createClient(null);
|
|
120
|
-
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
121
|
-
const edit = {
|
|
122
|
-
changes: {
|
|
123
|
-
'file:///workspace/src/index.ts': [{ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 9 } }, newText: 'bar' }]
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
await expect(getHandler(registrar, 'lsp_apply_workspace_edit')({ edit })).resolves.toEqual({
|
|
127
|
-
content: [{ type: 'text', text: 'Applied workspace edit to 1 file(s)' }],
|
|
128
|
-
raw: { changedFiles: ['/workspace/src/index.ts'] }
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
it('supports indexed code actions, empty edits, and document changes', async () => {
|
|
174
|
+
it("supports indexed code actions and document changes", async () => {
|
|
132
175
|
const registrar = new FakeRegistrar();
|
|
133
176
|
const client = createClient([
|
|
134
|
-
{ title:
|
|
177
|
+
{ title: "Skip me" },
|
|
135
178
|
{
|
|
136
|
-
title:
|
|
179
|
+
title: "Apply me",
|
|
137
180
|
edit: {
|
|
138
181
|
documentChanges: [
|
|
139
182
|
{
|
|
140
|
-
textDocument: {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
183
|
+
textDocument: {
|
|
184
|
+
uri: "file:///workspace/src/index.ts",
|
|
185
|
+
version: 1,
|
|
186
|
+
},
|
|
187
|
+
edits: [
|
|
188
|
+
{
|
|
189
|
+
range: {
|
|
190
|
+
start: { line: 0, character: 6 },
|
|
191
|
+
end: { line: 0, character: 9 },
|
|
192
|
+
},
|
|
193
|
+
newText: "bar",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
146
200
|
]);
|
|
147
201
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
148
|
-
await expect(getHandler(registrar,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
content: [{ type:
|
|
155
|
-
raw:
|
|
202
|
+
await expect(getHandler(registrar, "lsp_code_action")({
|
|
203
|
+
file: "/workspace/src/index.ts",
|
|
204
|
+
line: 0,
|
|
205
|
+
character: 0,
|
|
206
|
+
apply: { index: 1 },
|
|
207
|
+
})).resolves.toEqual({
|
|
208
|
+
content: [{ type: "text", text: "Applied code action: Apply me" }],
|
|
209
|
+
raw: { title: "Apply me", changedFiles: ["/workspace/src/index.ts"] },
|
|
156
210
|
});
|
|
157
211
|
});
|
|
158
|
-
it(
|
|
159
|
-
const registrar = new FakeRegistrar();
|
|
160
|
-
(0, write_tools_1.registerWriteTools)(registrar, {
|
|
161
|
-
getClientForFile: jest.fn(() => null),
|
|
162
|
-
getReadyClients: jest.fn(() => []),
|
|
163
|
-
getFileDiagnostics: jest.fn((_) => []),
|
|
164
|
-
getWorkspaceDiagnostics: jest.fn(() => []),
|
|
165
|
-
getHealth: jest.fn(() => []),
|
|
166
|
-
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
167
|
-
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
|
|
168
|
-
});
|
|
169
|
-
await expect(getHandler(registrar, 'lsp_apply_workspace_edit')({ edit: {} })).resolves.toEqual({
|
|
170
|
-
content: [{ type: 'text', text: 'No language servers are ready. Run lsp_health for details.' }],
|
|
171
|
-
error: true,
|
|
172
|
-
raw: null
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
it('handles missing clients, formatter defaults, and empty code actions', async () => {
|
|
212
|
+
it("handles missing clients, formatter defaults, and empty code actions", async () => {
|
|
176
213
|
const registrar = new FakeRegistrar();
|
|
177
214
|
const noClientLifecycle = {
|
|
178
215
|
getClientForFile: jest.fn(() => null),
|
|
@@ -181,45 +218,71 @@ describe('registerWriteTools', () => {
|
|
|
181
218
|
getWorkspaceDiagnostics: jest.fn(() => []),
|
|
182
219
|
getHealth: jest.fn(() => []),
|
|
183
220
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
184
|
-
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
|
|
221
|
+
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
222
|
+
analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
|
|
185
223
|
};
|
|
186
224
|
(0, write_tools_1.registerWriteTools)(registrar, noClientLifecycle);
|
|
187
|
-
await expect(getHandler(registrar,
|
|
188
|
-
content: [
|
|
225
|
+
await expect(getHandler(registrar, "lsp_rename")({ file: "/workspace/README.md", line: 0, character: 0, newName: "x" })).resolves.toEqual({
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: "No language server available for .md files. Run lsp_health for details.",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
189
232
|
error: true,
|
|
190
|
-
raw: null
|
|
233
|
+
raw: null,
|
|
191
234
|
});
|
|
192
|
-
await expect(getHandler(registrar,
|
|
193
|
-
content: [
|
|
235
|
+
await expect(getHandler(registrar, "lsp_code_action")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: "No language server available for .md files. Run lsp_health for details.",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
194
242
|
error: true,
|
|
195
|
-
raw: null
|
|
243
|
+
raw: null,
|
|
196
244
|
});
|
|
197
|
-
await expect(getHandler(registrar,
|
|
198
|
-
content: [
|
|
245
|
+
await expect(getHandler(registrar, "lsp_formatting")({ file: "/workspace/README.md" })).resolves.toEqual({
|
|
246
|
+
content: [
|
|
247
|
+
{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: "No language server available for .md files. Run lsp_health for details.",
|
|
250
|
+
},
|
|
251
|
+
],
|
|
199
252
|
error: true,
|
|
200
|
-
raw: null
|
|
253
|
+
raw: null,
|
|
201
254
|
});
|
|
202
255
|
const secondRegistrar = new FakeRegistrar();
|
|
203
256
|
const client = createClient([]);
|
|
204
|
-
promises_1.access.mockRejectedValue(new Error(
|
|
257
|
+
promises_1.access.mockRejectedValue(new Error("missing"));
|
|
205
258
|
(0, write_tools_1.registerWriteTools)(secondRegistrar, createLifecycle(client));
|
|
206
|
-
await expect(getHandler(secondRegistrar,
|
|
207
|
-
content: [{ type:
|
|
208
|
-
raw: { changedFiles: [] }
|
|
259
|
+
await expect(getHandler(secondRegistrar, "lsp_formatting")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
|
|
260
|
+
content: [{ type: "text", text: "Applied workspace edit to 0 file(s)" }],
|
|
261
|
+
raw: { changedFiles: [] },
|
|
209
262
|
});
|
|
210
|
-
await expect(getHandler(secondRegistrar,
|
|
211
|
-
content: [{ type:
|
|
212
|
-
raw: []
|
|
263
|
+
await expect(getHandler(secondRegistrar, "lsp_code_action")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
|
|
264
|
+
content: [{ type: "text", text: "No result" }],
|
|
265
|
+
raw: [],
|
|
213
266
|
});
|
|
214
267
|
});
|
|
215
|
-
it(
|
|
268
|
+
it("maps write failures to timeout guidance", async () => {
|
|
216
269
|
const registrar = new FakeRegistrar();
|
|
217
|
-
const client = createClient(new Error(
|
|
270
|
+
const client = createClient(new Error("LSP request timed out: textDocument/rename"));
|
|
218
271
|
(0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
|
|
219
|
-
await expect(getHandler(registrar,
|
|
220
|
-
|
|
272
|
+
await expect(getHandler(registrar, "lsp_rename")({
|
|
273
|
+
file: "/workspace/src/index.ts",
|
|
274
|
+
line: 0,
|
|
275
|
+
character: 0,
|
|
276
|
+
newName: "x",
|
|
277
|
+
})).resolves.toEqual({
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: "Operation timed out after 15s — try a more specific query or check the LSP server health",
|
|
282
|
+
},
|
|
283
|
+
],
|
|
221
284
|
error: true,
|
|
222
|
-
raw: null
|
|
285
|
+
raw: null,
|
|
223
286
|
});
|
|
224
287
|
});
|
|
225
288
|
});
|
|
@@ -238,13 +301,14 @@ function createLifecycle(client) {
|
|
|
238
301
|
getWorkspaceDiagnostics: jest.fn(() => []),
|
|
239
302
|
getHealth: jest.fn(() => []),
|
|
240
303
|
ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
|
|
241
|
-
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
|
|
304
|
+
ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
|
|
305
|
+
analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
|
|
242
306
|
};
|
|
243
307
|
}
|
|
244
308
|
function createClient(result, capabilities = { renameProvider: true }) {
|
|
245
309
|
return {
|
|
246
310
|
request: jest.fn().mockImplementation(async (method) => {
|
|
247
|
-
if (method ===
|
|
311
|
+
if (method === "workspace/executeCommand") {
|
|
248
312
|
return null;
|
|
249
313
|
}
|
|
250
314
|
if (result instanceof Error) {
|
|
@@ -256,6 +320,6 @@ function createClient(result, capabilities = { renameProvider: true }) {
|
|
|
256
320
|
getCapabilities: jest.fn(() => capabilities),
|
|
257
321
|
ensureDidOpen: jest.fn().mockResolvedValue(undefined),
|
|
258
322
|
waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
|
|
259
|
-
ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
|
|
323
|
+
ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined),
|
|
260
324
|
};
|
|
261
325
|
}
|