@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.
@@ -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('node:fs/promises', () => ({
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,149 +13,230 @@ class FakeRegistrar {
13
13
  this.tools.set(name, { handler });
14
14
  }
15
15
  }
16
- describe('registerWriteTools', () => {
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('.editorconfig')) {
23
- return 'root = true\n[*]\nindent_size = 2\nindent_style = space\n';
22
+ if (pathText.endsWith(".editorconfig")) {
23
+ return "root = true\n[*]\nindent_size = 2\nindent_style = space\n";
24
24
  }
25
- return 'const foo = oldName\n';
25
+ return "const foo = oldName\n";
26
26
  });
27
27
  promises_1.writeFile.mockResolvedValue(undefined);
28
28
  });
29
- it('renames symbols when the server supports rename and saves the changed file', async () => {
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
- 'file:///workspace/src/index.ts': [{ range: { start: { line: 0, character: 12 }, end: { line: 0, character: 19 } }, newText: 'newName' }]
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, 'lsp_rename')({ file: '/workspace/src/index.ts', line: 0, character: 12, newName: 'newName' });
38
- expect(client.request).toHaveBeenCalledWith('textDocument/rename', {
39
- textDocument: { uri: 'file:///workspace/src/index.ts' },
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: 'newName'
54
+ newName: "newName",
42
55
  }, 15000);
43
- expect(promises_1.writeFile).toHaveBeenCalledWith('/workspace/src/index.ts', 'const foo = newName\n', 'utf8');
44
- expect(client.notify).toHaveBeenCalledWith('textDocument/didSave', { textDocument: { uri: 'file:///workspace/src/index.ts' } });
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: 'text', text: 'Applied workspace edit to 1 file(s)' }],
47
- raw: { changedFiles: ['/workspace/src/index.ts'] }
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('returns an error when rename is unsupported', async () => {
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, 'lsp_rename')({ file: '/workspace/src/index.ts', line: 0, character: 0, newName: 'x' })).resolves.toEqual({
55
- content: [{ type: 'text', text: 'Rename is not supported by the active language server.' }],
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('lists code actions without applying them', async () => {
85
+ it("lists code actions without applying them", async () => {
61
86
  const registrar = new FakeRegistrar();
62
- const actions = [{ title: 'Fix import', kind: 'quickfix' }];
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, 'lsp_code_action')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
66
- content: [{ type: 'text', text: 'Available code actions:\n- [0] Fix import' }],
67
- raw: actions
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('applies the selected code action edit and command', async () => {
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: 'Fix import',
102
+ title: "Fix import",
76
103
  edit: {
77
104
  changes: {
78
- 'file:///workspace/src/index.ts': [{ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, newText: 'let' }]
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: 'workspace.applyFix', arguments: ['x'] }
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, 'lsp_code_action')({ file: '/workspace/src/index.ts', line: 0, character: 0, apply: true });
86
- expect(client.request).toHaveBeenCalledWith('workspace/executeCommand', { command: 'workspace.applyFix', arguments: ['x'] }, 15000);
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: 'text', text: 'Applied code action: Fix import' }],
89
- raw: { title: 'Fix import', changedFiles: ['/workspace/src/index.ts'] }
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('formats a file using editorconfig defaults', async () => {
127
+ it("formats a file using editorconfig defaults", async () => {
93
128
  const registrar = new FakeRegistrar();
94
- const client = createClient([{ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 21 } }, newText: 'const foo = oldName;\n' }]);
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, 'lsp_formatting')({ file: '/workspace/src/index.ts' });
97
- expect(client.request).toHaveBeenCalledWith('textDocument/formatting', {
98
- textDocument: { uri: 'file:///workspace/src/index.ts' },
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('formats a range with explicit options', async () => {
145
+ it("formats a range with explicit options", async () => {
103
146
  const registrar = new FakeRegistrar();
104
- const client = createClient([{ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 21 } }, newText: 'const foo = oldName;\n' }]);
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, 'lsp_range_formatting')({
107
- file: '/workspace/src/index.ts',
108
- range: { start: { line: 0, character: 0 }, end: { line: 0, character: 21 } },
109
- options: { tabSize: 4, insertSpaces: false }
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('textDocument/rangeFormatting', {
112
- textDocument: { uri: 'file:///workspace/src/index.ts' },
113
- range: { start: { line: 0, character: 0 }, end: { line: 0, character: 21 } },
114
- options: { tabSize: 4, insertSpaces: false }
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('applies a raw workspace edit across files', async () => {
174
+ it("applies a raw workspace edit across files", async () => {
118
175
  const registrar = new FakeRegistrar();
119
176
  const client = createClient(null);
120
177
  (0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
121
178
  const edit = {
122
179
  changes: {
123
- 'file:///workspace/src/index.ts': [{ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 9 } }, newText: 'bar' }]
124
- }
180
+ "file:///workspace/src/index.ts": [
181
+ {
182
+ range: {
183
+ start: { line: 0, character: 6 },
184
+ end: { line: 0, character: 9 },
185
+ },
186
+ newText: "bar",
187
+ },
188
+ ],
189
+ },
125
190
  };
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'] }
191
+ await expect(getHandler(registrar, "lsp_apply_workspace_edit")({ edit })).resolves.toEqual({
192
+ content: [{ type: "text", text: "Applied workspace edit to 1 file(s)" }],
193
+ raw: { changedFiles: ["/workspace/src/index.ts"] },
129
194
  });
130
195
  });
131
- it('supports indexed code actions, empty edits, and document changes', async () => {
196
+ it("supports indexed code actions, empty edits, and document changes", async () => {
132
197
  const registrar = new FakeRegistrar();
133
198
  const client = createClient([
134
- { title: 'Skip me' },
199
+ { title: "Skip me" },
135
200
  {
136
- title: 'Apply me',
201
+ title: "Apply me",
137
202
  edit: {
138
203
  documentChanges: [
139
204
  {
140
- textDocument: { uri: 'file:///workspace/src/index.ts', version: 1 },
141
- edits: [{ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 9 } }, newText: 'bar' }]
142
- }
143
- ]
144
- }
145
- }
205
+ textDocument: {
206
+ uri: "file:///workspace/src/index.ts",
207
+ version: 1,
208
+ },
209
+ edits: [
210
+ {
211
+ range: {
212
+ start: { line: 0, character: 6 },
213
+ end: { line: 0, character: 9 },
214
+ },
215
+ newText: "bar",
216
+ },
217
+ ],
218
+ },
219
+ ],
220
+ },
221
+ },
146
222
  ]);
147
223
  (0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
148
- await expect(getHandler(registrar, 'lsp_code_action')({ file: '/workspace/src/index.ts', line: 0, character: 0, apply: { index: 1 } })).resolves.toEqual({
149
- content: [{ type: 'text', text: 'Applied code action: Apply me' }],
150
- raw: { title: 'Apply me', changedFiles: ['/workspace/src/index.ts'] }
224
+ await expect(getHandler(registrar, "lsp_code_action")({
225
+ file: "/workspace/src/index.ts",
226
+ line: 0,
227
+ character: 0,
228
+ apply: { index: 1 },
229
+ })).resolves.toEqual({
230
+ content: [{ type: "text", text: "Applied code action: Apply me" }],
231
+ raw: { title: "Apply me", changedFiles: ["/workspace/src/index.ts"] },
151
232
  });
152
233
  client.request.mockResolvedValueOnce(null);
153
- await expect(getHandler(registrar, 'lsp_apply_workspace_edit')({ edit: null })).resolves.toEqual({
154
- content: [{ type: 'text', text: 'No result' }],
155
- raw: null
234
+ await expect(getHandler(registrar, "lsp_apply_workspace_edit")({ edit: null })).resolves.toEqual({
235
+ content: [{ type: "text", text: "No result" }],
236
+ raw: null,
156
237
  });
157
238
  });
158
- it('returns an error when no write-capable client is ready', async () => {
239
+ it("returns an error when no write-capable client is ready", async () => {
159
240
  const registrar = new FakeRegistrar();
160
241
  (0, write_tools_1.registerWriteTools)(registrar, {
161
242
  getClientForFile: jest.fn(() => null),
@@ -164,15 +245,21 @@ describe('registerWriteTools', () => {
164
245
  getWorkspaceDiagnostics: jest.fn(() => []),
165
246
  getHealth: jest.fn(() => []),
166
247
  ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
167
- ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
248
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
249
+ analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
168
250
  });
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.' }],
251
+ await expect(getHandler(registrar, "lsp_apply_workspace_edit")({ edit: {} })).resolves.toEqual({
252
+ content: [
253
+ {
254
+ type: "text",
255
+ text: "No language servers are ready. Run lsp_health for details.",
256
+ },
257
+ ],
171
258
  error: true,
172
- raw: null
259
+ raw: null,
173
260
  });
174
261
  });
175
- it('handles missing clients, formatter defaults, and empty code actions', async () => {
262
+ it("handles missing clients, formatter defaults, and empty code actions", async () => {
176
263
  const registrar = new FakeRegistrar();
177
264
  const noClientLifecycle = {
178
265
  getClientForFile: jest.fn(() => null),
@@ -181,45 +268,71 @@ describe('registerWriteTools', () => {
181
268
  getWorkspaceDiagnostics: jest.fn(() => []),
182
269
  getHealth: jest.fn(() => []),
183
270
  ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
184
- ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
271
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
272
+ analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
185
273
  };
186
274
  (0, write_tools_1.registerWriteTools)(registrar, noClientLifecycle);
187
- await expect(getHandler(registrar, 'lsp_rename')({ file: '/workspace/README.md', line: 0, character: 0, newName: 'x' })).resolves.toEqual({
188
- content: [{ type: 'text', text: 'No language server available for .md files. Run lsp_health for details.' }],
275
+ await expect(getHandler(registrar, "lsp_rename")({ file: "/workspace/README.md", line: 0, character: 0, newName: "x" })).resolves.toEqual({
276
+ content: [
277
+ {
278
+ type: "text",
279
+ text: "No language server available for .md files. Run lsp_health for details.",
280
+ },
281
+ ],
189
282
  error: true,
190
- raw: null
283
+ raw: null,
191
284
  });
192
- await expect(getHandler(registrar, 'lsp_code_action')({ file: '/workspace/README.md', line: 0, character: 0 })).resolves.toEqual({
193
- content: [{ type: 'text', text: 'No language server available for .md files. Run lsp_health for details.' }],
285
+ await expect(getHandler(registrar, "lsp_code_action")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
286
+ content: [
287
+ {
288
+ type: "text",
289
+ text: "No language server available for .md files. Run lsp_health for details.",
290
+ },
291
+ ],
194
292
  error: true,
195
- raw: null
293
+ raw: null,
196
294
  });
197
- await expect(getHandler(registrar, 'lsp_formatting')({ file: '/workspace/README.md' })).resolves.toEqual({
198
- content: [{ type: 'text', text: 'No language server available for .md files. Run lsp_health for details.' }],
295
+ await expect(getHandler(registrar, "lsp_formatting")({ file: "/workspace/README.md" })).resolves.toEqual({
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: "No language server available for .md files. Run lsp_health for details.",
300
+ },
301
+ ],
199
302
  error: true,
200
- raw: null
303
+ raw: null,
201
304
  });
202
305
  const secondRegistrar = new FakeRegistrar();
203
306
  const client = createClient([]);
204
- promises_1.access.mockRejectedValue(new Error('missing'));
307
+ promises_1.access.mockRejectedValue(new Error("missing"));
205
308
  (0, write_tools_1.registerWriteTools)(secondRegistrar, createLifecycle(client));
206
- await expect(getHandler(secondRegistrar, 'lsp_formatting')({ file: '/workspace/src/index.ts' })).resolves.toEqual({
207
- content: [{ type: 'text', text: 'Applied workspace edit to 0 file(s)' }],
208
- raw: { changedFiles: [] }
309
+ await expect(getHandler(secondRegistrar, "lsp_formatting")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
310
+ content: [{ type: "text", text: "Applied workspace edit to 0 file(s)" }],
311
+ raw: { changedFiles: [] },
209
312
  });
210
- await expect(getHandler(secondRegistrar, 'lsp_code_action')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
211
- content: [{ type: 'text', text: 'No result' }],
212
- raw: []
313
+ await expect(getHandler(secondRegistrar, "lsp_code_action")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
314
+ content: [{ type: "text", text: "No result" }],
315
+ raw: [],
213
316
  });
214
317
  });
215
- it('maps write failures to timeout guidance', async () => {
318
+ it("maps write failures to timeout guidance", async () => {
216
319
  const registrar = new FakeRegistrar();
217
- const client = createClient(new Error('LSP request timed out: textDocument/rename'));
320
+ const client = createClient(new Error("LSP request timed out: textDocument/rename"));
218
321
  (0, write_tools_1.registerWriteTools)(registrar, createLifecycle(client));
219
- await expect(getHandler(registrar, 'lsp_rename')({ file: '/workspace/src/index.ts', line: 0, character: 0, newName: 'x' })).resolves.toEqual({
220
- content: [{ type: 'text', text: 'Operation timed out after 15s — try a more specific query or check the LSP server health' }],
322
+ await expect(getHandler(registrar, "lsp_rename")({
323
+ file: "/workspace/src/index.ts",
324
+ line: 0,
325
+ character: 0,
326
+ newName: "x",
327
+ })).resolves.toEqual({
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: "Operation timed out after 15s — try a more specific query or check the LSP server health",
332
+ },
333
+ ],
221
334
  error: true,
222
- raw: null
335
+ raw: null,
223
336
  });
224
337
  });
225
338
  });
@@ -238,13 +351,14 @@ function createLifecycle(client) {
238
351
  getWorkspaceDiagnostics: jest.fn(() => []),
239
352
  getHealth: jest.fn(() => []),
240
353
  ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
241
- ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
354
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
355
+ analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
242
356
  };
243
357
  }
244
358
  function createClient(result, capabilities = { renameProvider: true }) {
245
359
  return {
246
360
  request: jest.fn().mockImplementation(async (method) => {
247
- if (method === 'workspace/executeCommand') {
361
+ if (method === "workspace/executeCommand") {
248
362
  return null;
249
363
  }
250
364
  if (result instanceof Error) {
@@ -256,6 +370,6 @@ function createClient(result, capabilities = { renameProvider: true }) {
256
370
  getCapabilities: jest.fn(() => capabilities),
257
371
  ensureDidOpen: jest.fn().mockResolvedValue(undefined),
258
372
  waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
259
- ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
373
+ ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined),
260
374
  };
261
375
  }