@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/lsp-client.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
|
-
import { dirname, resolve } from 'path';
|
|
4
|
-
import { findProjectRoot } from './utils/position.js';
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const workerScript = resolve(__dirname, 'pyright-worker.mjs');
|
|
8
|
-
function log(message) {
|
|
9
|
-
console.error(`[LSP] ${message}`);
|
|
10
|
-
}
|
|
11
|
-
export class LspClient {
|
|
12
|
-
constructor() {
|
|
13
|
-
// No longer stores workspace root - it's determined per-file
|
|
14
|
-
}
|
|
15
|
-
async start() {
|
|
16
|
-
// No-op, worker starts fresh for each request
|
|
17
|
-
}
|
|
18
|
-
async stop() {
|
|
19
|
-
// No-op
|
|
20
|
-
}
|
|
21
|
-
runWorker(method, filePath, args) {
|
|
22
|
-
// Automatically find project root from file path
|
|
23
|
-
const workspaceRoot = findProjectRoot(filePath);
|
|
24
|
-
log(`Workspace root: ${workspaceRoot}`);
|
|
25
|
-
const params = JSON.stringify({
|
|
26
|
-
workspaceRoot,
|
|
27
|
-
method,
|
|
28
|
-
filePath,
|
|
29
|
-
...args,
|
|
30
|
-
});
|
|
31
|
-
log(`Running worker: ${method}`);
|
|
32
|
-
try {
|
|
33
|
-
const result = execSync(`node "${workerScript}" '${params.replace(/'/g, "'\\''")}'`, {
|
|
34
|
-
encoding: 'utf-8',
|
|
35
|
-
timeout: 30000,
|
|
36
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
37
|
-
});
|
|
38
|
-
const parsed = JSON.parse(result.trim());
|
|
39
|
-
if (parsed.success) {
|
|
40
|
-
log(`Worker success: ${method}`);
|
|
41
|
-
return parsed.result;
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
throw new Error(parsed.error);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
log(`Worker error: ${error}`);
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async hover(filePath, position) {
|
|
53
|
-
return this.runWorker('hover', filePath, { position });
|
|
54
|
-
}
|
|
55
|
-
async definition(filePath, position) {
|
|
56
|
-
return this.runWorker('definition', filePath, { position });
|
|
57
|
-
}
|
|
58
|
-
async references(filePath, position, includeDeclaration = true) {
|
|
59
|
-
return this.runWorker('references', filePath, { position, includeDeclaration });
|
|
60
|
-
}
|
|
61
|
-
async completions(filePath, position) {
|
|
62
|
-
return this.runWorker('completions', filePath, { position });
|
|
63
|
-
}
|
|
64
|
-
async signatureHelp(filePath, position) {
|
|
65
|
-
return this.runWorker('signatureHelp', filePath, { position });
|
|
66
|
-
}
|
|
67
|
-
async rename(filePath, position, newName) {
|
|
68
|
-
return this.runWorker('rename', filePath, { position, newName });
|
|
69
|
-
}
|
|
70
|
-
async getDiagnostics(filePath) {
|
|
71
|
-
return this.runWorker('diagnostics', filePath, {});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
let client = null;
|
|
75
|
-
export function getLspClient() {
|
|
76
|
-
if (!client) {
|
|
77
|
-
client = new LspClient();
|
|
78
|
-
}
|
|
79
|
-
return client;
|
|
80
|
-
}
|
package/dist/pyright-worker.mjs
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Standalone worker script that communicates with pyright-langserver
|
|
3
|
-
// Uses vscode-jsonrpc for reliable LSP communication
|
|
4
|
-
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
import * as rpc from 'vscode-jsonrpc/node.js';
|
|
7
|
-
import * as fs from 'fs';
|
|
8
|
-
|
|
9
|
-
const args = JSON.parse(process.argv[2]);
|
|
10
|
-
const { workspaceRoot, method, filePath, position, newName, includeDeclaration } = args;
|
|
11
|
-
|
|
12
|
-
function log(msg) {
|
|
13
|
-
console.error(`[worker] ${msg}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function pathToUri(p) {
|
|
17
|
-
return p.startsWith('file://') ? p : `file://${p}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Create process and connection at module level like the working test script
|
|
21
|
-
const proc = spawn('pyright-langserver', ['--stdio'], {
|
|
22
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
proc.stderr.on('data', (data) => {
|
|
26
|
-
log(`stderr: ${data.toString().trim()}`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const connection = rpc.createMessageConnection(
|
|
30
|
-
new rpc.StreamMessageReader(proc.stdout),
|
|
31
|
-
new rpc.StreamMessageWriter(proc.stdin)
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
connection.onError((err) => log(`Connection error: ${err}`));
|
|
35
|
-
connection.onClose(() => log('Connection closed'));
|
|
36
|
-
connection.listen();
|
|
37
|
-
|
|
38
|
-
async function main() {
|
|
39
|
-
try {
|
|
40
|
-
// Initialize with simpler capabilities like the working test
|
|
41
|
-
log('Sending initialize...');
|
|
42
|
-
await connection.sendRequest('initialize', {
|
|
43
|
-
processId: process.pid,
|
|
44
|
-
rootUri: pathToUri(workspaceRoot),
|
|
45
|
-
capabilities: {
|
|
46
|
-
textDocument: {
|
|
47
|
-
hover: { contentFormat: ['markdown', 'plaintext'] },
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
log('Initialize done');
|
|
52
|
-
|
|
53
|
-
await connection.sendNotification('initialized', {});
|
|
54
|
-
log('Sent initialized notification');
|
|
55
|
-
|
|
56
|
-
// Open document
|
|
57
|
-
let uri = null;
|
|
58
|
-
if (filePath) {
|
|
59
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
60
|
-
uri = pathToUri(filePath);
|
|
61
|
-
|
|
62
|
-
log(`Opening document: ${uri}`);
|
|
63
|
-
await connection.sendNotification('textDocument/didOpen', {
|
|
64
|
-
textDocument: {
|
|
65
|
-
uri,
|
|
66
|
-
languageId: 'python',
|
|
67
|
-
version: 1,
|
|
68
|
-
text: content,
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Wait for analysis
|
|
73
|
-
log('Waiting for analysis...');
|
|
74
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let result;
|
|
78
|
-
|
|
79
|
-
switch (method) {
|
|
80
|
-
case 'hover':
|
|
81
|
-
log(`Sending hover request at ${JSON.stringify(position)}`);
|
|
82
|
-
result = await connection.sendRequest('textDocument/hover', {
|
|
83
|
-
textDocument: { uri },
|
|
84
|
-
position,
|
|
85
|
-
});
|
|
86
|
-
break;
|
|
87
|
-
|
|
88
|
-
case 'definition':
|
|
89
|
-
log('Sending definition request');
|
|
90
|
-
result = await connection.sendRequest('textDocument/definition', {
|
|
91
|
-
textDocument: { uri },
|
|
92
|
-
position,
|
|
93
|
-
});
|
|
94
|
-
break;
|
|
95
|
-
|
|
96
|
-
case 'references':
|
|
97
|
-
log('Sending references request');
|
|
98
|
-
result = await connection.sendRequest('textDocument/references', {
|
|
99
|
-
textDocument: { uri },
|
|
100
|
-
position,
|
|
101
|
-
context: { includeDeclaration: includeDeclaration !== false },
|
|
102
|
-
});
|
|
103
|
-
break;
|
|
104
|
-
|
|
105
|
-
case 'completions':
|
|
106
|
-
log('Sending completions request');
|
|
107
|
-
result = await connection.sendRequest('textDocument/completion', {
|
|
108
|
-
textDocument: { uri },
|
|
109
|
-
position,
|
|
110
|
-
});
|
|
111
|
-
break;
|
|
112
|
-
|
|
113
|
-
case 'signatureHelp':
|
|
114
|
-
log('Sending signatureHelp request');
|
|
115
|
-
result = await connection.sendRequest('textDocument/signatureHelp', {
|
|
116
|
-
textDocument: { uri },
|
|
117
|
-
position,
|
|
118
|
-
});
|
|
119
|
-
break;
|
|
120
|
-
|
|
121
|
-
case 'rename':
|
|
122
|
-
log('Sending rename request');
|
|
123
|
-
result = await connection.sendRequest('textDocument/rename', {
|
|
124
|
-
textDocument: { uri },
|
|
125
|
-
position,
|
|
126
|
-
newName,
|
|
127
|
-
});
|
|
128
|
-
break;
|
|
129
|
-
|
|
130
|
-
case 'diagnostics':
|
|
131
|
-
result = [];
|
|
132
|
-
break;
|
|
133
|
-
|
|
134
|
-
default:
|
|
135
|
-
throw new Error(`Unknown method: ${method}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
console.log(JSON.stringify({ success: true, result }));
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.log(JSON.stringify({ success: false, error: error.message }));
|
|
141
|
-
} finally {
|
|
142
|
-
connection.dispose();
|
|
143
|
-
proc.kill();
|
|
144
|
-
process.exit(0);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
main();
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getLspClient } from '../lsp-client.js';
|
|
3
|
-
import { toPosition } from '../utils/position.js';
|
|
4
|
-
import { CompletionItemKind } from 'vscode-languageserver-protocol';
|
|
5
|
-
export const completionsSchema = {
|
|
6
|
-
file: z.string().describe('Absolute path to the Python file'),
|
|
7
|
-
line: z.number().int().positive().describe('Line number (1-based)'),
|
|
8
|
-
column: z.number().int().positive().describe('Column number (1-based)'),
|
|
9
|
-
limit: z.number().int().positive().optional().default(20).describe('Maximum number of completions to return'),
|
|
10
|
-
};
|
|
11
|
-
const kindNames = {
|
|
12
|
-
[CompletionItemKind.Text]: 'Text',
|
|
13
|
-
[CompletionItemKind.Method]: 'Method',
|
|
14
|
-
[CompletionItemKind.Function]: 'Function',
|
|
15
|
-
[CompletionItemKind.Constructor]: 'Constructor',
|
|
16
|
-
[CompletionItemKind.Field]: 'Field',
|
|
17
|
-
[CompletionItemKind.Variable]: 'Variable',
|
|
18
|
-
[CompletionItemKind.Class]: 'Class',
|
|
19
|
-
[CompletionItemKind.Interface]: 'Interface',
|
|
20
|
-
[CompletionItemKind.Module]: 'Module',
|
|
21
|
-
[CompletionItemKind.Property]: 'Property',
|
|
22
|
-
[CompletionItemKind.Unit]: 'Unit',
|
|
23
|
-
[CompletionItemKind.Value]: 'Value',
|
|
24
|
-
[CompletionItemKind.Enum]: 'Enum',
|
|
25
|
-
[CompletionItemKind.Keyword]: 'Keyword',
|
|
26
|
-
[CompletionItemKind.Snippet]: 'Snippet',
|
|
27
|
-
[CompletionItemKind.Color]: 'Color',
|
|
28
|
-
[CompletionItemKind.File]: 'File',
|
|
29
|
-
[CompletionItemKind.Reference]: 'Reference',
|
|
30
|
-
[CompletionItemKind.Folder]: 'Folder',
|
|
31
|
-
[CompletionItemKind.EnumMember]: 'EnumMember',
|
|
32
|
-
[CompletionItemKind.Constant]: 'Constant',
|
|
33
|
-
[CompletionItemKind.Struct]: 'Struct',
|
|
34
|
-
[CompletionItemKind.Event]: 'Event',
|
|
35
|
-
[CompletionItemKind.Operator]: 'Operator',
|
|
36
|
-
[CompletionItemKind.TypeParameter]: 'TypeParameter',
|
|
37
|
-
};
|
|
38
|
-
export async function completions(args) {
|
|
39
|
-
const client = getLspClient();
|
|
40
|
-
const position = toPosition(args.line, args.column);
|
|
41
|
-
const limit = args.limit ?? 20;
|
|
42
|
-
const result = await client.completions(args.file, position);
|
|
43
|
-
if (!result) {
|
|
44
|
-
return {
|
|
45
|
-
content: [{ type: 'text', text: 'No completions available at this position.' }],
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
let items;
|
|
49
|
-
if (Array.isArray(result)) {
|
|
50
|
-
items = result;
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
items = result.items;
|
|
54
|
-
}
|
|
55
|
-
if (items.length === 0) {
|
|
56
|
-
return {
|
|
57
|
-
content: [{ type: 'text', text: 'No completions available at this position.' }],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const limitedItems = items.slice(0, limit);
|
|
61
|
-
let output = `**Completions** at ${args.file}:${args.line}:${args.column}\n\n`;
|
|
62
|
-
output += `Showing ${limitedItems.length} of ${items.length} completion(s):\n\n`;
|
|
63
|
-
for (const item of limitedItems) {
|
|
64
|
-
const kind = item.kind ? kindNames[item.kind] || 'Unknown' : 'Unknown';
|
|
65
|
-
const detail = item.detail ? ` - ${item.detail}` : '';
|
|
66
|
-
output += `- **${item.label}** (${kind})${detail}\n`;
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
content: [{ type: 'text', text: output }],
|
|
70
|
-
};
|
|
71
|
-
}
|
package/dist/tools/definition.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getLspClient } from '../lsp-client.js';
|
|
3
|
-
import { toPosition, fromPosition, uriToPath } from '../utils/position.js';
|
|
4
|
-
export const definitionSchema = {
|
|
5
|
-
file: z.string().describe('Absolute path to the Python file'),
|
|
6
|
-
line: z.number().int().positive().describe('Line number (1-based)'),
|
|
7
|
-
column: z.number().int().positive().describe('Column number (1-based)'),
|
|
8
|
-
};
|
|
9
|
-
export async function definition(args) {
|
|
10
|
-
const client = getLspClient();
|
|
11
|
-
const position = toPosition(args.line, args.column);
|
|
12
|
-
const result = await client.definition(args.file, position);
|
|
13
|
-
if (!result) {
|
|
14
|
-
return {
|
|
15
|
-
content: [{ type: 'text', text: 'No definition found at this position.' }],
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
const locations = [];
|
|
19
|
-
if (Array.isArray(result)) {
|
|
20
|
-
for (const item of result) {
|
|
21
|
-
if ('targetUri' in item) {
|
|
22
|
-
// LocationLink
|
|
23
|
-
const link = item;
|
|
24
|
-
const pos = fromPosition(link.targetSelectionRange.start);
|
|
25
|
-
locations.push({
|
|
26
|
-
file: uriToPath(link.targetUri),
|
|
27
|
-
line: pos.line,
|
|
28
|
-
column: pos.column,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
// Location
|
|
33
|
-
const loc = item;
|
|
34
|
-
const pos = fromPosition(loc.range.start);
|
|
35
|
-
locations.push({
|
|
36
|
-
file: uriToPath(loc.uri),
|
|
37
|
-
line: pos.line,
|
|
38
|
-
column: pos.column,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
// Single Location
|
|
45
|
-
const loc = result;
|
|
46
|
-
const pos = fromPosition(loc.range.start);
|
|
47
|
-
locations.push({
|
|
48
|
-
file: uriToPath(loc.uri),
|
|
49
|
-
line: pos.line,
|
|
50
|
-
column: pos.column,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (locations.length === 0) {
|
|
54
|
-
return {
|
|
55
|
-
content: [{ type: 'text', text: 'No definition found at this position.' }],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
let output = `**Definition(s)** for symbol at ${args.file}:${args.line}:${args.column}\n\n`;
|
|
59
|
-
for (const loc of locations) {
|
|
60
|
-
output += `- ${loc.file}:${loc.line}:${loc.column}\n`;
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
content: [{ type: 'text', text: output }],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { findProjectRoot } from '../utils/position.js';
|
|
4
|
-
export const diagnosticsSchema = {
|
|
5
|
-
path: z
|
|
6
|
-
.string()
|
|
7
|
-
.describe('Path to a Python file or directory to check'),
|
|
8
|
-
};
|
|
9
|
-
export async function diagnostics(args) {
|
|
10
|
-
const { path } = args;
|
|
11
|
-
// Determine project root from path
|
|
12
|
-
const projectRoot = findProjectRoot(path);
|
|
13
|
-
// Build pyright command - path can be file or directory
|
|
14
|
-
const target = path;
|
|
15
|
-
const cmd = `pyright "${target}" --outputjson`;
|
|
16
|
-
let output;
|
|
17
|
-
try {
|
|
18
|
-
const result = execSync(cmd, {
|
|
19
|
-
encoding: 'utf-8',
|
|
20
|
-
cwd: projectRoot,
|
|
21
|
-
timeout: 60000,
|
|
22
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
23
|
-
});
|
|
24
|
-
output = JSON.parse(result);
|
|
25
|
-
}
|
|
26
|
-
catch (e) {
|
|
27
|
-
const error = e;
|
|
28
|
-
// pyright returns non-zero exit code if there are errors
|
|
29
|
-
if (error.stdout) {
|
|
30
|
-
try {
|
|
31
|
-
output = JSON.parse(error.stdout);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return {
|
|
35
|
-
content: [
|
|
36
|
-
{
|
|
37
|
-
type: 'text',
|
|
38
|
-
text: `Error running pyright: ${error.message || 'Unknown error'}`,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: 'text',
|
|
49
|
-
text: `Error running pyright: ${error.message || 'Unknown error'}\n\nMake sure pyright is installed: npm install -g pyright`,
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const diags = output.generalDiagnostics || [];
|
|
56
|
-
const summary = output.summary;
|
|
57
|
-
if (diags.length === 0) {
|
|
58
|
-
let text = `**No issues found**\n\n`;
|
|
59
|
-
text += `- Files analyzed: ${summary.filesAnalyzed}\n`;
|
|
60
|
-
text += `- Time: ${summary.timeInSec}s`;
|
|
61
|
-
return {
|
|
62
|
-
content: [{ type: 'text', text }],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// Group by file
|
|
66
|
-
const byFile = new Map();
|
|
67
|
-
for (const diag of diags) {
|
|
68
|
-
const list = byFile.get(diag.file) || [];
|
|
69
|
-
list.push(diag);
|
|
70
|
-
byFile.set(diag.file, list);
|
|
71
|
-
}
|
|
72
|
-
let text = `**Diagnostics Summary**\n\n`;
|
|
73
|
-
text += `- Errors: ${summary.errorCount}\n`;
|
|
74
|
-
text += `- Warnings: ${summary.warningCount}\n`;
|
|
75
|
-
text += `- Information: ${summary.informationCount}\n`;
|
|
76
|
-
text += `- Files analyzed: ${summary.filesAnalyzed}\n`;
|
|
77
|
-
text += `- Time: ${summary.timeInSec}s\n\n`;
|
|
78
|
-
text += `---\n\n`;
|
|
79
|
-
for (const [filePath, fileDiags] of byFile) {
|
|
80
|
-
text += `### ${filePath}\n\n`;
|
|
81
|
-
for (const diag of fileDiags) {
|
|
82
|
-
const line = diag.range.start.line + 1;
|
|
83
|
-
const col = diag.range.start.character + 1;
|
|
84
|
-
const rule = diag.rule ? ` (${diag.rule})` : '';
|
|
85
|
-
const icon = diag.severity === 'error' ? '❌' : diag.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
86
|
-
text += `- ${icon} **${diag.severity}** at ${line}:${col}${rule}\n`;
|
|
87
|
-
text += ` ${diag.message}\n\n`;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
content: [{ type: 'text', text }],
|
|
92
|
-
};
|
|
93
|
-
}
|
package/dist/tools/hover.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getLspClient } from '../lsp-client.js';
|
|
3
|
-
import { toPosition, formatRange } from '../utils/position.js';
|
|
4
|
-
export const hoverSchema = {
|
|
5
|
-
file: z.string().describe('Absolute path to the Python file'),
|
|
6
|
-
line: z.number().int().positive().describe('Line number (1-based)'),
|
|
7
|
-
column: z.number().int().positive().describe('Column number (1-based)'),
|
|
8
|
-
};
|
|
9
|
-
export async function hover(args) {
|
|
10
|
-
console.error(`[hover] Starting hover for ${args.file}:${args.line}:${args.column}`);
|
|
11
|
-
const client = getLspClient();
|
|
12
|
-
const position = toPosition(args.line, args.column);
|
|
13
|
-
console.error(`[hover] Calling LSP hover...`);
|
|
14
|
-
const result = await client.hover(args.file, position);
|
|
15
|
-
console.error(`[hover] Got result: ${result ? 'yes' : 'no'}`);
|
|
16
|
-
if (!result) {
|
|
17
|
-
return {
|
|
18
|
-
content: [{ type: 'text', text: 'No hover information available at this position.' }],
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
let hoverText = '';
|
|
22
|
-
if (typeof result.contents === 'string') {
|
|
23
|
-
hoverText = result.contents;
|
|
24
|
-
}
|
|
25
|
-
else if (Array.isArray(result.contents)) {
|
|
26
|
-
hoverText = result.contents
|
|
27
|
-
.map((c) => (typeof c === 'string' ? c : c.value))
|
|
28
|
-
.join('\n\n');
|
|
29
|
-
}
|
|
30
|
-
else if ('kind' in result.contents) {
|
|
31
|
-
hoverText = result.contents.value;
|
|
32
|
-
}
|
|
33
|
-
else if ('value' in result.contents) {
|
|
34
|
-
hoverText = result.contents.value;
|
|
35
|
-
}
|
|
36
|
-
let output = `**Hover Info** at ${args.file}:${args.line}:${args.column}\n\n`;
|
|
37
|
-
output += hoverText;
|
|
38
|
-
if (result.range) {
|
|
39
|
-
output += `\n\n**Range:** ${formatRange(result.range)}`;
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: 'text', text: output }],
|
|
43
|
-
};
|
|
44
|
-
}
|
package/dist/tools/references.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
import { findProjectRoot } from '../utils/position.js';
|
|
5
|
-
export const referencesSchema = {
|
|
6
|
-
file: z.string().describe('Absolute path to the Python file'),
|
|
7
|
-
line: z.number().int().positive().describe('Line number (1-based)'),
|
|
8
|
-
column: z.number().int().positive().describe('Column number (1-based)'),
|
|
9
|
-
};
|
|
10
|
-
function getSymbolAtPosition(filePath, line, column) {
|
|
11
|
-
try {
|
|
12
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
13
|
-
const lines = content.split('\n');
|
|
14
|
-
const targetLine = lines[line - 1];
|
|
15
|
-
if (!targetLine)
|
|
16
|
-
return null;
|
|
17
|
-
// Find word at position
|
|
18
|
-
const col = column - 1;
|
|
19
|
-
let start = col;
|
|
20
|
-
let end = col;
|
|
21
|
-
// Expand left
|
|
22
|
-
while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
|
|
23
|
-
start--;
|
|
24
|
-
}
|
|
25
|
-
// Expand right
|
|
26
|
-
while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
|
|
27
|
-
end++;
|
|
28
|
-
}
|
|
29
|
-
const symbol = targetLine.slice(start, end);
|
|
30
|
-
return symbol.length > 0 ? symbol : null;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export async function references(args) {
|
|
37
|
-
const { file, line, column } = args;
|
|
38
|
-
// Get symbol name at position
|
|
39
|
-
const symbol = getSymbolAtPosition(file, line, column);
|
|
40
|
-
if (!symbol) {
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: 'text', text: 'Could not identify symbol at this position.' }],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
// Find project root
|
|
46
|
-
const projectRoot = findProjectRoot(file);
|
|
47
|
-
// Use ripgrep to find all references in Python files
|
|
48
|
-
// Match word boundaries to avoid partial matches
|
|
49
|
-
const pattern = `\\b${symbol}\\b`;
|
|
50
|
-
let rgOutput;
|
|
51
|
-
try {
|
|
52
|
-
// Try ripgrep first
|
|
53
|
-
rgOutput = execSync(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
|
|
54
|
-
encoding: 'utf-8',
|
|
55
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
56
|
-
timeout: 30000,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
const error = e;
|
|
61
|
-
if (error.status === 1) {
|
|
62
|
-
// No matches found
|
|
63
|
-
return {
|
|
64
|
-
content: [
|
|
65
|
-
{
|
|
66
|
-
type: 'text',
|
|
67
|
-
text: `No references found for symbol \`${symbol}\``,
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
// Try grep as fallback
|
|
73
|
-
try {
|
|
74
|
-
rgOutput = execSync(`grep -rn --include="*.py" -w "${symbol}" "${projectRoot}"`, {
|
|
75
|
-
encoding: 'utf-8',
|
|
76
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
77
|
-
timeout: 30000,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return {
|
|
82
|
-
content: [
|
|
83
|
-
{
|
|
84
|
-
type: 'text',
|
|
85
|
-
text: `No references found for symbol \`${symbol}\``,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Parse ripgrep output: file:line:column:text
|
|
92
|
-
const refs = [];
|
|
93
|
-
const lines = rgOutput.trim().split('\n').filter(Boolean);
|
|
94
|
-
for (const outputLine of lines) {
|
|
95
|
-
// Format: /path/to/file.py:10:5: some code here
|
|
96
|
-
const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
|
|
97
|
-
if (match) {
|
|
98
|
-
const [, filePath, lineNum, colNum, text] = match;
|
|
99
|
-
refs.push({
|
|
100
|
-
file: filePath,
|
|
101
|
-
line: parseInt(lineNum, 10),
|
|
102
|
-
column: parseInt(colNum, 10),
|
|
103
|
-
text: text.trim(),
|
|
104
|
-
isDefinition: filePath === file && parseInt(lineNum, 10) === line,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
// Fallback for grep format: /path/to/file.py:10: some code here
|
|
109
|
-
const grepMatch = outputLine.match(/^(.+?):(\d+):(.*)$/);
|
|
110
|
-
if (grepMatch) {
|
|
111
|
-
const [, filePath, lineNum, text] = grepMatch;
|
|
112
|
-
refs.push({
|
|
113
|
-
file: filePath,
|
|
114
|
-
line: parseInt(lineNum, 10),
|
|
115
|
-
column: 1,
|
|
116
|
-
text: text.trim(),
|
|
117
|
-
isDefinition: filePath === file && parseInt(lineNum, 10) === line,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (refs.length === 0) {
|
|
123
|
-
return {
|
|
124
|
-
content: [
|
|
125
|
-
{
|
|
126
|
-
type: 'text',
|
|
127
|
-
text: `No references found for symbol \`${symbol}\``,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
// Sort: definition first, then by file and line
|
|
133
|
-
refs.sort((a, b) => {
|
|
134
|
-
if (a.isDefinition && !b.isDefinition)
|
|
135
|
-
return -1;
|
|
136
|
-
if (!a.isDefinition && b.isDefinition)
|
|
137
|
-
return 1;
|
|
138
|
-
if (a.file !== b.file)
|
|
139
|
-
return a.file.localeCompare(b.file);
|
|
140
|
-
return a.line - b.line;
|
|
141
|
-
});
|
|
142
|
-
// Group by file
|
|
143
|
-
const byFile = new Map();
|
|
144
|
-
for (const ref of refs) {
|
|
145
|
-
const list = byFile.get(ref.file) || [];
|
|
146
|
-
list.push(ref);
|
|
147
|
-
byFile.set(ref.file, list);
|
|
148
|
-
}
|
|
149
|
-
let output = `**References** for \`${symbol}\`\n\n`;
|
|
150
|
-
output += `Found ${refs.length} reference(s) in ${byFile.size} file(s):\n\n`;
|
|
151
|
-
for (const [filePath, fileRefs] of byFile) {
|
|
152
|
-
output += `### ${filePath}\n\n`;
|
|
153
|
-
for (const ref of fileRefs) {
|
|
154
|
-
const marker = ref.isDefinition ? ' (definition)' : '';
|
|
155
|
-
output += `- **${ref.line}:${ref.column}**${marker}: \`${ref.text}\`\n`;
|
|
156
|
-
}
|
|
157
|
-
output += '\n';
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
content: [{ type: 'text', text: output }],
|
|
161
|
-
};
|
|
162
|
-
}
|