@treedy/pyright-mcp 1.1.1 → 1.1.2
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/dist/tools/references.d.ts +0 -2
- package/dist/tools/references.js +147 -13
- package/dist/tools/rename.js +135 -26
- package/package.json +1 -1
|
@@ -3,13 +3,11 @@ export declare const referencesSchema: {
|
|
|
3
3
|
file: z.ZodString;
|
|
4
4
|
line: z.ZodNumber;
|
|
5
5
|
column: z.ZodNumber;
|
|
6
|
-
includeDeclaration: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
7
6
|
};
|
|
8
7
|
export declare function references(args: {
|
|
9
8
|
file: string;
|
|
10
9
|
line: number;
|
|
11
10
|
column: number;
|
|
12
|
-
includeDeclaration?: boolean;
|
|
13
11
|
}): Promise<{
|
|
14
12
|
content: {
|
|
15
13
|
type: "text";
|
package/dist/tools/references.js
CHANGED
|
@@ -1,26 +1,160 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { findProjectRoot } from '../utils/position.js';
|
|
4
5
|
export const referencesSchema = {
|
|
5
6
|
file: z.string().describe('Absolute path to the Python file'),
|
|
6
7
|
line: z.number().int().positive().describe('Line number (1-based)'),
|
|
7
8
|
column: z.number().int().positive().describe('Column number (1-based)'),
|
|
8
|
-
includeDeclaration: z.boolean().optional().default(true).describe('Include the declaration in results'),
|
|
9
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
|
+
}
|
|
10
36
|
export async function references(args) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
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) {
|
|
15
123
|
return {
|
|
16
|
-
content: [
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: 'text',
|
|
127
|
+
text: `No references found for symbol \`${symbol}\``,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
17
130
|
};
|
|
18
131
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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';
|
|
24
158
|
}
|
|
25
159
|
return {
|
|
26
160
|
content: [{ type: 'text', text: output }],
|
package/dist/tools/rename.js
CHANGED
|
@@ -1,43 +1,152 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { findProjectRoot } from '../utils/position.js';
|
|
4
5
|
export const renameSchema = {
|
|
5
6
|
file: z.string().describe('Absolute path to the Python file'),
|
|
6
7
|
line: z.number().int().positive().describe('Line number (1-based)'),
|
|
7
8
|
column: z.number().int().positive().describe('Column number (1-based)'),
|
|
8
9
|
newName: z.string().describe('New name for the symbol'),
|
|
9
10
|
};
|
|
11
|
+
function getSymbolAtPosition(filePath, line, column) {
|
|
12
|
+
try {
|
|
13
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
const targetLine = lines[line - 1];
|
|
16
|
+
if (!targetLine)
|
|
17
|
+
return null;
|
|
18
|
+
// Find word at position
|
|
19
|
+
const col = column - 1;
|
|
20
|
+
let start = col;
|
|
21
|
+
let end = col;
|
|
22
|
+
// Expand left
|
|
23
|
+
while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
|
|
24
|
+
start--;
|
|
25
|
+
}
|
|
26
|
+
// Expand right
|
|
27
|
+
while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
|
|
28
|
+
end++;
|
|
29
|
+
}
|
|
30
|
+
const symbol = targetLine.slice(start, end);
|
|
31
|
+
return symbol.length > 0 ? { symbol, start: start + 1, end: end + 1 } : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
10
37
|
export async function rename(args) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
38
|
+
const { file, line, column, newName } = args;
|
|
39
|
+
// Get symbol name at position
|
|
40
|
+
const symbolInfo = getSymbolAtPosition(file, line, column);
|
|
41
|
+
if (!symbolInfo) {
|
|
15
42
|
return {
|
|
16
|
-
content: [{ type: 'text', text: '
|
|
43
|
+
content: [{ type: 'text', text: 'Could not identify symbol at this position.' }],
|
|
17
44
|
};
|
|
18
45
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
const { symbol: oldName } = symbolInfo;
|
|
47
|
+
if (oldName === newName) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: 'New name is the same as the old name.' }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Find project root
|
|
53
|
+
const projectRoot = findProjectRoot(file);
|
|
54
|
+
// Use ripgrep to find all references in Python files
|
|
55
|
+
const pattern = `\\b${oldName}\\b`;
|
|
56
|
+
let rgOutput;
|
|
57
|
+
try {
|
|
58
|
+
rgOutput = execSync(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
61
|
+
timeout: 30000,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
const error = e;
|
|
66
|
+
if (error.status === 1) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: `No references found for symbol \`${oldName}\``,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Try grep as fallback
|
|
77
|
+
try {
|
|
78
|
+
rgOutput = execSync(`grep -rn --include="*.py" -w "${oldName}" "${projectRoot}"`, {
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
81
|
+
timeout: 30000,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `No references found for symbol \`${oldName}\``,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
33
93
|
}
|
|
34
|
-
output += `Total: ${totalEdits} edit(s) across ${Object.keys(result.changes).length} file(s)\n`;
|
|
35
94
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
95
|
+
// Parse output and create edits
|
|
96
|
+
const edits = [];
|
|
97
|
+
const outputLines = rgOutput.trim().split('\n').filter(Boolean);
|
|
98
|
+
for (const outputLine of outputLines) {
|
|
99
|
+
// Format: /path/to/file.py:10:5: some code here
|
|
100
|
+
const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
|
|
101
|
+
if (match) {
|
|
102
|
+
const [, filePath, lineNum, colNum, lineContent] = match;
|
|
103
|
+
const col = parseInt(colNum, 10);
|
|
104
|
+
edits.push({
|
|
105
|
+
file: filePath,
|
|
106
|
+
line: parseInt(lineNum, 10),
|
|
107
|
+
column: col,
|
|
108
|
+
endColumn: col + oldName.length,
|
|
109
|
+
oldText: oldName,
|
|
110
|
+
newText: newName,
|
|
111
|
+
lineContent: lineContent.trim(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (edits.length === 0) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: `No references found for symbol \`${oldName}\``,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Group by file
|
|
126
|
+
const byFile = new Map();
|
|
127
|
+
for (const edit of edits) {
|
|
128
|
+
const list = byFile.get(edit.file) || [];
|
|
129
|
+
list.push(edit);
|
|
130
|
+
byFile.set(edit.file, list);
|
|
131
|
+
}
|
|
132
|
+
let output = `**Rename Preview**\n\n`;
|
|
133
|
+
output += `- Symbol: \`${oldName}\` → \`${newName}\`\n`;
|
|
134
|
+
output += `- Found ${edits.length} occurrence(s) in ${byFile.size} file(s)\n\n`;
|
|
135
|
+
output += `---\n\n`;
|
|
136
|
+
for (const [filePath, fileEdits] of byFile) {
|
|
137
|
+
output += `### ${filePath}\n\n`;
|
|
138
|
+
for (const edit of fileEdits) {
|
|
139
|
+
const preview = edit.lineContent.replace(new RegExp(`\\b${oldName}\\b`), `~~${oldName}~~ **${newName}**`);
|
|
140
|
+
output += `- Line ${edit.line}: ${preview}\n`;
|
|
141
|
+
}
|
|
142
|
+
output += '\n';
|
|
39
143
|
}
|
|
40
|
-
output +=
|
|
144
|
+
output += `---\n\n`;
|
|
145
|
+
output += `**Note:** This is a preview only. To apply the rename, use your editor's rename feature or run:\n`;
|
|
146
|
+
output += `\`\`\`bash\n`;
|
|
147
|
+
output += `# Using sed (backup recommended)\n`;
|
|
148
|
+
output += `find "${projectRoot}" -name "*.py" -exec sed -i '' 's/\\b${oldName}\\b/${newName}/g' {} +\n`;
|
|
149
|
+
output += `\`\`\``;
|
|
41
150
|
return {
|
|
42
151
|
content: [{ type: 'text', text: output }],
|
|
43
152
|
};
|