@treedy/pyright-mcp 1.1.2 → 1.1.4
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 +13 -6
- package/dist/index.js +1611 -43
- package/dist/index.js.map +26 -0
- package/dist/lsp/connection.d.ts +71 -0
- package/dist/lsp/document-manager.d.ts +67 -0
- package/dist/lsp/index.d.ts +3 -0
- package/dist/lsp/types.d.ts +55 -0
- package/dist/lsp-client.d.ts +42 -2
- package/dist/tools/symbols.d.ts +17 -0
- package/dist/tools/update-document.d.ts +14 -0
- package/package.json +10 -8
- package/dist/lsp-client.js +0 -80
- package/dist/pyright-worker.mjs +0 -148
- package/dist/tools/completions.js +0 -71
- package/dist/tools/definition.js +0 -65
- package/dist/tools/diagnostics.js +0 -93
- package/dist/tools/hover.js +0 -44
- package/dist/tools/references.js +0 -162
- package/dist/tools/rename.js +0 -153
- package/dist/tools/search.js +0 -91
- package/dist/tools/signature-help.js +0 -54
- package/dist/tools/status.js +0 -147
- package/dist/utils/position.js +0 -74
package/dist/index.js
CHANGED
|
@@ -1,49 +1,1617 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
|
|
9
|
+
// src/tools/hover.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
// src/lsp-client.ts
|
|
13
|
+
import * as fs2 from "fs";
|
|
14
|
+
|
|
15
|
+
// src/utils/position.ts
|
|
16
|
+
import { Position } from "vscode-languageserver-protocol";
|
|
17
|
+
import { existsSync } from "fs";
|
|
18
|
+
import { dirname, join, resolve } from "path";
|
|
19
|
+
function toPosition(line, column) {
|
|
20
|
+
return Position.create(line - 1, column - 1);
|
|
21
|
+
}
|
|
22
|
+
function fromPosition(pos) {
|
|
23
|
+
return {
|
|
24
|
+
line: pos.line + 1,
|
|
25
|
+
column: pos.character + 1
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function formatRange(range) {
|
|
29
|
+
const start = fromPosition(range.start);
|
|
30
|
+
const end = fromPosition(range.end);
|
|
31
|
+
return `${start.line}:${start.column}-${end.line}:${end.column}`;
|
|
32
|
+
}
|
|
33
|
+
function uriToPath(uri) {
|
|
34
|
+
if (uri.startsWith("file://")) {
|
|
35
|
+
return uri.slice(7);
|
|
36
|
+
}
|
|
37
|
+
return uri;
|
|
38
|
+
}
|
|
39
|
+
function findProjectRoot(filePath) {
|
|
40
|
+
const configFiles = ["pyrightconfig.json", "pyproject.toml", ".git"];
|
|
41
|
+
let dir = dirname(resolve(filePath));
|
|
42
|
+
const root = "/";
|
|
43
|
+
while (dir !== root) {
|
|
44
|
+
for (const configFile of configFiles) {
|
|
45
|
+
if (existsSync(join(dir, configFile))) {
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir)
|
|
51
|
+
break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
return dirname(resolve(filePath));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/lsp/connection.ts
|
|
58
|
+
import { spawn } from "child_process";
|
|
59
|
+
import {
|
|
60
|
+
createMessageConnection,
|
|
61
|
+
StreamMessageReader,
|
|
62
|
+
StreamMessageWriter
|
|
63
|
+
} from "vscode-jsonrpc/node.js";
|
|
64
|
+
|
|
65
|
+
// src/lsp/document-manager.ts
|
|
66
|
+
import * as fs from "fs";
|
|
67
|
+
import * as path from "path";
|
|
68
|
+
|
|
69
|
+
class DocumentManager {
|
|
70
|
+
documents = new Map;
|
|
71
|
+
filePathToUri(filePath) {
|
|
72
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
73
|
+
return `file://${absolutePath}`;
|
|
74
|
+
}
|
|
75
|
+
uriToFilePath(uri) {
|
|
76
|
+
if (uri.startsWith("file://")) {
|
|
77
|
+
return uri.slice(7);
|
|
78
|
+
}
|
|
79
|
+
return uri;
|
|
80
|
+
}
|
|
81
|
+
isDocumentOpen(filePath) {
|
|
82
|
+
const uri = this.filePathToUri(filePath);
|
|
83
|
+
return this.documents.has(uri);
|
|
84
|
+
}
|
|
85
|
+
getDocument(filePath) {
|
|
86
|
+
const uri = this.filePathToUri(filePath);
|
|
87
|
+
return this.documents.get(uri);
|
|
88
|
+
}
|
|
89
|
+
openDocument(filePath, content) {
|
|
90
|
+
const uri = this.filePathToUri(filePath);
|
|
91
|
+
const actualContent = content ?? fs.readFileSync(filePath, "utf-8");
|
|
92
|
+
const state = {
|
|
93
|
+
uri,
|
|
94
|
+
version: 1,
|
|
95
|
+
content: actualContent,
|
|
96
|
+
languageId: "python"
|
|
97
|
+
};
|
|
98
|
+
this.documents.set(uri, state);
|
|
99
|
+
return state;
|
|
100
|
+
}
|
|
101
|
+
updateDocument(filePath, newContent) {
|
|
102
|
+
const uri = this.filePathToUri(filePath);
|
|
103
|
+
const existing = this.documents.get(uri);
|
|
104
|
+
if (!existing) {
|
|
105
|
+
const state = this.openDocument(filePath, newContent);
|
|
106
|
+
return state.version;
|
|
107
|
+
}
|
|
108
|
+
existing.version += 1;
|
|
109
|
+
existing.content = newContent;
|
|
110
|
+
return existing.version;
|
|
111
|
+
}
|
|
112
|
+
closeDocument(filePath) {
|
|
113
|
+
const uri = this.filePathToUri(filePath);
|
|
114
|
+
return this.documents.delete(uri);
|
|
115
|
+
}
|
|
116
|
+
getAllOpenDocuments() {
|
|
117
|
+
return Array.from(this.documents.values());
|
|
118
|
+
}
|
|
119
|
+
closeAll() {
|
|
120
|
+
this.documents.clear();
|
|
121
|
+
}
|
|
122
|
+
getTextDocumentIdentifier(filePath) {
|
|
123
|
+
return { uri: this.filePathToUri(filePath) };
|
|
124
|
+
}
|
|
125
|
+
getTextDocumentItem(filePath) {
|
|
126
|
+
const state = this.getDocument(filePath);
|
|
127
|
+
if (!state) {
|
|
128
|
+
throw new Error(`Document not open: ${filePath}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
uri: state.uri,
|
|
132
|
+
languageId: state.languageId,
|
|
133
|
+
version: state.version,
|
|
134
|
+
text: state.content
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
getVersionedTextDocumentIdentifier(filePath) {
|
|
138
|
+
const state = this.getDocument(filePath);
|
|
139
|
+
if (!state) {
|
|
140
|
+
throw new Error(`Document not open: ${filePath}`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
uri: state.uri,
|
|
144
|
+
version: state.version
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/lsp/connection.ts
|
|
150
|
+
class LspConnectionManager {
|
|
151
|
+
connections = new Map;
|
|
152
|
+
documentManagers = new Map;
|
|
153
|
+
initializationPromises = new Map;
|
|
154
|
+
static DEFAULT_INIT_TIMEOUT = 1e4;
|
|
155
|
+
static DOCUMENT_READY_DELAY = 500;
|
|
156
|
+
async getConnection(options) {
|
|
157
|
+
const { workspaceRoot } = options;
|
|
158
|
+
const existing = this.connections.get(workspaceRoot);
|
|
159
|
+
if (existing && existing.initialized) {
|
|
160
|
+
existing.lastUsed = Date.now();
|
|
161
|
+
return existing;
|
|
162
|
+
}
|
|
163
|
+
const pendingInit = this.initializationPromises.get(workspaceRoot);
|
|
164
|
+
if (pendingInit) {
|
|
165
|
+
return pendingInit;
|
|
166
|
+
}
|
|
167
|
+
const initPromise = this.createConnection(options);
|
|
168
|
+
this.initializationPromises.set(workspaceRoot, initPromise);
|
|
169
|
+
try {
|
|
170
|
+
const connection = await initPromise;
|
|
171
|
+
this.connections.set(workspaceRoot, connection);
|
|
172
|
+
return connection;
|
|
173
|
+
} finally {
|
|
174
|
+
this.initializationPromises.delete(workspaceRoot);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getDocumentManager(workspaceRoot) {
|
|
178
|
+
let manager = this.documentManagers.get(workspaceRoot);
|
|
179
|
+
if (!manager) {
|
|
180
|
+
manager = new DocumentManager;
|
|
181
|
+
this.documentManagers.set(workspaceRoot, manager);
|
|
182
|
+
}
|
|
183
|
+
return manager;
|
|
184
|
+
}
|
|
185
|
+
async createConnection(options) {
|
|
186
|
+
const { workspaceRoot, initTimeout = LspConnectionManager.DEFAULT_INIT_TIMEOUT } = options;
|
|
187
|
+
const pyrightProcess = spawn("pyright-langserver", ["--stdio"], {
|
|
188
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
189
|
+
cwd: workspaceRoot
|
|
190
|
+
});
|
|
191
|
+
pyrightProcess.on("error", (err) => {
|
|
192
|
+
console.error(`[LSP] Process error for ${workspaceRoot}:`, err);
|
|
193
|
+
this.closeConnection(workspaceRoot);
|
|
194
|
+
});
|
|
195
|
+
pyrightProcess.on("exit", (code, signal) => {
|
|
196
|
+
console.error(`[LSP] Process exited for ${workspaceRoot}: code=${code}, signal=${signal}`);
|
|
197
|
+
this.connections.delete(workspaceRoot);
|
|
198
|
+
});
|
|
199
|
+
console.error(`[LSP] Spawned pyright-langserver for ${workspaceRoot}`);
|
|
200
|
+
const connection = createMessageConnection(new StreamMessageReader(pyrightProcess.stdout), new StreamMessageWriter(pyrightProcess.stdin));
|
|
201
|
+
connection.onError((error) => {
|
|
202
|
+
console.error(`[LSP] Connection error for ${workspaceRoot}:`, error);
|
|
203
|
+
});
|
|
204
|
+
connection.onClose(() => {
|
|
205
|
+
console.error(`[LSP] Connection closed for ${workspaceRoot}`);
|
|
206
|
+
});
|
|
207
|
+
connection.listen();
|
|
208
|
+
const connectionId = Math.random().toString(36).slice(2, 8);
|
|
209
|
+
console.error(`[LSP] Created connection ${connectionId}`);
|
|
210
|
+
const lspConnection = {
|
|
211
|
+
connection,
|
|
212
|
+
process: pyrightProcess,
|
|
213
|
+
workspaceRoot,
|
|
214
|
+
initialized: false,
|
|
215
|
+
lastUsed: Date.now(),
|
|
216
|
+
id: connectionId
|
|
217
|
+
};
|
|
218
|
+
await this.initializeConnection(lspConnection, initTimeout);
|
|
219
|
+
return lspConnection;
|
|
220
|
+
}
|
|
221
|
+
async initializeConnection(lspConnection, timeout) {
|
|
222
|
+
const { connection, workspaceRoot } = lspConnection;
|
|
223
|
+
const initPromise = (async () => {
|
|
224
|
+
console.error(`[LSP] Sending initialize request...`);
|
|
225
|
+
const initResult = await connection.sendRequest("initialize", {
|
|
226
|
+
processId: process.pid,
|
|
227
|
+
rootUri: `file://${workspaceRoot}`,
|
|
228
|
+
capabilities: {
|
|
229
|
+
textDocument: {
|
|
230
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
231
|
+
completion: {
|
|
232
|
+
completionItem: {
|
|
233
|
+
snippetSupport: true,
|
|
234
|
+
documentationFormat: ["markdown", "plaintext"]
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
signatureHelp: {
|
|
238
|
+
signatureInformation: {
|
|
239
|
+
documentationFormat: ["markdown", "plaintext"]
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
definition: { linkSupport: true },
|
|
243
|
+
references: {},
|
|
244
|
+
rename: { prepareSupport: true },
|
|
245
|
+
documentSymbol: {
|
|
246
|
+
hierarchicalDocumentSymbolSupport: true
|
|
247
|
+
},
|
|
248
|
+
publishDiagnostics: {}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
workspaceFolders: [
|
|
252
|
+
{
|
|
253
|
+
uri: `file://${workspaceRoot}`,
|
|
254
|
+
name: workspaceRoot.split("/").pop() || "workspace"
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
});
|
|
258
|
+
console.error(`[LSP] Sending initialized notification...`);
|
|
259
|
+
await connection.sendNotification("initialized", {});
|
|
260
|
+
console.error(`[LSP] Connection initialized successfully`);
|
|
261
|
+
lspConnection.initialized = true;
|
|
262
|
+
})();
|
|
263
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
264
|
+
setTimeout(() => reject(new Error(`LSP initialization timed out after ${timeout}ms`)), timeout);
|
|
265
|
+
});
|
|
266
|
+
await Promise.race([initPromise, timeoutPromise]);
|
|
267
|
+
}
|
|
268
|
+
async openDocument(workspaceRoot, filePath, content) {
|
|
269
|
+
const lspConnection = this.connections.get(workspaceRoot);
|
|
270
|
+
if (!lspConnection || !lspConnection.initialized) {
|
|
271
|
+
throw new Error(`No initialized connection for workspace: ${workspaceRoot}`);
|
|
272
|
+
}
|
|
273
|
+
const docManager = this.getDocumentManager(workspaceRoot);
|
|
274
|
+
if (docManager.isDocumentOpen(filePath)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const docState = docManager.openDocument(filePath, content);
|
|
278
|
+
console.error(`[LSP] Sending didOpen for ${filePath}...`);
|
|
279
|
+
await lspConnection.connection.sendNotification("textDocument/didOpen", {
|
|
280
|
+
textDocument: {
|
|
281
|
+
uri: docState.uri,
|
|
282
|
+
languageId: docState.languageId,
|
|
283
|
+
version: docState.version,
|
|
284
|
+
text: docState.content
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
console.error(`[LSP] Waiting for analysis...`);
|
|
288
|
+
await this.waitForAnalysis();
|
|
289
|
+
console.error(`[LSP] Document ready: ${filePath}`);
|
|
290
|
+
}
|
|
291
|
+
async updateDocument(workspaceRoot, filePath, newContent) {
|
|
292
|
+
const lspConnection = this.connections.get(workspaceRoot);
|
|
293
|
+
if (!lspConnection || !lspConnection.initialized) {
|
|
294
|
+
throw new Error(`No initialized connection for workspace: ${workspaceRoot}`);
|
|
295
|
+
}
|
|
296
|
+
const docManager = this.getDocumentManager(workspaceRoot);
|
|
297
|
+
if (!docManager.isDocumentOpen(filePath)) {
|
|
298
|
+
await this.openDocument(workspaceRoot, filePath, newContent);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const newVersion = docManager.updateDocument(filePath, newContent);
|
|
302
|
+
const uri = docManager.filePathToUri(filePath);
|
|
303
|
+
await lspConnection.connection.sendNotification("textDocument/didChange", {
|
|
304
|
+
textDocument: {
|
|
305
|
+
uri,
|
|
306
|
+
version: newVersion
|
|
307
|
+
},
|
|
308
|
+
contentChanges: [{ text: newContent }]
|
|
309
|
+
});
|
|
310
|
+
await this.waitForAnalysis();
|
|
311
|
+
}
|
|
312
|
+
async closeDocument(workspaceRoot, filePath) {
|
|
313
|
+
const lspConnection = this.connections.get(workspaceRoot);
|
|
314
|
+
if (!lspConnection || !lspConnection.initialized) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const docManager = this.getDocumentManager(workspaceRoot);
|
|
318
|
+
if (!docManager.isDocumentOpen(filePath)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const uri = docManager.filePathToUri(filePath);
|
|
322
|
+
await lspConnection.connection.sendNotification("textDocument/didClose", {
|
|
323
|
+
textDocument: { uri }
|
|
324
|
+
});
|
|
325
|
+
docManager.closeDocument(filePath);
|
|
326
|
+
}
|
|
327
|
+
async waitForAnalysis() {
|
|
328
|
+
await new Promise((resolve3) => setTimeout(resolve3, LspConnectionManager.DOCUMENT_READY_DELAY));
|
|
329
|
+
}
|
|
330
|
+
async sendRequest(workspaceRoot, method, params) {
|
|
331
|
+
const lspConnection = this.connections.get(workspaceRoot);
|
|
332
|
+
if (!lspConnection || !lspConnection.initialized) {
|
|
333
|
+
throw new Error(`No initialized connection for workspace: ${workspaceRoot}`);
|
|
334
|
+
}
|
|
335
|
+
lspConnection.lastUsed = Date.now();
|
|
336
|
+
console.error(`[LSP] sendRequest: ${method}`);
|
|
337
|
+
const result = await lspConnection.connection.sendRequest(method, params);
|
|
338
|
+
console.error(`[LSP] Request ${method} done`);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
async closeConnection(workspaceRoot) {
|
|
342
|
+
const lspConnection = this.connections.get(workspaceRoot);
|
|
343
|
+
if (!lspConnection) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const docManager = this.documentManagers.get(workspaceRoot);
|
|
347
|
+
if (docManager) {
|
|
348
|
+
docManager.closeAll();
|
|
349
|
+
this.documentManagers.delete(workspaceRoot);
|
|
350
|
+
}
|
|
351
|
+
if (lspConnection.initialized) {
|
|
352
|
+
try {
|
|
353
|
+
await lspConnection.connection.sendRequest("shutdown");
|
|
354
|
+
await lspConnection.connection.sendNotification("exit");
|
|
355
|
+
} catch {}
|
|
356
|
+
}
|
|
357
|
+
lspConnection.connection.dispose();
|
|
358
|
+
if (!lspConnection.process.killed) {
|
|
359
|
+
lspConnection.process.kill();
|
|
360
|
+
}
|
|
361
|
+
this.connections.delete(workspaceRoot);
|
|
362
|
+
}
|
|
363
|
+
async closeAll() {
|
|
364
|
+
const workspaceRoots = Array.from(this.connections.keys());
|
|
365
|
+
await Promise.all(workspaceRoots.map((root) => this.closeConnection(root)));
|
|
366
|
+
}
|
|
367
|
+
getStatus() {
|
|
368
|
+
return Array.from(this.connections.entries()).map(([root, conn]) => ({
|
|
369
|
+
workspaceRoot: root,
|
|
370
|
+
initialized: conn.initialized,
|
|
371
|
+
lastUsed: new Date(conn.lastUsed)
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
var connectionManagerInstance = null;
|
|
376
|
+
function getConnectionManager() {
|
|
377
|
+
if (!connectionManagerInstance) {
|
|
378
|
+
connectionManagerInstance = new LspConnectionManager;
|
|
379
|
+
}
|
|
380
|
+
return connectionManagerInstance;
|
|
381
|
+
}
|
|
382
|
+
// src/lsp/types.ts
|
|
383
|
+
var SymbolKindNames = {
|
|
384
|
+
1: "File",
|
|
385
|
+
2: "Module",
|
|
386
|
+
3: "Namespace",
|
|
387
|
+
4: "Package",
|
|
388
|
+
5: "Class",
|
|
389
|
+
6: "Method",
|
|
390
|
+
7: "Property",
|
|
391
|
+
8: "Field",
|
|
392
|
+
9: "Constructor",
|
|
393
|
+
10: "Enum",
|
|
394
|
+
11: "Interface",
|
|
395
|
+
12: "Function",
|
|
396
|
+
13: "Variable",
|
|
397
|
+
14: "Constant",
|
|
398
|
+
15: "String",
|
|
399
|
+
16: "Number",
|
|
400
|
+
17: "Boolean",
|
|
401
|
+
18: "Array",
|
|
402
|
+
19: "Object",
|
|
403
|
+
20: "Key",
|
|
404
|
+
21: "Null",
|
|
405
|
+
22: "EnumMember",
|
|
406
|
+
23: "Struct",
|
|
407
|
+
24: "Event",
|
|
408
|
+
25: "Operator",
|
|
409
|
+
26: "TypeParameter"
|
|
410
|
+
};
|
|
411
|
+
// src/lsp-client.ts
|
|
412
|
+
function log(message) {
|
|
413
|
+
console.error(`[LSP] ${message}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
class LspClient {
|
|
417
|
+
connectionManager;
|
|
418
|
+
constructor() {
|
|
419
|
+
this.connectionManager = getConnectionManager();
|
|
420
|
+
}
|
|
421
|
+
async start() {}
|
|
422
|
+
async stop() {
|
|
423
|
+
await this.connectionManager.closeAll();
|
|
424
|
+
}
|
|
425
|
+
async ensureDocumentOpen(workspaceRoot, filePath) {
|
|
426
|
+
const docManager = this.connectionManager.getDocumentManager(workspaceRoot);
|
|
427
|
+
if (docManager.isDocumentOpen(filePath)) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
431
|
+
await this.connectionManager.openDocument(workspaceRoot, filePath, content);
|
|
432
|
+
}
|
|
433
|
+
async prepareRequest(filePath) {
|
|
434
|
+
const workspaceRoot = findProjectRoot(filePath);
|
|
435
|
+
log(`Workspace root: ${workspaceRoot}`);
|
|
436
|
+
await this.connectionManager.getConnection({ workspaceRoot });
|
|
437
|
+
await this.ensureDocumentOpen(workspaceRoot, filePath);
|
|
438
|
+
return workspaceRoot;
|
|
439
|
+
}
|
|
440
|
+
getDocumentUri(workspaceRoot, filePath) {
|
|
441
|
+
return this.connectionManager.getDocumentManager(workspaceRoot).filePathToUri(filePath);
|
|
442
|
+
}
|
|
443
|
+
async hover(filePath, position) {
|
|
444
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
445
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
446
|
+
log("Sending hover request");
|
|
447
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/hover", {
|
|
448
|
+
textDocument: { uri },
|
|
449
|
+
position
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async definition(filePath, position) {
|
|
453
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
454
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
455
|
+
log("Sending definition request");
|
|
456
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/definition", {
|
|
457
|
+
textDocument: { uri },
|
|
458
|
+
position
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async references(filePath, position, includeDeclaration = true) {
|
|
462
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
463
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
464
|
+
log("Sending references request");
|
|
465
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/references", {
|
|
466
|
+
textDocument: { uri },
|
|
467
|
+
position,
|
|
468
|
+
context: { includeDeclaration }
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async completions(filePath, position) {
|
|
472
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
473
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
474
|
+
log("Sending completions request");
|
|
475
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/completion", {
|
|
476
|
+
textDocument: { uri },
|
|
477
|
+
position
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
async signatureHelp(filePath, position) {
|
|
481
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
482
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
483
|
+
log("Sending signatureHelp request");
|
|
484
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/signatureHelp", {
|
|
485
|
+
textDocument: { uri },
|
|
486
|
+
position
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async rename(filePath, position, newName) {
|
|
490
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
491
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
492
|
+
log("Sending rename request");
|
|
493
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/rename", {
|
|
494
|
+
textDocument: { uri },
|
|
495
|
+
position,
|
|
496
|
+
newName
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async getDiagnostics(filePath) {
|
|
500
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
501
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
502
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
503
|
+
const docManager = this.connectionManager.getDocumentManager(workspaceRoot);
|
|
504
|
+
if (docManager.isDocumentOpen(filePath)) {
|
|
505
|
+
await this.connectionManager.closeDocument(workspaceRoot, filePath);
|
|
506
|
+
}
|
|
507
|
+
await this.connectionManager.openDocument(workspaceRoot, filePath, content);
|
|
508
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
509
|
+
log("Getting diagnostics via CLI");
|
|
510
|
+
return this.getDiagnosticsViaCli(filePath, workspaceRoot);
|
|
511
|
+
}
|
|
512
|
+
getDiagnosticsViaCli(filePath, workspaceRoot) {
|
|
513
|
+
const { execSync } = __require("child_process");
|
|
514
|
+
try {
|
|
515
|
+
const result = execSync(`pyright --outputjson "${filePath}"`, {
|
|
516
|
+
encoding: "utf-8",
|
|
517
|
+
cwd: workspaceRoot,
|
|
518
|
+
timeout: 30000,
|
|
519
|
+
maxBuffer: 10 * 1024 * 1024
|
|
520
|
+
});
|
|
521
|
+
const parsed = JSON.parse(result);
|
|
522
|
+
return this.convertPyrightDiagnostics(parsed);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (error && typeof error === "object" && "stdout" in error) {
|
|
525
|
+
try {
|
|
526
|
+
const parsed = JSON.parse(error.stdout);
|
|
527
|
+
return this.convertPyrightDiagnostics(parsed);
|
|
528
|
+
} catch {}
|
|
529
|
+
}
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
convertPyrightDiagnostics(output) {
|
|
534
|
+
if (!output.generalDiagnostics) {
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
return output.generalDiagnostics.map((d) => ({
|
|
538
|
+
range: {
|
|
539
|
+
start: { line: d.range.start.line, character: d.range.start.character },
|
|
540
|
+
end: { line: d.range.end.line, character: d.range.end.character }
|
|
541
|
+
},
|
|
542
|
+
severity: d.severity,
|
|
543
|
+
message: d.message,
|
|
544
|
+
source: "pyright",
|
|
545
|
+
code: d.rule
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
async documentSymbols(filePath) {
|
|
549
|
+
const workspaceRoot = await this.prepareRequest(filePath);
|
|
550
|
+
const uri = this.getDocumentUri(workspaceRoot, filePath);
|
|
551
|
+
log("Sending documentSymbol request");
|
|
552
|
+
return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/documentSymbol", {
|
|
553
|
+
textDocument: { uri }
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
async updateDocument(filePath, content) {
|
|
557
|
+
const workspaceRoot = findProjectRoot(filePath);
|
|
558
|
+
await this.connectionManager.getConnection({ workspaceRoot });
|
|
559
|
+
await this.connectionManager.updateDocument(workspaceRoot, filePath, content);
|
|
560
|
+
log(`Updated document: ${filePath}`);
|
|
561
|
+
}
|
|
562
|
+
getStatus() {
|
|
563
|
+
return this.connectionManager.getStatus();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
var client = null;
|
|
567
|
+
function getLspClient() {
|
|
568
|
+
if (!client) {
|
|
569
|
+
client = new LspClient;
|
|
570
|
+
}
|
|
571
|
+
return client;
|
|
572
|
+
}
|
|
573
|
+
async function shutdownLspClient() {
|
|
574
|
+
if (client) {
|
|
575
|
+
await client.stop();
|
|
576
|
+
client = null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/tools/hover.ts
|
|
581
|
+
var hoverSchema = {
|
|
582
|
+
file: z.string().describe("Absolute path to the Python file"),
|
|
583
|
+
line: z.number().int().positive().describe("Line number (1-based)"),
|
|
584
|
+
column: z.number().int().positive().describe("Column number (1-based)")
|
|
585
|
+
};
|
|
586
|
+
async function hover(args) {
|
|
587
|
+
console.error(`[hover] Starting hover for ${args.file}:${args.line}:${args.column}`);
|
|
588
|
+
const client2 = getLspClient();
|
|
589
|
+
const position = toPosition(args.line, args.column);
|
|
590
|
+
console.error(`[hover] Calling LSP hover...`);
|
|
591
|
+
const result = await client2.hover(args.file, position);
|
|
592
|
+
console.error(`[hover] Got result: ${result ? "yes" : "no"}`);
|
|
593
|
+
if (!result) {
|
|
594
|
+
return {
|
|
595
|
+
content: [{ type: "text", text: "No hover information available at this position." }]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
let hoverText = "";
|
|
599
|
+
if (typeof result.contents === "string") {
|
|
600
|
+
hoverText = result.contents;
|
|
601
|
+
} else if (Array.isArray(result.contents)) {
|
|
602
|
+
hoverText = result.contents.map((c) => typeof c === "string" ? c : c.value).join(`
|
|
603
|
+
|
|
604
|
+
`);
|
|
605
|
+
} else if ("kind" in result.contents) {
|
|
606
|
+
hoverText = result.contents.value;
|
|
607
|
+
} else if ("value" in result.contents) {
|
|
608
|
+
hoverText = result.contents.value;
|
|
609
|
+
}
|
|
610
|
+
let output = `**Hover Info** at ${args.file}:${args.line}:${args.column}
|
|
611
|
+
|
|
612
|
+
`;
|
|
613
|
+
output += hoverText;
|
|
614
|
+
if (result.range) {
|
|
615
|
+
output += `
|
|
616
|
+
|
|
617
|
+
**Range:** ${formatRange(result.range)}`;
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
content: [{ type: "text", text: output }]
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/tools/definition.ts
|
|
625
|
+
import { z as z2 } from "zod";
|
|
626
|
+
var definitionSchema = {
|
|
627
|
+
file: z2.string().describe("Absolute path to the Python file"),
|
|
628
|
+
line: z2.number().int().positive().describe("Line number (1-based)"),
|
|
629
|
+
column: z2.number().int().positive().describe("Column number (1-based)")
|
|
630
|
+
};
|
|
631
|
+
async function definition(args) {
|
|
632
|
+
const client2 = getLspClient();
|
|
633
|
+
const position = toPosition(args.line, args.column);
|
|
634
|
+
const result = await client2.definition(args.file, position);
|
|
635
|
+
if (!result) {
|
|
636
|
+
return {
|
|
637
|
+
content: [{ type: "text", text: "No definition found at this position." }]
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const locations = [];
|
|
641
|
+
if (Array.isArray(result)) {
|
|
642
|
+
for (const item of result) {
|
|
643
|
+
if ("targetUri" in item) {
|
|
644
|
+
const link = item;
|
|
645
|
+
const pos = fromPosition(link.targetSelectionRange.start);
|
|
646
|
+
locations.push({
|
|
647
|
+
file: uriToPath(link.targetUri),
|
|
648
|
+
line: pos.line,
|
|
649
|
+
column: pos.column
|
|
650
|
+
});
|
|
651
|
+
} else {
|
|
652
|
+
const loc = item;
|
|
653
|
+
const pos = fromPosition(loc.range.start);
|
|
654
|
+
locations.push({
|
|
655
|
+
file: uriToPath(loc.uri),
|
|
656
|
+
line: pos.line,
|
|
657
|
+
column: pos.column
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
const loc = result;
|
|
663
|
+
const pos = fromPosition(loc.range.start);
|
|
664
|
+
locations.push({
|
|
665
|
+
file: uriToPath(loc.uri),
|
|
666
|
+
line: pos.line,
|
|
667
|
+
column: pos.column
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (locations.length === 0) {
|
|
671
|
+
return {
|
|
672
|
+
content: [{ type: "text", text: "No definition found at this position." }]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
let output = `**Definition(s)** for symbol at ${args.file}:${args.line}:${args.column}
|
|
676
|
+
|
|
677
|
+
`;
|
|
678
|
+
for (const loc of locations) {
|
|
679
|
+
output += `- ${loc.file}:${loc.line}:${loc.column}
|
|
680
|
+
`;
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
content: [{ type: "text", text: output }]
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/tools/references.ts
|
|
688
|
+
import { z as z3 } from "zod";
|
|
689
|
+
import { execSync } from "child_process";
|
|
690
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
691
|
+
var referencesSchema = {
|
|
692
|
+
file: z3.string().describe("Absolute path to the Python file"),
|
|
693
|
+
line: z3.number().int().positive().describe("Line number (1-based)"),
|
|
694
|
+
column: z3.number().int().positive().describe("Column number (1-based)")
|
|
695
|
+
};
|
|
696
|
+
function getSymbolAtPosition(filePath, line, column) {
|
|
697
|
+
try {
|
|
698
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
699
|
+
const lines = content.split(`
|
|
700
|
+
`);
|
|
701
|
+
const targetLine = lines[line - 1];
|
|
702
|
+
if (!targetLine)
|
|
703
|
+
return null;
|
|
704
|
+
const col = column - 1;
|
|
705
|
+
let start = col;
|
|
706
|
+
let end = col;
|
|
707
|
+
while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
|
|
708
|
+
start--;
|
|
709
|
+
}
|
|
710
|
+
while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
|
|
711
|
+
end++;
|
|
712
|
+
}
|
|
713
|
+
const symbol = targetLine.slice(start, end);
|
|
714
|
+
return symbol.length > 0 ? symbol : null;
|
|
715
|
+
} catch {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async function references(args) {
|
|
720
|
+
const { file, line, column } = args;
|
|
721
|
+
const symbol = getSymbolAtPosition(file, line, column);
|
|
722
|
+
if (!symbol) {
|
|
723
|
+
return {
|
|
724
|
+
content: [{ type: "text", text: "Could not identify symbol at this position." }]
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const projectRoot = findProjectRoot(file);
|
|
728
|
+
const pattern = `\\b${symbol}\\b`;
|
|
729
|
+
let rgOutput;
|
|
730
|
+
try {
|
|
731
|
+
rgOutput = execSync(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
|
|
732
|
+
encoding: "utf-8",
|
|
733
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
734
|
+
timeout: 30000
|
|
735
|
+
});
|
|
736
|
+
} catch (e) {
|
|
737
|
+
const error = e;
|
|
738
|
+
if (error.status === 1) {
|
|
739
|
+
return {
|
|
740
|
+
content: [
|
|
741
|
+
{
|
|
742
|
+
type: "text",
|
|
743
|
+
text: `No references found for symbol \`${symbol}\``
|
|
744
|
+
}
|
|
745
|
+
]
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
rgOutput = execSync(`grep -rn --include="*.py" -w "${symbol}" "${projectRoot}"`, {
|
|
750
|
+
encoding: "utf-8",
|
|
751
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
752
|
+
timeout: 30000
|
|
753
|
+
});
|
|
754
|
+
} catch {
|
|
755
|
+
return {
|
|
756
|
+
content: [
|
|
757
|
+
{
|
|
758
|
+
type: "text",
|
|
759
|
+
text: `No references found for symbol \`${symbol}\``
|
|
760
|
+
}
|
|
761
|
+
]
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const refs = [];
|
|
766
|
+
const lines = rgOutput.trim().split(`
|
|
767
|
+
`).filter(Boolean);
|
|
768
|
+
for (const outputLine of lines) {
|
|
769
|
+
const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
|
|
770
|
+
if (match) {
|
|
771
|
+
const [, filePath, lineNum, colNum, text] = match;
|
|
772
|
+
refs.push({
|
|
773
|
+
file: filePath,
|
|
774
|
+
line: parseInt(lineNum, 10),
|
|
775
|
+
column: parseInt(colNum, 10),
|
|
776
|
+
text: text.trim(),
|
|
777
|
+
isDefinition: filePath === file && parseInt(lineNum, 10) === line
|
|
778
|
+
});
|
|
779
|
+
} else {
|
|
780
|
+
const grepMatch = outputLine.match(/^(.+?):(\d+):(.*)$/);
|
|
781
|
+
if (grepMatch) {
|
|
782
|
+
const [, filePath, lineNum, text] = grepMatch;
|
|
783
|
+
refs.push({
|
|
784
|
+
file: filePath,
|
|
785
|
+
line: parseInt(lineNum, 10),
|
|
786
|
+
column: 1,
|
|
787
|
+
text: text.trim(),
|
|
788
|
+
isDefinition: filePath === file && parseInt(lineNum, 10) === line
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (refs.length === 0) {
|
|
794
|
+
return {
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: "text",
|
|
798
|
+
text: `No references found for symbol \`${symbol}\``
|
|
799
|
+
}
|
|
800
|
+
]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
refs.sort((a, b) => {
|
|
804
|
+
if (a.isDefinition && !b.isDefinition)
|
|
805
|
+
return -1;
|
|
806
|
+
if (!a.isDefinition && b.isDefinition)
|
|
807
|
+
return 1;
|
|
808
|
+
if (a.file !== b.file)
|
|
809
|
+
return a.file.localeCompare(b.file);
|
|
810
|
+
return a.line - b.line;
|
|
811
|
+
});
|
|
812
|
+
const byFile = new Map;
|
|
813
|
+
for (const ref of refs) {
|
|
814
|
+
const list = byFile.get(ref.file) || [];
|
|
815
|
+
list.push(ref);
|
|
816
|
+
byFile.set(ref.file, list);
|
|
817
|
+
}
|
|
818
|
+
let output = `**References** for \`${symbol}\`
|
|
819
|
+
|
|
820
|
+
`;
|
|
821
|
+
output += `Found ${refs.length} reference(s) in ${byFile.size} file(s):
|
|
822
|
+
|
|
823
|
+
`;
|
|
824
|
+
for (const [filePath, fileRefs] of byFile) {
|
|
825
|
+
output += `### ${filePath}
|
|
826
|
+
|
|
827
|
+
`;
|
|
828
|
+
for (const ref of fileRefs) {
|
|
829
|
+
const marker = ref.isDefinition ? " (definition)" : "";
|
|
830
|
+
output += `- **${ref.line}:${ref.column}**${marker}: \`${ref.text}\`
|
|
831
|
+
`;
|
|
832
|
+
}
|
|
833
|
+
output += `
|
|
834
|
+
`;
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
content: [{ type: "text", text: output }]
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/tools/completions.ts
|
|
842
|
+
import { z as z4 } from "zod";
|
|
843
|
+
import { CompletionItemKind } from "vscode-languageserver-protocol";
|
|
844
|
+
var completionsSchema = {
|
|
845
|
+
file: z4.string().describe("Absolute path to the Python file"),
|
|
846
|
+
line: z4.number().int().positive().describe("Line number (1-based)"),
|
|
847
|
+
column: z4.number().int().positive().describe("Column number (1-based)"),
|
|
848
|
+
limit: z4.number().int().positive().optional().default(20).describe("Maximum number of completions to return")
|
|
849
|
+
};
|
|
850
|
+
var kindNames = {
|
|
851
|
+
[CompletionItemKind.Text]: "Text",
|
|
852
|
+
[CompletionItemKind.Method]: "Method",
|
|
853
|
+
[CompletionItemKind.Function]: "Function",
|
|
854
|
+
[CompletionItemKind.Constructor]: "Constructor",
|
|
855
|
+
[CompletionItemKind.Field]: "Field",
|
|
856
|
+
[CompletionItemKind.Variable]: "Variable",
|
|
857
|
+
[CompletionItemKind.Class]: "Class",
|
|
858
|
+
[CompletionItemKind.Interface]: "Interface",
|
|
859
|
+
[CompletionItemKind.Module]: "Module",
|
|
860
|
+
[CompletionItemKind.Property]: "Property",
|
|
861
|
+
[CompletionItemKind.Unit]: "Unit",
|
|
862
|
+
[CompletionItemKind.Value]: "Value",
|
|
863
|
+
[CompletionItemKind.Enum]: "Enum",
|
|
864
|
+
[CompletionItemKind.Keyword]: "Keyword",
|
|
865
|
+
[CompletionItemKind.Snippet]: "Snippet",
|
|
866
|
+
[CompletionItemKind.Color]: "Color",
|
|
867
|
+
[CompletionItemKind.File]: "File",
|
|
868
|
+
[CompletionItemKind.Reference]: "Reference",
|
|
869
|
+
[CompletionItemKind.Folder]: "Folder",
|
|
870
|
+
[CompletionItemKind.EnumMember]: "EnumMember",
|
|
871
|
+
[CompletionItemKind.Constant]: "Constant",
|
|
872
|
+
[CompletionItemKind.Struct]: "Struct",
|
|
873
|
+
[CompletionItemKind.Event]: "Event",
|
|
874
|
+
[CompletionItemKind.Operator]: "Operator",
|
|
875
|
+
[CompletionItemKind.TypeParameter]: "TypeParameter"
|
|
876
|
+
};
|
|
877
|
+
async function completions(args) {
|
|
878
|
+
const client2 = getLspClient();
|
|
879
|
+
const position = toPosition(args.line, args.column);
|
|
880
|
+
const limit = args.limit ?? 20;
|
|
881
|
+
const result = await client2.completions(args.file, position);
|
|
882
|
+
if (!result) {
|
|
883
|
+
return {
|
|
884
|
+
content: [{ type: "text", text: "No completions available at this position." }]
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
let items;
|
|
888
|
+
if (Array.isArray(result)) {
|
|
889
|
+
items = result;
|
|
890
|
+
} else {
|
|
891
|
+
items = result.items;
|
|
892
|
+
}
|
|
893
|
+
if (items.length === 0) {
|
|
894
|
+
return {
|
|
895
|
+
content: [{ type: "text", text: "No completions available at this position." }]
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const limitedItems = items.slice(0, limit);
|
|
899
|
+
let output = `**Completions** at ${args.file}:${args.line}:${args.column}
|
|
900
|
+
|
|
901
|
+
`;
|
|
902
|
+
output += `Showing ${limitedItems.length} of ${items.length} completion(s):
|
|
903
|
+
|
|
904
|
+
`;
|
|
905
|
+
for (const item of limitedItems) {
|
|
906
|
+
const kind = item.kind ? kindNames[item.kind] || "Unknown" : "Unknown";
|
|
907
|
+
const detail = item.detail ? ` - ${item.detail}` : "";
|
|
908
|
+
output += `- **${item.label}** (${kind})${detail}
|
|
909
|
+
`;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
content: [{ type: "text", text: output }]
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/tools/diagnostics.ts
|
|
917
|
+
import { z as z5 } from "zod";
|
|
918
|
+
import { execSync as execSync2 } from "child_process";
|
|
919
|
+
var diagnosticsSchema = {
|
|
920
|
+
path: z5.string().describe("Path to a Python file or directory to check")
|
|
921
|
+
};
|
|
922
|
+
async function diagnostics(args) {
|
|
923
|
+
const { path: path2 } = args;
|
|
924
|
+
const projectRoot = findProjectRoot(path2);
|
|
925
|
+
const target = path2;
|
|
926
|
+
const cmd = `pyright "${target}" --outputjson`;
|
|
927
|
+
let output;
|
|
928
|
+
try {
|
|
929
|
+
const result = execSync2(cmd, {
|
|
930
|
+
encoding: "utf-8",
|
|
931
|
+
cwd: projectRoot,
|
|
932
|
+
timeout: 60000,
|
|
933
|
+
maxBuffer: 10 * 1024 * 1024
|
|
934
|
+
});
|
|
935
|
+
output = JSON.parse(result);
|
|
936
|
+
} catch (e) {
|
|
937
|
+
const error = e;
|
|
938
|
+
if (error.stdout) {
|
|
939
|
+
try {
|
|
940
|
+
output = JSON.parse(error.stdout);
|
|
941
|
+
} catch {
|
|
942
|
+
return {
|
|
943
|
+
content: [
|
|
944
|
+
{
|
|
945
|
+
type: "text",
|
|
946
|
+
text: `Error running pyright: ${error.message || "Unknown error"}`
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
return {
|
|
953
|
+
content: [
|
|
954
|
+
{
|
|
955
|
+
type: "text",
|
|
956
|
+
text: `Error running pyright: ${error.message || "Unknown error"}
|
|
957
|
+
|
|
958
|
+
Make sure pyright is installed: npm install -g pyright`
|
|
959
|
+
}
|
|
960
|
+
]
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const diags = output.generalDiagnostics || [];
|
|
965
|
+
const summary = output.summary;
|
|
966
|
+
if (diags.length === 0) {
|
|
967
|
+
let text2 = `**No issues found**
|
|
968
|
+
|
|
969
|
+
`;
|
|
970
|
+
text2 += `- Files analyzed: ${summary.filesAnalyzed}
|
|
971
|
+
`;
|
|
972
|
+
text2 += `- Time: ${summary.timeInSec}s`;
|
|
973
|
+
return {
|
|
974
|
+
content: [{ type: "text", text: text2 }]
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const byFile = new Map;
|
|
978
|
+
for (const diag of diags) {
|
|
979
|
+
const list = byFile.get(diag.file) || [];
|
|
980
|
+
list.push(diag);
|
|
981
|
+
byFile.set(diag.file, list);
|
|
982
|
+
}
|
|
983
|
+
let text = `**Diagnostics Summary**
|
|
984
|
+
|
|
985
|
+
`;
|
|
986
|
+
text += `- Errors: ${summary.errorCount}
|
|
987
|
+
`;
|
|
988
|
+
text += `- Warnings: ${summary.warningCount}
|
|
989
|
+
`;
|
|
990
|
+
text += `- Information: ${summary.informationCount}
|
|
991
|
+
`;
|
|
992
|
+
text += `- Files analyzed: ${summary.filesAnalyzed}
|
|
993
|
+
`;
|
|
994
|
+
text += `- Time: ${summary.timeInSec}s
|
|
995
|
+
|
|
996
|
+
`;
|
|
997
|
+
text += `---
|
|
998
|
+
|
|
999
|
+
`;
|
|
1000
|
+
for (const [filePath, fileDiags] of byFile) {
|
|
1001
|
+
text += `### ${filePath}
|
|
1002
|
+
|
|
1003
|
+
`;
|
|
1004
|
+
for (const diag of fileDiags) {
|
|
1005
|
+
const line = diag.range.start.line + 1;
|
|
1006
|
+
const col = diag.range.start.character + 1;
|
|
1007
|
+
const rule = diag.rule ? ` (${diag.rule})` : "";
|
|
1008
|
+
const icon = diag.severity === "error" ? "❌" : diag.severity === "warning" ? "⚠️" : "ℹ️";
|
|
1009
|
+
text += `- ${icon} **${diag.severity}** at ${line}:${col}${rule}
|
|
1010
|
+
`;
|
|
1011
|
+
text += ` ${diag.message}
|
|
1012
|
+
|
|
1013
|
+
`;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
content: [{ type: "text", text }]
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/tools/signature-help.ts
|
|
1022
|
+
import { z as z6 } from "zod";
|
|
1023
|
+
var signatureHelpSchema = {
|
|
1024
|
+
file: z6.string().describe("Absolute path to the Python file"),
|
|
1025
|
+
line: z6.number().int().positive().describe("Line number (1-based)"),
|
|
1026
|
+
column: z6.number().int().positive().describe("Column number (1-based)")
|
|
1027
|
+
};
|
|
1028
|
+
async function signatureHelp(args) {
|
|
1029
|
+
const client2 = getLspClient();
|
|
1030
|
+
const position = toPosition(args.line, args.column);
|
|
1031
|
+
const result = await client2.signatureHelp(args.file, position);
|
|
1032
|
+
if (!result || result.signatures.length === 0) {
|
|
1033
|
+
return {
|
|
1034
|
+
content: [{ type: "text", text: "No signature help available at this position." }]
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
let output = `**Signature Help** at ${args.file}:${args.line}:${args.column}
|
|
1038
|
+
|
|
1039
|
+
`;
|
|
1040
|
+
const activeIndex = result.activeSignature ?? 0;
|
|
1041
|
+
const activeParam = result.activeParameter ?? 0;
|
|
1042
|
+
for (let i = 0;i < result.signatures.length; i++) {
|
|
1043
|
+
const sig = result.signatures[i];
|
|
1044
|
+
const isActive = i === activeIndex;
|
|
1045
|
+
output += `${isActive ? "→ " : " "}**${sig.label}**
|
|
1046
|
+
`;
|
|
1047
|
+
if (sig.documentation) {
|
|
1048
|
+
const doc = typeof sig.documentation === "string" ? sig.documentation : sig.documentation.value;
|
|
1049
|
+
output += ` ${doc}
|
|
1050
|
+
`;
|
|
1051
|
+
}
|
|
1052
|
+
if (sig.parameters && sig.parameters.length > 0) {
|
|
1053
|
+
output += `
|
|
1054
|
+
Parameters:
|
|
1055
|
+
`;
|
|
1056
|
+
for (let j = 0;j < sig.parameters.length; j++) {
|
|
1057
|
+
const param = sig.parameters[j];
|
|
1058
|
+
const isActiveParam = isActive && j === activeParam;
|
|
1059
|
+
const label = typeof param.label === "string" ? param.label : sig.label.slice(param.label[0], param.label[1]);
|
|
1060
|
+
output += ` ${isActiveParam ? "→ " : " "}${label}`;
|
|
1061
|
+
if (param.documentation) {
|
|
1062
|
+
const paramDoc = typeof param.documentation === "string" ? param.documentation : param.documentation.value;
|
|
1063
|
+
output += ` - ${paramDoc}`;
|
|
1064
|
+
}
|
|
1065
|
+
output += `
|
|
1066
|
+
`;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
output += `
|
|
1070
|
+
`;
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
content: [{ type: "text", text: output }]
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/tools/rename.ts
|
|
1078
|
+
import { z as z7 } from "zod";
|
|
1079
|
+
import { execSync as execSync3 } from "child_process";
|
|
1080
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1081
|
+
var renameSchema = {
|
|
1082
|
+
file: z7.string().describe("Absolute path to the Python file"),
|
|
1083
|
+
line: z7.number().int().positive().describe("Line number (1-based)"),
|
|
1084
|
+
column: z7.number().int().positive().describe("Column number (1-based)"),
|
|
1085
|
+
newName: z7.string().describe("New name for the symbol")
|
|
1086
|
+
};
|
|
1087
|
+
function getSymbolAtPosition2(filePath, line, column) {
|
|
1088
|
+
try {
|
|
1089
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
1090
|
+
const lines = content.split(`
|
|
1091
|
+
`);
|
|
1092
|
+
const targetLine = lines[line - 1];
|
|
1093
|
+
if (!targetLine)
|
|
1094
|
+
return null;
|
|
1095
|
+
const col = column - 1;
|
|
1096
|
+
let start = col;
|
|
1097
|
+
let end = col;
|
|
1098
|
+
while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
|
|
1099
|
+
start--;
|
|
1100
|
+
}
|
|
1101
|
+
while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
|
|
1102
|
+
end++;
|
|
1103
|
+
}
|
|
1104
|
+
const symbol = targetLine.slice(start, end);
|
|
1105
|
+
return symbol.length > 0 ? { symbol, start: start + 1, end: end + 1 } : null;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function rename(args) {
|
|
1111
|
+
const { file, line, column, newName } = args;
|
|
1112
|
+
const symbolInfo = getSymbolAtPosition2(file, line, column);
|
|
1113
|
+
if (!symbolInfo) {
|
|
1114
|
+
return {
|
|
1115
|
+
content: [{ type: "text", text: "Could not identify symbol at this position." }]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const { symbol: oldName } = symbolInfo;
|
|
1119
|
+
if (oldName === newName) {
|
|
1120
|
+
return {
|
|
1121
|
+
content: [{ type: "text", text: "New name is the same as the old name." }]
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const projectRoot = findProjectRoot(file);
|
|
1125
|
+
const pattern = `\\b${oldName}\\b`;
|
|
1126
|
+
let rgOutput;
|
|
1127
|
+
try {
|
|
1128
|
+
rgOutput = execSync3(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
|
|
1129
|
+
encoding: "utf-8",
|
|
1130
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1131
|
+
timeout: 30000
|
|
1132
|
+
});
|
|
1133
|
+
} catch (e) {
|
|
1134
|
+
const error = e;
|
|
1135
|
+
if (error.status === 1) {
|
|
1136
|
+
return {
|
|
1137
|
+
content: [
|
|
1138
|
+
{
|
|
1139
|
+
type: "text",
|
|
1140
|
+
text: `No references found for symbol \`${oldName}\``
|
|
1141
|
+
}
|
|
1142
|
+
]
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
rgOutput = execSync3(`grep -rn --include="*.py" -w "${oldName}" "${projectRoot}"`, {
|
|
1147
|
+
encoding: "utf-8",
|
|
1148
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1149
|
+
timeout: 30000
|
|
1150
|
+
});
|
|
1151
|
+
} catch {
|
|
1152
|
+
return {
|
|
1153
|
+
content: [
|
|
1154
|
+
{
|
|
1155
|
+
type: "text",
|
|
1156
|
+
text: `No references found for symbol \`${oldName}\``
|
|
1157
|
+
}
|
|
1158
|
+
]
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const edits = [];
|
|
1163
|
+
const outputLines = rgOutput.trim().split(`
|
|
1164
|
+
`).filter(Boolean);
|
|
1165
|
+
for (const outputLine of outputLines) {
|
|
1166
|
+
const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
|
|
1167
|
+
if (match) {
|
|
1168
|
+
const [, filePath, lineNum, colNum, lineContent] = match;
|
|
1169
|
+
const col = parseInt(colNum, 10);
|
|
1170
|
+
edits.push({
|
|
1171
|
+
file: filePath,
|
|
1172
|
+
line: parseInt(lineNum, 10),
|
|
1173
|
+
column: col,
|
|
1174
|
+
endColumn: col + oldName.length,
|
|
1175
|
+
oldText: oldName,
|
|
1176
|
+
newText: newName,
|
|
1177
|
+
lineContent: lineContent.trim()
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (edits.length === 0) {
|
|
1182
|
+
return {
|
|
1183
|
+
content: [
|
|
1184
|
+
{
|
|
1185
|
+
type: "text",
|
|
1186
|
+
text: `No references found for symbol \`${oldName}\``
|
|
1187
|
+
}
|
|
1188
|
+
]
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const byFile = new Map;
|
|
1192
|
+
for (const edit of edits) {
|
|
1193
|
+
const list = byFile.get(edit.file) || [];
|
|
1194
|
+
list.push(edit);
|
|
1195
|
+
byFile.set(edit.file, list);
|
|
1196
|
+
}
|
|
1197
|
+
let output = `**Rename Preview**
|
|
1198
|
+
|
|
1199
|
+
`;
|
|
1200
|
+
output += `- Symbol: \`${oldName}\` → \`${newName}\`
|
|
1201
|
+
`;
|
|
1202
|
+
output += `- Found ${edits.length} occurrence(s) in ${byFile.size} file(s)
|
|
1203
|
+
|
|
1204
|
+
`;
|
|
1205
|
+
output += `---
|
|
1206
|
+
|
|
1207
|
+
`;
|
|
1208
|
+
for (const [filePath, fileEdits] of byFile) {
|
|
1209
|
+
output += `### ${filePath}
|
|
1210
|
+
|
|
1211
|
+
`;
|
|
1212
|
+
for (const edit of fileEdits) {
|
|
1213
|
+
const preview = edit.lineContent.replace(new RegExp(`\\b${oldName}\\b`), `~~${oldName}~~ **${newName}**`);
|
|
1214
|
+
output += `- Line ${edit.line}: ${preview}
|
|
1215
|
+
`;
|
|
1216
|
+
}
|
|
1217
|
+
output += `
|
|
1218
|
+
`;
|
|
1219
|
+
}
|
|
1220
|
+
output += `---
|
|
1221
|
+
|
|
1222
|
+
`;
|
|
1223
|
+
output += `**Note:** This is a preview only. To apply the rename, use your editor's rename feature or run:
|
|
1224
|
+
`;
|
|
1225
|
+
output += `\`\`\`bash
|
|
1226
|
+
`;
|
|
1227
|
+
output += `# Using sed (backup recommended)
|
|
1228
|
+
`;
|
|
1229
|
+
output += `find "${projectRoot}" -name "*.py" -exec sed -i '' 's/\\b${oldName}\\b/${newName}/g' {} +
|
|
1230
|
+
`;
|
|
1231
|
+
output += `\`\`\``;
|
|
1232
|
+
return {
|
|
1233
|
+
content: [{ type: "text", text: output }]
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/tools/search.ts
|
|
1238
|
+
import { z as z8 } from "zod";
|
|
1239
|
+
import { execSync as execSync4 } from "child_process";
|
|
1240
|
+
import * as path2 from "path";
|
|
1241
|
+
var searchSchema = {
|
|
1242
|
+
pattern: z8.string().describe("The regex pattern to search for"),
|
|
1243
|
+
path: z8.string().optional().describe("Directory or file to search in (defaults to current working directory)"),
|
|
1244
|
+
glob: z8.string().optional().describe('Glob pattern to filter files (e.g., "*.py", "**/*.ts")'),
|
|
1245
|
+
caseSensitive: z8.boolean().optional().default(true).describe("Whether the search is case sensitive"),
|
|
1246
|
+
maxResults: z8.number().int().positive().optional().default(50).describe("Maximum number of results to return")
|
|
1247
|
+
};
|
|
1248
|
+
async function search(args) {
|
|
1249
|
+
const searchPath = args.path || process.cwd();
|
|
1250
|
+
const caseSensitive = args.caseSensitive ?? true;
|
|
1251
|
+
const maxResults = args.maxResults ?? 50;
|
|
1252
|
+
const rgArgs = [
|
|
1253
|
+
"--json",
|
|
1254
|
+
"--line-number",
|
|
1255
|
+
"--column"
|
|
1256
|
+
];
|
|
1257
|
+
if (!caseSensitive) {
|
|
1258
|
+
rgArgs.push("--ignore-case");
|
|
1259
|
+
}
|
|
1260
|
+
if (args.glob) {
|
|
1261
|
+
rgArgs.push("--glob", args.glob);
|
|
1262
|
+
}
|
|
1263
|
+
rgArgs.push("--", args.pattern, searchPath);
|
|
1264
|
+
try {
|
|
1265
|
+
const result = execSync4(`rg ${rgArgs.map((a) => `'${a}'`).join(" ")}`, {
|
|
1266
|
+
encoding: "utf-8",
|
|
1267
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1268
|
+
});
|
|
1269
|
+
const results = [];
|
|
1270
|
+
const lines = result.split(`
|
|
1271
|
+
`).filter(Boolean);
|
|
1272
|
+
for (const line of lines) {
|
|
1273
|
+
if (results.length >= maxResults)
|
|
1274
|
+
break;
|
|
1275
|
+
try {
|
|
1276
|
+
const json = JSON.parse(line);
|
|
1277
|
+
if (json.type === "match") {
|
|
1278
|
+
const data = json.data;
|
|
1279
|
+
const filePath = data.path.text;
|
|
1280
|
+
const lineNumber = data.line_number;
|
|
1281
|
+
const lineText = data.lines.text.trimEnd();
|
|
1282
|
+
for (const submatch of data.submatches) {
|
|
1283
|
+
if (results.length >= maxResults)
|
|
1284
|
+
break;
|
|
1285
|
+
results.push({
|
|
1286
|
+
file: path2.resolve(filePath),
|
|
1287
|
+
line: lineNumber,
|
|
1288
|
+
column: submatch.start + 1,
|
|
1289
|
+
text: lineText,
|
|
1290
|
+
match: submatch.match.text
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
} catch {}
|
|
1295
|
+
}
|
|
1296
|
+
if (results.length === 0) {
|
|
1297
|
+
return {
|
|
1298
|
+
content: [{ type: "text", text: `No matches found for pattern: ${args.pattern}` }]
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
let output = `**Search Results** for \`${args.pattern}\`
|
|
1302
|
+
|
|
1303
|
+
`;
|
|
1304
|
+
output += `Found ${results.length} match(es)${results.length >= maxResults ? ` (limited to ${maxResults})` : ""}:
|
|
1305
|
+
|
|
1306
|
+
`;
|
|
1307
|
+
for (const r of results) {
|
|
1308
|
+
output += `**${r.file}:${r.line}:${r.column}**
|
|
1309
|
+
`;
|
|
1310
|
+
output += ` \`${r.text}\`
|
|
1311
|
+
`;
|
|
1312
|
+
output += ` Match: \`${r.match}\`
|
|
1313
|
+
|
|
1314
|
+
`;
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{ type: "text", text: output }]
|
|
1318
|
+
};
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
const error = err;
|
|
1321
|
+
if (error.status === 1) {
|
|
1322
|
+
return {
|
|
1323
|
+
content: [{ type: "text", text: `No matches found for pattern: ${args.pattern}` }]
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
content: [{ type: "text", text: `Search error: ${error.message || "Unknown error"}` }]
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/tools/status.ts
|
|
1333
|
+
import { z as z9 } from "zod";
|
|
1334
|
+
import { execSync as execSync5 } from "child_process";
|
|
1335
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
1336
|
+
import { join as join2 } from "path";
|
|
1337
|
+
var statusSchema = {
|
|
1338
|
+
file: z9.string().describe("A Python file path to check the project status for")
|
|
1339
|
+
};
|
|
1340
|
+
async function status(args) {
|
|
1341
|
+
const { file } = args;
|
|
1342
|
+
const lines = [];
|
|
1343
|
+
const projectRoot = findProjectRoot(file);
|
|
1344
|
+
lines.push(`## Project Root`);
|
|
1345
|
+
lines.push(`\`${projectRoot}\``);
|
|
1346
|
+
lines.push("");
|
|
1347
|
+
lines.push(`## Pyright`);
|
|
1348
|
+
try {
|
|
1349
|
+
const pyrightVersion = execSync5("pyright --version", { encoding: "utf-8" }).trim();
|
|
1350
|
+
lines.push(`- Version: ${pyrightVersion}`);
|
|
1351
|
+
} catch {
|
|
1352
|
+
lines.push(`- ⚠️ **Not installed or not in PATH**`);
|
|
1353
|
+
lines.push(` Install with: \`npm install -g pyright\``);
|
|
1354
|
+
}
|
|
1355
|
+
lines.push("");
|
|
1356
|
+
lines.push(`## Pyright Config`);
|
|
1357
|
+
const pyrightConfigPath = join2(projectRoot, "pyrightconfig.json");
|
|
1358
|
+
const pyprojectPath = join2(projectRoot, "pyproject.toml");
|
|
1359
|
+
if (existsSync2(pyrightConfigPath)) {
|
|
1360
|
+
lines.push(`- Config file: \`pyrightconfig.json\``);
|
|
1361
|
+
try {
|
|
1362
|
+
const config = JSON.parse(readFileSync5(pyrightConfigPath, "utf-8"));
|
|
1363
|
+
if (config.pythonVersion) {
|
|
1364
|
+
lines.push(`- Python version: ${config.pythonVersion}`);
|
|
1365
|
+
}
|
|
1366
|
+
if (config.pythonPlatform) {
|
|
1367
|
+
lines.push(`- Platform: ${config.pythonPlatform}`);
|
|
1368
|
+
}
|
|
1369
|
+
if (config.venvPath) {
|
|
1370
|
+
lines.push(`- Venv path: ${config.venvPath}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (config.venv) {
|
|
1373
|
+
lines.push(`- Venv: ${config.venv}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (config.typeCheckingMode) {
|
|
1376
|
+
lines.push(`- Type checking mode: ${config.typeCheckingMode}`);
|
|
1377
|
+
}
|
|
1378
|
+
if (config.include) {
|
|
1379
|
+
lines.push(`- Include: ${JSON.stringify(config.include)}`);
|
|
1380
|
+
}
|
|
1381
|
+
if (config.exclude) {
|
|
1382
|
+
lines.push(`- Exclude: ${JSON.stringify(config.exclude)}`);
|
|
1383
|
+
}
|
|
1384
|
+
} catch (e) {
|
|
1385
|
+
lines.push(`- ⚠️ Failed to parse config: ${e}`);
|
|
1386
|
+
}
|
|
1387
|
+
} else if (existsSync2(pyprojectPath)) {
|
|
1388
|
+
lines.push(`- Config file: \`pyproject.toml\` (may contain [tool.pyright] section)`);
|
|
1389
|
+
} else {
|
|
1390
|
+
lines.push(`- ⚠️ No pyrightconfig.json or pyproject.toml found`);
|
|
1391
|
+
lines.push(` Pyright will use default settings`);
|
|
1392
|
+
}
|
|
1393
|
+
lines.push("");
|
|
1394
|
+
lines.push(`## Python Environment`);
|
|
1395
|
+
try {
|
|
1396
|
+
const pythonVersion = execSync5("python3 --version", { encoding: "utf-8" }).trim();
|
|
1397
|
+
lines.push(`- System Python: ${pythonVersion}`);
|
|
1398
|
+
} catch {
|
|
1399
|
+
try {
|
|
1400
|
+
const pythonVersion = execSync5("python --version", { encoding: "utf-8" }).trim();
|
|
1401
|
+
lines.push(`- System Python: ${pythonVersion}`);
|
|
1402
|
+
} catch {
|
|
1403
|
+
lines.push(`- ⚠️ Python not found in PATH`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const venvPaths = [".venv", "venv", ".env", "env"];
|
|
1407
|
+
for (const venv of venvPaths) {
|
|
1408
|
+
const venvPath = join2(projectRoot, venv);
|
|
1409
|
+
if (existsSync2(venvPath)) {
|
|
1410
|
+
lines.push(`- Virtual env found: \`${venv}/\``);
|
|
1411
|
+
const venvPython = join2(venvPath, "bin", "python");
|
|
1412
|
+
if (existsSync2(venvPython)) {
|
|
1413
|
+
try {
|
|
1414
|
+
const venvVersion = execSync5(`"${venvPython}" --version`, { encoding: "utf-8" }).trim();
|
|
1415
|
+
lines.push(` - ${venvVersion}`);
|
|
1416
|
+
} catch {}
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
lines.push("");
|
|
1422
|
+
lines.push(`## File Check`);
|
|
1423
|
+
lines.push(`- File: \`${file}\``);
|
|
1424
|
+
if (existsSync2(file)) {
|
|
1425
|
+
lines.push(`- Exists: ✅`);
|
|
1426
|
+
try {
|
|
1427
|
+
const result = execSync5(`pyright "${file}" --outputjson`, {
|
|
1428
|
+
encoding: "utf-8",
|
|
1429
|
+
cwd: projectRoot,
|
|
1430
|
+
timeout: 30000
|
|
1431
|
+
});
|
|
1432
|
+
const output = JSON.parse(result);
|
|
1433
|
+
const errors = output.generalDiagnostics?.filter((d) => d.severity === "error")?.length || 0;
|
|
1434
|
+
const warnings = output.generalDiagnostics?.filter((d) => d.severity === "warning")?.length || 0;
|
|
1435
|
+
lines.push(`- Diagnostics: ${errors} errors, ${warnings} warnings`);
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
const error = e;
|
|
1438
|
+
if (error.stdout) {
|
|
1439
|
+
try {
|
|
1440
|
+
const output = JSON.parse(error.stdout);
|
|
1441
|
+
const errors = output.generalDiagnostics?.filter((d) => d.severity === "error")?.length || 0;
|
|
1442
|
+
const warnings = output.generalDiagnostics?.filter((d) => d.severity === "warning")?.length || 0;
|
|
1443
|
+
lines.push(`- Diagnostics: ${errors} errors, ${warnings} warnings`);
|
|
1444
|
+
} catch {
|
|
1445
|
+
lines.push(`- ⚠️ Could not run pyright check`);
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
lines.push(`- ⚠️ Could not run pyright check`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
} else {
|
|
1452
|
+
lines.push(`- Exists: ❌ File not found`);
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
content: [{ type: "text", text: lines.join(`
|
|
1456
|
+
`) }]
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/tools/symbols.ts
|
|
1461
|
+
import { z as z10 } from "zod";
|
|
1462
|
+
import { SymbolKind } from "vscode-languageserver-protocol";
|
|
1463
|
+
var symbolsSchema = {
|
|
1464
|
+
file: z10.string().describe("Absolute path to the Python file"),
|
|
1465
|
+
filter: z10.enum(["all", "classes", "functions", "methods", "variables"]).optional().default("all").describe("Filter symbols by type"),
|
|
1466
|
+
includeChildren: z10.boolean().optional().default(true).describe("Include nested symbols (e.g., methods inside classes)")
|
|
1467
|
+
};
|
|
1468
|
+
function getSymbolKindName(kind) {
|
|
1469
|
+
return SymbolKindNames[kind] || "Unknown";
|
|
1470
|
+
}
|
|
1471
|
+
function matchesFilter(kind, filter) {
|
|
1472
|
+
if (filter === "all") {
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
switch (filter) {
|
|
1476
|
+
case "classes":
|
|
1477
|
+
return kind === SymbolKind.Class;
|
|
1478
|
+
case "functions":
|
|
1479
|
+
return kind === SymbolKind.Function;
|
|
1480
|
+
case "methods":
|
|
1481
|
+
return kind === SymbolKind.Method;
|
|
1482
|
+
case "variables":
|
|
1483
|
+
return kind === SymbolKind.Variable || kind === SymbolKind.Constant;
|
|
1484
|
+
default:
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function formatSymbol(symbol, filter, includeChildren, indent = 0) {
|
|
1489
|
+
const lines = [];
|
|
1490
|
+
const indentStr = " ".repeat(indent);
|
|
1491
|
+
const kindName = getSymbolKindName(symbol.kind);
|
|
1492
|
+
const line = symbol.range.start.line + 1;
|
|
1493
|
+
const symbolMatches = matchesFilter(symbol.kind, filter);
|
|
1494
|
+
if (symbolMatches) {
|
|
1495
|
+
lines.push(`${indentStr}- **${symbol.name}** (${kindName}) at line ${line}`);
|
|
1496
|
+
}
|
|
1497
|
+
if (includeChildren && symbol.children && symbol.children.length > 0) {
|
|
1498
|
+
for (const child of symbol.children) {
|
|
1499
|
+
const childLines = formatSymbol(child, filter, includeChildren, symbolMatches ? indent + 1 : indent);
|
|
1500
|
+
lines.push(...childLines);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return lines;
|
|
1504
|
+
}
|
|
1505
|
+
function isDocumentSymbolArray(result) {
|
|
1506
|
+
return Array.isArray(result) && result.length > 0 && "range" in result[0];
|
|
1507
|
+
}
|
|
1508
|
+
async function symbols(args) {
|
|
1509
|
+
const filter = args.filter || "all";
|
|
1510
|
+
const includeChildren = args.includeChildren !== false;
|
|
1511
|
+
console.error(`[symbols] Getting symbols for ${args.file} (filter: ${filter})`);
|
|
1512
|
+
const client2 = getLspClient();
|
|
1513
|
+
const result = await client2.documentSymbols(args.file);
|
|
1514
|
+
console.error(`[symbols] Got result: ${result ? `${result.length} symbols` : "no"}`);
|
|
1515
|
+
if (!result || Array.isArray(result) && result.length === 0) {
|
|
1516
|
+
return {
|
|
1517
|
+
content: [{ type: "text", text: `No symbols found in ${args.file}` }]
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
let output = `**Symbols** in ${args.file}
|
|
1521
|
+
|
|
1522
|
+
`;
|
|
1523
|
+
if (isDocumentSymbolArray(result)) {
|
|
1524
|
+
const lines = [];
|
|
1525
|
+
for (const symbol of result) {
|
|
1526
|
+
lines.push(...formatSymbol(symbol, filter, includeChildren));
|
|
1527
|
+
}
|
|
1528
|
+
if (lines.length === 0) {
|
|
1529
|
+
output += `No ${filter} symbols found.`;
|
|
1530
|
+
} else {
|
|
1531
|
+
output += lines.join(`
|
|
1532
|
+
`);
|
|
1533
|
+
}
|
|
1534
|
+
} else {
|
|
1535
|
+
const filteredSymbols = result.filter((s) => matchesFilter(s.kind, filter));
|
|
1536
|
+
if (filteredSymbols.length === 0) {
|
|
1537
|
+
output += `No ${filter} symbols found.`;
|
|
1538
|
+
} else {
|
|
1539
|
+
for (const symbol of filteredSymbols) {
|
|
1540
|
+
const kindName = getSymbolKindName(symbol.kind);
|
|
1541
|
+
const line = symbol.location.range.start.line + 1;
|
|
1542
|
+
output += `- **${symbol.name}** (${kindName}) at line ${line}
|
|
1543
|
+
`;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
content: [{ type: "text", text: output.trim() }]
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/tools/update-document.ts
|
|
1553
|
+
import { z as z11 } from "zod";
|
|
1554
|
+
var updateDocumentSchema = {
|
|
1555
|
+
file: z11.string().describe("Absolute path to the Python file"),
|
|
1556
|
+
content: z11.string().describe("New content for the file")
|
|
1557
|
+
};
|
|
1558
|
+
async function updateDocument(args) {
|
|
1559
|
+
console.error(`[updateDocument] Updating ${args.file}`);
|
|
1560
|
+
const client2 = getLspClient();
|
|
1561
|
+
await client2.updateDocument(args.file, args.content);
|
|
1562
|
+
return {
|
|
1563
|
+
content: [
|
|
1564
|
+
{
|
|
1565
|
+
type: "text",
|
|
1566
|
+
text: `Document updated: ${args.file}`
|
|
1567
|
+
}
|
|
1568
|
+
]
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/index.ts
|
|
1573
|
+
var server = new McpServer({
|
|
1574
|
+
name: "pyright-mcp",
|
|
1575
|
+
version: "1.0.0"
|
|
17
1576
|
});
|
|
18
|
-
|
|
19
|
-
server.tool(
|
|
20
|
-
|
|
21
|
-
server.tool(
|
|
22
|
-
|
|
23
|
-
server.tool(
|
|
24
|
-
|
|
25
|
-
server.tool(
|
|
26
|
-
|
|
27
|
-
server.tool(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
server.
|
|
36
|
-
|
|
1577
|
+
server.tool("hover", "Get type information and documentation at a specific position in a Python file", hoverSchema, async (args) => hover(args));
|
|
1578
|
+
server.tool("definition", "Go to definition of a symbol at a specific position in a Python file", definitionSchema, async (args) => definition(args));
|
|
1579
|
+
server.tool("references", "Find all references to a symbol at a specific position in a Python file", referencesSchema, async (args) => references(args));
|
|
1580
|
+
server.tool("completions", "Get code completion suggestions at a specific position in a Python file", completionsSchema, async (args) => completions(args));
|
|
1581
|
+
server.tool("diagnostics", "Get diagnostics (errors, warnings) for a Python file", diagnosticsSchema, async (args) => diagnostics(args));
|
|
1582
|
+
server.tool("signature_help", "Get function signature help at a specific position in a Python file", signatureHelpSchema, async (args) => signatureHelp(args));
|
|
1583
|
+
server.tool("rename", "Preview renaming a symbol at a specific position in a Python file", renameSchema, async (args) => rename(args));
|
|
1584
|
+
server.tool("search", "Search for a pattern in files and return file:line:column locations", searchSchema, async (args) => search(args));
|
|
1585
|
+
server.tool("status", "Check Python/Pyright environment status for a project", statusSchema, async (args) => status(args));
|
|
1586
|
+
server.tool("symbols", "Extract symbols (classes, functions, methods, variables) from a Python file", symbolsSchema, async (args) => symbols(args));
|
|
1587
|
+
server.tool("update_document", "Update the content of an open Python file for incremental analysis", updateDocumentSchema, async (args) => updateDocument(args));
|
|
1588
|
+
async function gracefulShutdown(signal) {
|
|
1589
|
+
console.error(`
|
|
1590
|
+
[Server] Received ${signal}, shutting down gracefully...`);
|
|
1591
|
+
try {
|
|
1592
|
+
await shutdownLspClient();
|
|
1593
|
+
console.error("[Server] LSP connections closed");
|
|
1594
|
+
await server.close();
|
|
1595
|
+
console.error("[Server] MCP server closed");
|
|
1596
|
+
process.exit(0);
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.error("[Server] Error during shutdown:", error);
|
|
1599
|
+
process.exit(1);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
1603
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
37
1604
|
async function main() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.error(` Ready`);
|
|
1605
|
+
console.error(`Pyright MCP server`);
|
|
1606
|
+
console.error(` Workspace: auto-detected from file path`);
|
|
1607
|
+
getLspClient();
|
|
1608
|
+
const transport = new StdioServerTransport;
|
|
1609
|
+
await server.connect(transport);
|
|
1610
|
+
console.error(` Ready`);
|
|
45
1611
|
}
|
|
46
1612
|
main().catch((error) => {
|
|
47
|
-
|
|
48
|
-
|
|
1613
|
+
console.error("Failed to start server:", error);
|
|
1614
|
+
process.exit(1);
|
|
49
1615
|
});
|
|
1616
|
+
|
|
1617
|
+
//# debugId=6646DB737E47BEFC64756E2164756E21
|