stravinsky 0.2.67__py3-none-any.whl → 0.4.18__py3-none-any.whl
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.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/token_store.py +113 -11
- mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/hook_config.py +249 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +222 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/__init__.py +8 -3
- mcp_bridge/hooks/manager.py +8 -0
- mcp_bridge/hooks/tool_messaging.py +113 -10
- mcp_bridge/notifications.py +151 -0
- mcp_bridge/server.py +202 -48
- mcp_bridge/server_tools.py +440 -0
- mcp_bridge/tools/__init__.py +22 -18
- mcp_bridge/tools/agent_manager.py +197 -28
- mcp_bridge/tools/code_search.py +16 -2
- mcp_bridge/tools/lsp/__init__.py +7 -0
- mcp_bridge/tools/lsp/manager.py +448 -0
- mcp_bridge/tools/lsp/tools.py +634 -151
- mcp_bridge/tools/model_invoke.py +186 -159
- mcp_bridge/tools/query_classifier.py +323 -0
- mcp_bridge/tools/semantic_search.py +3042 -0
- mcp_bridge/update_manager.py +589 -0
- mcp_bridge/update_manager_pypi.py +299 -0
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.18.dist-info}/METADATA +209 -25
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.18.dist-info}/RECORD +29 -17
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.18.dist-info}/WHEEL +0 -0
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.18.dist-info}/entry_points.txt +0 -0
mcp_bridge/tools/lsp/tools.py
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
"""
|
|
2
2
|
LSP Tools - Advanced Language Server Protocol Operations
|
|
3
3
|
|
|
4
|
-
Provides comprehensive LSP functionality via
|
|
4
|
+
Provides comprehensive LSP functionality via persistent connections to language servers.
|
|
5
5
|
Supplements Claude Code's native LSP support with advanced operations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
|
+
import logging
|
|
10
11
|
import subprocess
|
|
11
|
-
import
|
|
12
|
+
import sys
|
|
12
13
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
-
import
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
15
|
+
from urllib.parse import unquote, urlparse
|
|
16
|
+
|
|
17
|
+
# Use lsprotocol for types
|
|
18
|
+
try:
|
|
19
|
+
from lsprotocol.types import (
|
|
20
|
+
CodeActionContext,
|
|
21
|
+
CodeActionParams,
|
|
22
|
+
CodeActionTriggerKind,
|
|
23
|
+
DidCloseTextDocumentParams,
|
|
24
|
+
DidOpenTextDocumentParams,
|
|
25
|
+
DocumentSymbolParams,
|
|
26
|
+
HoverParams,
|
|
27
|
+
Location,
|
|
28
|
+
Position,
|
|
29
|
+
Range,
|
|
30
|
+
ReferenceContext,
|
|
31
|
+
ReferenceParams,
|
|
32
|
+
RenameParams,
|
|
33
|
+
TextDocumentIdentifier,
|
|
34
|
+
TextDocumentItem,
|
|
35
|
+
TextDocumentPositionParams,
|
|
36
|
+
WorkspaceSymbolParams,
|
|
37
|
+
PrepareRenameParams,
|
|
38
|
+
)
|
|
39
|
+
except ImportError:
|
|
40
|
+
# Fallback/Mock for environment without lsprotocol
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
from .manager import get_lsp_manager
|
|
15
44
|
|
|
16
45
|
logger = logging.getLogger(__name__)
|
|
17
46
|
|
|
@@ -37,42 +66,92 @@ def _get_language_for_file(file_path: str) -> str:
|
|
|
37
66
|
return mapping.get(suffix, "unknown")
|
|
38
67
|
|
|
39
68
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
async def _get_client_and_params(
|
|
70
|
+
file_path: str, needs_open: bool = True
|
|
71
|
+
) -> Tuple[Optional[Any], Optional[str], str]:
|
|
72
|
+
"""
|
|
73
|
+
Get LSP client and prepare file for operations.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
(client, uri, language)
|
|
77
|
+
"""
|
|
78
|
+
path = Path(file_path)
|
|
79
|
+
if not path.exists():
|
|
80
|
+
return None, None, "unknown"
|
|
81
|
+
|
|
82
|
+
lang = _get_language_for_file(file_path)
|
|
83
|
+
manager = get_lsp_manager()
|
|
84
|
+
client = await manager.get_server(lang)
|
|
85
|
+
|
|
86
|
+
if not client:
|
|
87
|
+
return None, None, lang
|
|
88
|
+
|
|
89
|
+
uri = f"file://{path.absolute()}"
|
|
90
|
+
|
|
91
|
+
if needs_open:
|
|
92
|
+
try:
|
|
93
|
+
content = path.read_text()
|
|
94
|
+
# Send didOpen notification
|
|
95
|
+
# We don't check if it's already open because we're stateless-ish
|
|
96
|
+
# and want to ensure fresh content.
|
|
97
|
+
# Using version=1
|
|
98
|
+
params = DidOpenTextDocumentParams(
|
|
99
|
+
text_document=TextDocumentItem(uri=uri, language_id=lang, version=1, text=content)
|
|
100
|
+
)
|
|
101
|
+
client.protocol.notify("textDocument/didOpen", params)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.warning(f"Failed to send didOpen for {file_path}: {e}")
|
|
104
|
+
|
|
105
|
+
return client, uri, lang
|
|
46
106
|
|
|
47
107
|
|
|
48
108
|
async def lsp_hover(file_path: str, line: int, character: int) -> str:
|
|
49
109
|
"""
|
|
50
110
|
Get type info, documentation, and signature at a position.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
file_path: Absolute path to the file
|
|
54
|
-
line: Line number (1-indexed)
|
|
55
|
-
character: Character position (0-indexed)
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Type information and documentation at the position.
|
|
59
111
|
"""
|
|
60
112
|
# USER-VISIBLE NOTIFICATION
|
|
61
|
-
import sys
|
|
62
113
|
print(f"📍 LSP-HOVER: {file_path}:{line}:{character}", file=sys.stderr)
|
|
63
114
|
|
|
115
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
116
|
+
|
|
117
|
+
if client:
|
|
118
|
+
try:
|
|
119
|
+
params = HoverParams(
|
|
120
|
+
text_document=TextDocumentIdentifier(uri=uri),
|
|
121
|
+
position=Position(line=line - 1, character=character),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
response = await asyncio.wait_for(
|
|
125
|
+
client.protocol.send_request_async("textDocument/hover", params), timeout=5.0
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if response and response.contents:
|
|
129
|
+
# Handle MarkupContent or text
|
|
130
|
+
contents = response.contents
|
|
131
|
+
if hasattr(contents, "value"):
|
|
132
|
+
return contents.value
|
|
133
|
+
elif isinstance(contents, list):
|
|
134
|
+
return "\n".join([str(c) for c in contents])
|
|
135
|
+
return str(contents)
|
|
136
|
+
|
|
137
|
+
return f"No hover info at line {line}, character {character}"
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"LSP hover failed: {e}")
|
|
141
|
+
# Fall through to legacy fallback
|
|
142
|
+
|
|
143
|
+
# Legacy Fallback
|
|
64
144
|
path = Path(file_path)
|
|
65
145
|
if not path.exists():
|
|
66
146
|
return f"Error: File not found: {file_path}"
|
|
67
|
-
|
|
68
|
-
lang = _get_language_for_file(file_path)
|
|
69
|
-
|
|
147
|
+
|
|
70
148
|
try:
|
|
71
149
|
if lang == "python":
|
|
72
150
|
# Use jedi for Python hover info
|
|
73
151
|
result = subprocess.run(
|
|
74
152
|
[
|
|
75
|
-
"python",
|
|
153
|
+
"python",
|
|
154
|
+
"-c",
|
|
76
155
|
f"""
|
|
77
156
|
import jedi
|
|
78
157
|
script = jedi.Script(path='{file_path}')
|
|
@@ -82,7 +161,7 @@ for c in completions[:1]:
|
|
|
82
161
|
logger.info(f"Name: {{c.full_name}}")
|
|
83
162
|
if c.docstring():
|
|
84
163
|
logger.info(f"\\nDocstring:\\n{{c.docstring()[:500]}}")
|
|
85
|
-
"""
|
|
164
|
+
""",
|
|
86
165
|
],
|
|
87
166
|
capture_output=True,
|
|
88
167
|
text=True,
|
|
@@ -92,15 +171,13 @@ for c in completions[:1]:
|
|
|
92
171
|
if output:
|
|
93
172
|
return output
|
|
94
173
|
return f"No hover info at line {line}, character {character}"
|
|
95
|
-
|
|
174
|
+
|
|
96
175
|
elif lang in ("typescript", "javascript", "typescriptreact", "javascriptreact"):
|
|
97
|
-
# Use tsserver via quick-info
|
|
98
|
-
# For simplicity, fall back to message
|
|
99
176
|
return f"TypeScript hover requires running language server. Use Claude Code's native hover."
|
|
100
|
-
|
|
177
|
+
|
|
101
178
|
else:
|
|
102
179
|
return f"Hover not available for language: {lang}"
|
|
103
|
-
|
|
180
|
+
|
|
104
181
|
except FileNotFoundError as e:
|
|
105
182
|
return f"Tool not found: {e.filename}. Install jedi: pip install jedi"
|
|
106
183
|
except subprocess.TimeoutExpired:
|
|
@@ -112,33 +189,68 @@ for c in completions[:1]:
|
|
|
112
189
|
async def lsp_goto_definition(file_path: str, line: int, character: int) -> str:
|
|
113
190
|
"""
|
|
114
191
|
Find where a symbol is defined.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
file_path: Absolute path to the file
|
|
118
|
-
line: Line number (1-indexed)
|
|
119
|
-
character: Character position (0-indexed)
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
Location(s) where the symbol is defined.
|
|
123
192
|
"""
|
|
193
|
+
# USER-VISIBLE NOTIFICATION
|
|
194
|
+
print(f"🎯 LSP-GOTO-DEF: {file_path}:{line}:{character}", file=sys.stderr)
|
|
195
|
+
|
|
196
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
197
|
+
|
|
198
|
+
if client:
|
|
199
|
+
try:
|
|
200
|
+
params = TextDocumentPositionParams(
|
|
201
|
+
text_document=TextDocumentIdentifier(uri=uri),
|
|
202
|
+
position=Position(line=line - 1, character=character),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
response = await asyncio.wait_for(
|
|
206
|
+
client.protocol.send_request_async("textDocument/definition", params), timeout=5.0
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if response:
|
|
210
|
+
if isinstance(response, list):
|
|
211
|
+
locations = response
|
|
212
|
+
else:
|
|
213
|
+
locations = [response]
|
|
214
|
+
|
|
215
|
+
results = []
|
|
216
|
+
for loc in locations:
|
|
217
|
+
# Parse URI to path
|
|
218
|
+
target_uri = loc.uri
|
|
219
|
+
parsed = urlparse(target_uri)
|
|
220
|
+
target_path = unquote(parsed.path)
|
|
221
|
+
|
|
222
|
+
# Handle range
|
|
223
|
+
start_line = loc.range.start.line + 1
|
|
224
|
+
start_char = loc.range.start.character
|
|
225
|
+
results.append(f"{target_path}:{start_line}:{start_char}")
|
|
226
|
+
|
|
227
|
+
if results:
|
|
228
|
+
return "\n".join(results)
|
|
229
|
+
|
|
230
|
+
return "No definition found"
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"LSP goto definition failed: {e}")
|
|
234
|
+
# Fall through
|
|
235
|
+
|
|
236
|
+
# Legacy fallback logic... (copy from existing)
|
|
124
237
|
path = Path(file_path)
|
|
125
238
|
if not path.exists():
|
|
126
239
|
return f"Error: File not found: {file_path}"
|
|
127
|
-
|
|
128
|
-
lang = _get_language_for_file(file_path)
|
|
129
|
-
|
|
240
|
+
|
|
130
241
|
try:
|
|
131
242
|
if lang == "python":
|
|
132
243
|
result = subprocess.run(
|
|
133
244
|
[
|
|
134
|
-
"python",
|
|
245
|
+
"python",
|
|
246
|
+
"-c",
|
|
135
247
|
f"""
|
|
136
248
|
import jedi
|
|
137
249
|
script = jedi.Script(path='{file_path}')
|
|
138
250
|
definitions = script.goto({line}, {character})
|
|
139
251
|
for d in definitions:
|
|
140
252
|
logger.info(f"{{d.module_path}}:{{d.line}}:{{d.column}} - {{d.full_name}}")
|
|
141
|
-
"""
|
|
253
|
+
""",
|
|
142
254
|
],
|
|
143
255
|
capture_output=True,
|
|
144
256
|
text=True,
|
|
@@ -148,13 +260,13 @@ for d in definitions:
|
|
|
148
260
|
if output:
|
|
149
261
|
return output
|
|
150
262
|
return "No definition found"
|
|
151
|
-
|
|
263
|
+
|
|
152
264
|
elif lang in ("typescript", "javascript"):
|
|
153
265
|
return "TypeScript goto definition requires running language server. Use Claude Code's native navigation."
|
|
154
|
-
|
|
266
|
+
|
|
155
267
|
else:
|
|
156
268
|
return f"Goto definition not available for language: {lang}"
|
|
157
|
-
|
|
269
|
+
|
|
158
270
|
except FileNotFoundError as e:
|
|
159
271
|
return f"Tool not found: Install jedi: pip install jedi"
|
|
160
272
|
except subprocess.TimeoutExpired:
|
|
@@ -164,34 +276,62 @@ for d in definitions:
|
|
|
164
276
|
|
|
165
277
|
|
|
166
278
|
async def lsp_find_references(
|
|
167
|
-
file_path: str,
|
|
168
|
-
line: int,
|
|
169
|
-
character: int,
|
|
170
|
-
include_declaration: bool = True
|
|
279
|
+
file_path: str, line: int, character: int, include_declaration: bool = True
|
|
171
280
|
) -> str:
|
|
172
281
|
"""
|
|
173
282
|
Find all references to a symbol across the workspace.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
file_path: Absolute path to the file
|
|
177
|
-
line: Line number (1-indexed)
|
|
178
|
-
character: Character position (0-indexed)
|
|
179
|
-
include_declaration: Include the declaration itself
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
All locations where the symbol is used.
|
|
183
283
|
"""
|
|
284
|
+
# USER-VISIBLE NOTIFICATION
|
|
285
|
+
print(f"🔗 LSP-REFS: {file_path}:{line}:{character}", file=sys.stderr)
|
|
286
|
+
|
|
287
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
288
|
+
|
|
289
|
+
if client:
|
|
290
|
+
try:
|
|
291
|
+
params = ReferenceParams(
|
|
292
|
+
text_document=TextDocumentIdentifier(uri=uri),
|
|
293
|
+
position=Position(line=line - 1, character=character),
|
|
294
|
+
context=ReferenceContext(include_declaration=include_declaration),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
response = await asyncio.wait_for(
|
|
298
|
+
client.protocol.send_request_async("textDocument/references", params), timeout=10.0
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if response:
|
|
302
|
+
results = []
|
|
303
|
+
for loc in response:
|
|
304
|
+
# Parse URI to path
|
|
305
|
+
target_uri = loc.uri
|
|
306
|
+
parsed = urlparse(target_uri)
|
|
307
|
+
target_path = unquote(parsed.path)
|
|
308
|
+
|
|
309
|
+
start_line = loc.range.start.line + 1
|
|
310
|
+
start_char = loc.range.start.character
|
|
311
|
+
results.append(f"{target_path}:{start_line}:{start_char}")
|
|
312
|
+
|
|
313
|
+
if results:
|
|
314
|
+
# Limit output
|
|
315
|
+
if len(results) > 50:
|
|
316
|
+
return "\n".join(results[:50]) + f"\n... and {len(results) - 50} more"
|
|
317
|
+
return "\n".join(results)
|
|
318
|
+
|
|
319
|
+
return "No references found"
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"LSP find references failed: {e}")
|
|
323
|
+
|
|
324
|
+
# Legacy fallback...
|
|
184
325
|
path = Path(file_path)
|
|
185
326
|
if not path.exists():
|
|
186
327
|
return f"Error: File not found: {file_path}"
|
|
187
|
-
|
|
188
|
-
lang = _get_language_for_file(file_path)
|
|
189
|
-
|
|
328
|
+
|
|
190
329
|
try:
|
|
191
330
|
if lang == "python":
|
|
192
331
|
result = subprocess.run(
|
|
193
332
|
[
|
|
194
|
-
"python",
|
|
333
|
+
"python",
|
|
334
|
+
"-c",
|
|
195
335
|
f"""
|
|
196
336
|
import jedi
|
|
197
337
|
script = jedi.Script(path='{file_path}')
|
|
@@ -200,7 +340,7 @@ for r in references[:30]:
|
|
|
200
340
|
logger.info(f"{{r.module_path}}:{{r.line}}:{{r.column}}")
|
|
201
341
|
if len(references) > 30:
|
|
202
342
|
logger.info(f"... and {{len(references) - 30}} more")
|
|
203
|
-
"""
|
|
343
|
+
""",
|
|
204
344
|
],
|
|
205
345
|
capture_output=True,
|
|
206
346
|
text=True,
|
|
@@ -210,10 +350,10 @@ if len(references) > 30:
|
|
|
210
350
|
if output:
|
|
211
351
|
return output
|
|
212
352
|
return "No references found"
|
|
213
|
-
|
|
353
|
+
|
|
214
354
|
else:
|
|
215
355
|
return f"Find references not available for language: {lang}"
|
|
216
|
-
|
|
356
|
+
|
|
217
357
|
except subprocess.TimeoutExpired:
|
|
218
358
|
return "Reference search timed out"
|
|
219
359
|
except Exception as e:
|
|
@@ -223,24 +363,72 @@ if len(references) > 30:
|
|
|
223
363
|
async def lsp_document_symbols(file_path: str) -> str:
|
|
224
364
|
"""
|
|
225
365
|
Get hierarchical outline of all symbols in a file.
|
|
226
|
-
|
|
227
|
-
Args:
|
|
228
|
-
file_path: Absolute path to the file
|
|
229
|
-
|
|
230
|
-
Returns:
|
|
231
|
-
Structured list of functions, classes, methods in the file.
|
|
232
366
|
"""
|
|
367
|
+
# USER-VISIBLE NOTIFICATION
|
|
368
|
+
print(f"📋 LSP-SYMBOLS: {file_path}", file=sys.stderr)
|
|
369
|
+
|
|
370
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
371
|
+
|
|
372
|
+
if client:
|
|
373
|
+
try:
|
|
374
|
+
params = DocumentSymbolParams(text_document=TextDocumentIdentifier(uri=uri))
|
|
375
|
+
|
|
376
|
+
response = await asyncio.wait_for(
|
|
377
|
+
client.protocol.send_request_async("textDocument/documentSymbol", params),
|
|
378
|
+
timeout=5.0,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if response:
|
|
382
|
+
lines = []
|
|
383
|
+
# response can be List[DocumentSymbol] or List[SymbolInformation]
|
|
384
|
+
# We'll handle a flat list representation for simplicity or traverse if hierarchical
|
|
385
|
+
# For output, a simple flat list with indentation is good.
|
|
386
|
+
|
|
387
|
+
# Helper to process symbols
|
|
388
|
+
def process_symbols(symbols, indent=0):
|
|
389
|
+
for sym in symbols:
|
|
390
|
+
name = sym.name
|
|
391
|
+
kind = str(sym.kind) # Enum integer
|
|
392
|
+
# Map some kinds to text if possible, but int is fine or name
|
|
393
|
+
|
|
394
|
+
# Handle location
|
|
395
|
+
if hasattr(sym, "range"): # DocumentSymbol
|
|
396
|
+
line = sym.range.start.line + 1
|
|
397
|
+
children = getattr(sym, "children", [])
|
|
398
|
+
else: # SymbolInformation
|
|
399
|
+
line = sym.location.range.start.line + 1
|
|
400
|
+
children = []
|
|
401
|
+
|
|
402
|
+
lines.append(f"{line:4d} | {' ' * indent}{kind:4} {name}")
|
|
403
|
+
|
|
404
|
+
if children:
|
|
405
|
+
process_symbols(children, indent + 1)
|
|
406
|
+
|
|
407
|
+
process_symbols(response)
|
|
408
|
+
|
|
409
|
+
if lines:
|
|
410
|
+
return (
|
|
411
|
+
f"**Symbols in {Path(file_path).name}:**\n```\nLine | Kind Name\n"
|
|
412
|
+
+ "\n".join(lines)
|
|
413
|
+
+ "\n```"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return "No symbols found"
|
|
417
|
+
|
|
418
|
+
except Exception as e:
|
|
419
|
+
logger.error(f"LSP document symbols failed: {e}")
|
|
420
|
+
|
|
421
|
+
# Legacy fallback...
|
|
233
422
|
path = Path(file_path)
|
|
234
423
|
if not path.exists():
|
|
235
424
|
return f"Error: File not found: {file_path}"
|
|
236
|
-
|
|
237
|
-
lang = _get_language_for_file(file_path)
|
|
238
|
-
|
|
425
|
+
|
|
239
426
|
try:
|
|
240
427
|
if lang == "python":
|
|
241
428
|
result = subprocess.run(
|
|
242
429
|
[
|
|
243
|
-
"python",
|
|
430
|
+
"python",
|
|
431
|
+
"-c",
|
|
244
432
|
f"""
|
|
245
433
|
import jedi
|
|
246
434
|
script = jedi.Script(path='{file_path}')
|
|
@@ -248,7 +436,7 @@ names = script.get_names(all_scopes=True, definitions=True)
|
|
|
248
436
|
for n in names:
|
|
249
437
|
indent = " " * (n.get_line_code().count(" ") if n.get_line_code() else 0)
|
|
250
438
|
logger.info(f"{{n.line:4d}} | {{indent}}{{n.type:10}} {{n.name}}")
|
|
251
|
-
"""
|
|
439
|
+
""",
|
|
252
440
|
],
|
|
253
441
|
capture_output=True,
|
|
254
442
|
text=True,
|
|
@@ -258,7 +446,7 @@ for n in names:
|
|
|
258
446
|
if output:
|
|
259
447
|
return f"**Symbols in {path.name}:**\n```\nLine | Symbol\n{output}\n```"
|
|
260
448
|
return "No symbols found"
|
|
261
|
-
|
|
449
|
+
|
|
262
450
|
else:
|
|
263
451
|
# Fallback: use ctags
|
|
264
452
|
result = subprocess.run(
|
|
@@ -271,7 +459,7 @@ for n in names:
|
|
|
271
459
|
if output:
|
|
272
460
|
return f"**Symbols in {path.name}:**\n```\n{output}\n```"
|
|
273
461
|
return "No symbols found"
|
|
274
|
-
|
|
462
|
+
|
|
275
463
|
except FileNotFoundError:
|
|
276
464
|
return "Install jedi (pip install jedi) or ctags for symbol lookup"
|
|
277
465
|
except subprocess.TimeoutExpired:
|
|
@@ -283,14 +471,40 @@ for n in names:
|
|
|
283
471
|
async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
284
472
|
"""
|
|
285
473
|
Search for symbols by name across the entire workspace.
|
|
286
|
-
|
|
287
|
-
Args:
|
|
288
|
-
query: Symbol name to search for (fuzzy match)
|
|
289
|
-
directory: Workspace directory
|
|
290
|
-
|
|
291
|
-
Returns:
|
|
292
|
-
Matching symbols with their locations.
|
|
293
474
|
"""
|
|
475
|
+
# USER-VISIBLE NOTIFICATION
|
|
476
|
+
print(f"🔍 LSP-WS-SYMBOLS: query='{query}' dir={directory}", file=sys.stderr)
|
|
477
|
+
|
|
478
|
+
# We need any client (python/ts) to search workspace, or maybe all of them?
|
|
479
|
+
# Workspace symbols usually require a server to be initialized.
|
|
480
|
+
# We can try to get python server if available, or just fallback to ripgrep if no persistent server is appropriate.
|
|
481
|
+
# LSP 'workspace/symbol' is language-specific.
|
|
482
|
+
|
|
483
|
+
manager = get_lsp_manager()
|
|
484
|
+
results = []
|
|
485
|
+
|
|
486
|
+
# Try Python
|
|
487
|
+
client_py = await manager.get_server("python")
|
|
488
|
+
if client_py:
|
|
489
|
+
try:
|
|
490
|
+
params = WorkspaceSymbolParams(query=query)
|
|
491
|
+
response = await asyncio.wait_for(
|
|
492
|
+
client_py.protocol.send_request_async("workspace/symbol", params), timeout=5.0
|
|
493
|
+
)
|
|
494
|
+
if response:
|
|
495
|
+
for sym in response:
|
|
496
|
+
target_uri = sym.location.uri
|
|
497
|
+
parsed = urlparse(target_uri)
|
|
498
|
+
target_path = unquote(parsed.path)
|
|
499
|
+
line = sym.location.range.start.line + 1
|
|
500
|
+
results.append(f"{target_path}:{line} - {sym.name} ({sym.kind})")
|
|
501
|
+
except Exception as e:
|
|
502
|
+
logger.error(f"LSP workspace symbols (python) failed: {e}")
|
|
503
|
+
|
|
504
|
+
if results:
|
|
505
|
+
return "\n".join(results[:20])
|
|
506
|
+
|
|
507
|
+
# Fallback to legacy grep/ctags
|
|
294
508
|
try:
|
|
295
509
|
# Use ctags to index and grep for symbols
|
|
296
510
|
result = subprocess.run(
|
|
@@ -299,12 +513,12 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
299
513
|
text=True,
|
|
300
514
|
timeout=15,
|
|
301
515
|
)
|
|
302
|
-
|
|
516
|
+
|
|
303
517
|
files = result.stdout.strip().split("\n")[:10] # Limit files
|
|
304
|
-
|
|
518
|
+
|
|
305
519
|
if not files or files == [""]:
|
|
306
520
|
return "No matching files found"
|
|
307
|
-
|
|
521
|
+
|
|
308
522
|
symbols = []
|
|
309
523
|
for f in files:
|
|
310
524
|
if not f:
|
|
@@ -319,11 +533,11 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
319
533
|
for line in ctags_result.stdout.split("\n"):
|
|
320
534
|
if query.lower() in line.lower():
|
|
321
535
|
symbols.append(line)
|
|
322
|
-
|
|
536
|
+
|
|
323
537
|
if symbols:
|
|
324
538
|
return "\n".join(symbols[:20])
|
|
325
539
|
return f"No symbols matching '{query}' found"
|
|
326
|
-
|
|
540
|
+
|
|
327
541
|
except FileNotFoundError:
|
|
328
542
|
return "Install ctags and ripgrep for workspace symbol search"
|
|
329
543
|
except subprocess.TimeoutExpired:
|
|
@@ -335,26 +549,48 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
335
549
|
async def lsp_prepare_rename(file_path: str, line: int, character: int) -> str:
|
|
336
550
|
"""
|
|
337
551
|
Check if a symbol at position can be renamed.
|
|
338
|
-
|
|
339
|
-
Args:
|
|
340
|
-
file_path: Absolute path to the file
|
|
341
|
-
line: Line number (1-indexed)
|
|
342
|
-
character: Character position (0-indexed)
|
|
343
|
-
|
|
344
|
-
Returns:
|
|
345
|
-
The symbol that would be renamed and validation status.
|
|
346
552
|
"""
|
|
553
|
+
# USER-VISIBLE NOTIFICATION
|
|
554
|
+
print(f"✏️ LSP-PREP-RENAME: {file_path}:{line}:{character}", file=sys.stderr)
|
|
555
|
+
|
|
556
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
557
|
+
|
|
558
|
+
if client:
|
|
559
|
+
try:
|
|
560
|
+
params = PrepareRenameParams(
|
|
561
|
+
text_document=TextDocumentIdentifier(uri=uri),
|
|
562
|
+
position=Position(line=line - 1, character=character),
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
response = await asyncio.wait_for(
|
|
566
|
+
client.protocol.send_request_async("textDocument/prepareRename", params),
|
|
567
|
+
timeout=5.0,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
if response:
|
|
571
|
+
# Response can be Range, {range, placeholder}, or null
|
|
572
|
+
if hasattr(response, "placeholder"):
|
|
573
|
+
return f"✅ Rename is valid. Current name: {response.placeholder}"
|
|
574
|
+
return "✅ Rename is valid at this position"
|
|
575
|
+
|
|
576
|
+
# If null/false, invalid
|
|
577
|
+
return "❌ Rename not valid at this position"
|
|
578
|
+
|
|
579
|
+
except Exception as e:
|
|
580
|
+
logger.error(f"LSP prepare rename failed: {e}")
|
|
581
|
+
return f"Prepare rename failed: {e}"
|
|
582
|
+
|
|
583
|
+
# Fallback
|
|
347
584
|
path = Path(file_path)
|
|
348
585
|
if not path.exists():
|
|
349
586
|
return f"Error: File not found: {file_path}"
|
|
350
|
-
|
|
351
|
-
lang = _get_language_for_file(file_path)
|
|
352
|
-
|
|
587
|
+
|
|
353
588
|
try:
|
|
354
589
|
if lang == "python":
|
|
355
590
|
result = subprocess.run(
|
|
356
591
|
[
|
|
357
|
-
"python",
|
|
592
|
+
"python",
|
|
593
|
+
"-c",
|
|
358
594
|
f"""
|
|
359
595
|
import jedi
|
|
360
596
|
script = jedi.Script(path='{file_path}')
|
|
@@ -366,52 +602,97 @@ if refs:
|
|
|
366
602
|
logger.info("✅ Rename is valid")
|
|
367
603
|
else:
|
|
368
604
|
logger.info("❌ No symbol found at position")
|
|
369
|
-
"""
|
|
605
|
+
""",
|
|
370
606
|
],
|
|
371
607
|
capture_output=True,
|
|
372
608
|
text=True,
|
|
373
609
|
timeout=10,
|
|
374
610
|
)
|
|
375
611
|
return result.stdout.strip() or "No symbol found at position"
|
|
376
|
-
|
|
612
|
+
|
|
377
613
|
else:
|
|
378
614
|
return f"Prepare rename not available for language: {lang}"
|
|
379
|
-
|
|
615
|
+
|
|
380
616
|
except Exception as e:
|
|
381
617
|
return f"Error: {str(e)}"
|
|
382
618
|
|
|
383
619
|
|
|
384
620
|
async def lsp_rename(
|
|
385
|
-
file_path: str,
|
|
386
|
-
line: int,
|
|
387
|
-
character: int,
|
|
388
|
-
new_name: str,
|
|
389
|
-
dry_run: bool = True
|
|
621
|
+
file_path: str, line: int, character: int, new_name: str, dry_run: bool = True
|
|
390
622
|
) -> str:
|
|
391
623
|
"""
|
|
392
624
|
Rename a symbol across the workspace.
|
|
393
|
-
|
|
394
|
-
Args:
|
|
395
|
-
file_path: Absolute path to the file
|
|
396
|
-
line: Line number (1-indexed)
|
|
397
|
-
character: Character position (0-indexed)
|
|
398
|
-
new_name: New name for the symbol
|
|
399
|
-
dry_run: If True, only show what would be changed
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
List of changes that would be made (or were made if not dry_run).
|
|
403
625
|
"""
|
|
626
|
+
# USER-VISIBLE NOTIFICATION
|
|
627
|
+
mode = "dry-run" if dry_run else "APPLY"
|
|
628
|
+
print(f"✏️ LSP-RENAME: {file_path}:{line}:{character} → '{new_name}' [{mode}]", file=sys.stderr)
|
|
629
|
+
|
|
630
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
631
|
+
|
|
632
|
+
if client:
|
|
633
|
+
try:
|
|
634
|
+
params = RenameParams(
|
|
635
|
+
text_document=TextDocumentIdentifier(uri=uri),
|
|
636
|
+
position=Position(line=line - 1, character=character),
|
|
637
|
+
new_name=new_name,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
response = await asyncio.wait_for(
|
|
641
|
+
client.protocol.send_request_async("textDocument/rename", params), timeout=10.0
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
if response and response.changes:
|
|
645
|
+
# WorkspaceEdit
|
|
646
|
+
changes_summary = []
|
|
647
|
+
for file_uri, edits in response.changes.items():
|
|
648
|
+
parsed = urlparse(file_uri)
|
|
649
|
+
path_str = unquote(parsed.path)
|
|
650
|
+
changes_summary.append(f"File: {path_str}")
|
|
651
|
+
for edit in edits:
|
|
652
|
+
changes_summary.append(
|
|
653
|
+
f" Line {edit.range.start.line + 1}: {edit.new_text}"
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
output = "\n".join(changes_summary)
|
|
657
|
+
|
|
658
|
+
if dry_run:
|
|
659
|
+
return f"**Would rename to '{new_name}':**\n{output}"
|
|
660
|
+
else:
|
|
661
|
+
# Apply changes
|
|
662
|
+
# Since we are an MCP tool, we should ideally use the Edit tool or similar.
|
|
663
|
+
# But the 'Apply' contract implies we do it.
|
|
664
|
+
# We have file paths and edits. We should apply them.
|
|
665
|
+
# Implementation detail: Applying edits to files is complex to do robustly here without the Edit tool.
|
|
666
|
+
# However, since this tool is rewriting 'lsp_rename', we must support applying.
|
|
667
|
+
# But 'tools.py' previously used `jedi.refactoring.apply()`.
|
|
668
|
+
|
|
669
|
+
# For now, we'll return the diff and instruction to use Edit, OR implement a basic applier.
|
|
670
|
+
# Given the instruction "Rewrite ... to use the persistent client", implying functionality parity.
|
|
671
|
+
# Applying edits from LSP response requires careful handling.
|
|
672
|
+
|
|
673
|
+
# Let's try to apply if not dry_run
|
|
674
|
+
try:
|
|
675
|
+
_apply_workspace_edit(response.changes)
|
|
676
|
+
return f"✅ Renamed to '{new_name}'. Modified files:\n{output}"
|
|
677
|
+
except Exception as e:
|
|
678
|
+
return f"Failed to apply edits: {e}\nDiff:\n{output}"
|
|
679
|
+
|
|
680
|
+
return "No changes returned from server"
|
|
681
|
+
|
|
682
|
+
except Exception as e:
|
|
683
|
+
logger.error(f"LSP rename failed: {e}")
|
|
684
|
+
|
|
685
|
+
# Fallback
|
|
404
686
|
path = Path(file_path)
|
|
405
687
|
if not path.exists():
|
|
406
688
|
return f"Error: File not found: {file_path}"
|
|
407
|
-
|
|
408
|
-
lang = _get_language_for_file(file_path)
|
|
409
|
-
|
|
689
|
+
|
|
410
690
|
try:
|
|
411
691
|
if lang == "python":
|
|
412
692
|
result = subprocess.run(
|
|
413
693
|
[
|
|
414
|
-
"python",
|
|
694
|
+
"python",
|
|
695
|
+
"-c",
|
|
415
696
|
f"""
|
|
416
697
|
import jedi
|
|
417
698
|
script = jedi.Script(path='{file_path}')
|
|
@@ -420,7 +701,7 @@ for path, changed in refactoring.get_changed_files().items():
|
|
|
420
701
|
logger.info(f"File: {{path}}")
|
|
421
702
|
logger.info(changed[:500])
|
|
422
703
|
logger.info("---")
|
|
423
|
-
"""
|
|
704
|
+
""",
|
|
424
705
|
],
|
|
425
706
|
capture_output=True,
|
|
426
707
|
text=True,
|
|
@@ -428,37 +709,103 @@ for path, changed in refactoring.get_changed_files().items():
|
|
|
428
709
|
)
|
|
429
710
|
output = result.stdout.strip()
|
|
430
711
|
if output and not dry_run:
|
|
431
|
-
# Apply changes
|
|
712
|
+
# Apply changes - Jedi handles this? No, get_changed_files returns the content.
|
|
432
713
|
return f"**Dry run** (set dry_run=False to apply):\n{output}"
|
|
433
714
|
elif output:
|
|
434
715
|
return f"**Would rename to '{new_name}':**\n{output}"
|
|
435
716
|
return "No changes needed"
|
|
436
|
-
|
|
717
|
+
|
|
437
718
|
else:
|
|
438
719
|
return f"Rename not available for language: {lang}. Use IDE refactoring."
|
|
439
|
-
|
|
720
|
+
|
|
440
721
|
except Exception as e:
|
|
441
722
|
return f"Error: {str(e)}"
|
|
442
723
|
|
|
443
724
|
|
|
725
|
+
def _apply_workspace_edit(changes: Dict[str, List[Any]]):
|
|
726
|
+
"""Apply LSP changes to files."""
|
|
727
|
+
for file_uri, edits in changes.items():
|
|
728
|
+
parsed = urlparse(file_uri)
|
|
729
|
+
path = Path(unquote(parsed.path))
|
|
730
|
+
if not path.exists():
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
content = path.read_text().splitlines(keepends=True)
|
|
734
|
+
# Apply edits in reverse order to preserve offsets
|
|
735
|
+
# Note: robust application requires handling multiple edits on same line, etc.
|
|
736
|
+
# This is a simplified version.
|
|
737
|
+
|
|
738
|
+
# Sort edits by start position descending
|
|
739
|
+
edits.sort(key=lambda e: (e.range.start.line, e.range.start.character), reverse=True)
|
|
740
|
+
|
|
741
|
+
for edit in edits:
|
|
742
|
+
start_line = edit.range.start.line
|
|
743
|
+
start_char = edit.range.start.character
|
|
744
|
+
end_line = edit.range.end.line
|
|
745
|
+
end_char = edit.range.end.character
|
|
746
|
+
new_text = edit.new_text
|
|
747
|
+
|
|
748
|
+
# This is tricky with splitlines.
|
|
749
|
+
# Convert to single string, patch, then split back?
|
|
750
|
+
# Or assume non-overlapping simple edits.
|
|
751
|
+
|
|
752
|
+
if start_line == end_line:
|
|
753
|
+
line_content = content[start_line]
|
|
754
|
+
content[start_line] = line_content[:start_char] + new_text + line_content[end_char:]
|
|
755
|
+
else:
|
|
756
|
+
# Multi-line edit - complex
|
|
757
|
+
# For safety, raise error for complex edits
|
|
758
|
+
raise NotImplementedError(
|
|
759
|
+
"Complex multi-line edits not safe to apply automatically yet."
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
# Write back
|
|
763
|
+
path.write_text("".join(content))
|
|
764
|
+
|
|
765
|
+
|
|
444
766
|
async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
445
767
|
"""
|
|
446
768
|
Get available quick fixes and refactorings at a position.
|
|
447
|
-
|
|
448
|
-
Args:
|
|
449
|
-
file_path: Absolute path to the file
|
|
450
|
-
line: Line number (1-indexed)
|
|
451
|
-
character: Character position (0-indexed)
|
|
452
|
-
|
|
453
|
-
Returns:
|
|
454
|
-
List of available code actions.
|
|
455
769
|
"""
|
|
770
|
+
# USER-VISIBLE NOTIFICATION
|
|
771
|
+
print(f"💡 LSP-ACTIONS: {file_path}:{line}:{character}", file=sys.stderr)
|
|
772
|
+
|
|
773
|
+
client, uri, lang = await _get_client_and_params(file_path)
|
|
774
|
+
|
|
775
|
+
if client:
|
|
776
|
+
try:
|
|
777
|
+
params = CodeActionParams(
|
|
778
|
+
text_document=TextDocumentIdentifier(uri=uri),
|
|
779
|
+
range=Range(
|
|
780
|
+
start=Position(line=line - 1, character=character),
|
|
781
|
+
end=Position(line=line - 1, character=character),
|
|
782
|
+
),
|
|
783
|
+
context=CodeActionContext(
|
|
784
|
+
diagnostics=[]
|
|
785
|
+
), # We should ideally provide diagnostics here
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
response = await asyncio.wait_for(
|
|
789
|
+
client.protocol.send_request_async("textDocument/codeAction", params), timeout=5.0
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
if response:
|
|
793
|
+
actions = []
|
|
794
|
+
for action in response:
|
|
795
|
+
title = action.title
|
|
796
|
+
kind = action.kind
|
|
797
|
+
actions.append(f"- {title} ({kind})")
|
|
798
|
+
return "**Available code actions:**\n" + "\n".join(actions)
|
|
799
|
+
return "No code actions available at this position"
|
|
800
|
+
|
|
801
|
+
except Exception as e:
|
|
802
|
+
logger.error(f"LSP code actions failed: {e}")
|
|
803
|
+
|
|
804
|
+
# Fallback
|
|
456
805
|
path = Path(file_path)
|
|
457
806
|
if not path.exists():
|
|
458
807
|
return f"Error: File not found: {file_path}"
|
|
459
|
-
|
|
460
|
-
lang = _get_language_for_file(file_path)
|
|
461
|
-
|
|
808
|
+
|
|
462
809
|
try:
|
|
463
810
|
if lang == "python":
|
|
464
811
|
# Use ruff to suggest fixes
|
|
@@ -468,7 +815,7 @@ async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
|
468
815
|
text=True,
|
|
469
816
|
timeout=10,
|
|
470
817
|
)
|
|
471
|
-
|
|
818
|
+
|
|
472
819
|
try:
|
|
473
820
|
diagnostics = json.loads(result.stdout)
|
|
474
821
|
actions = []
|
|
@@ -481,40 +828,145 @@ async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
|
481
828
|
actions.append(f"- [{code}] {msg} (auto-fix available)")
|
|
482
829
|
else:
|
|
483
830
|
actions.append(f"- [{code}] {msg}")
|
|
484
|
-
|
|
831
|
+
|
|
485
832
|
if actions:
|
|
486
833
|
return "**Available code actions:**\n" + "\n".join(actions)
|
|
487
834
|
return "No code actions available at this position"
|
|
488
|
-
|
|
835
|
+
|
|
489
836
|
except json.JSONDecodeError:
|
|
490
837
|
return "No code actions available"
|
|
491
|
-
|
|
838
|
+
|
|
492
839
|
else:
|
|
493
840
|
return f"Code actions not available for language: {lang}"
|
|
494
|
-
|
|
841
|
+
|
|
495
842
|
except FileNotFoundError:
|
|
496
843
|
return "Install ruff for Python code actions: pip install ruff"
|
|
497
844
|
except Exception as e:
|
|
498
845
|
return f"Error: {str(e)}"
|
|
499
846
|
|
|
500
847
|
|
|
848
|
+
async def lsp_code_action_resolve(file_path: str, action_code: str, line: int = None) -> str:
|
|
849
|
+
"""
|
|
850
|
+
Apply a specific code action/fix to a file.
|
|
851
|
+
"""
|
|
852
|
+
# USER-VISIBLE NOTIFICATION
|
|
853
|
+
print(f"🔧 LSP-RESOLVE: {action_code} at {file_path}", file=sys.stderr)
|
|
854
|
+
|
|
855
|
+
# Implementing via LSP requires 'codeAction/resolve' which is complex.
|
|
856
|
+
# We stick to Ruff fallback for now as it's more direct for Python "fixes".
|
|
857
|
+
# Unless we want to use the persistent client to trigger the action.
|
|
858
|
+
# Most LSP servers return the Edit in the CodeAction response, so resolve might not be needed if we cache the actions.
|
|
859
|
+
# But since this is a stateless call, we can't easily resolve a previous action.
|
|
860
|
+
|
|
861
|
+
# We'll default to the existing robust Ruff implementation for Python.
|
|
862
|
+
|
|
863
|
+
path = Path(file_path)
|
|
864
|
+
if not path.exists():
|
|
865
|
+
return f"Error: File not found: {file_path}"
|
|
866
|
+
|
|
867
|
+
lang = _get_language_for_file(file_path)
|
|
868
|
+
|
|
869
|
+
if lang == "python":
|
|
870
|
+
try:
|
|
871
|
+
result = subprocess.run(
|
|
872
|
+
["ruff", "check", str(path), "--fix", "--select", action_code],
|
|
873
|
+
capture_output=True,
|
|
874
|
+
text=True,
|
|
875
|
+
timeout=15,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
if result.returncode == 0:
|
|
879
|
+
return f"✅ Applied fix [{action_code}] to {path.name}"
|
|
880
|
+
else:
|
|
881
|
+
stderr = result.stderr.strip()
|
|
882
|
+
if stderr:
|
|
883
|
+
return f"⚠️ {stderr}"
|
|
884
|
+
return f"No changes needed for action [{action_code}]"
|
|
885
|
+
|
|
886
|
+
except FileNotFoundError:
|
|
887
|
+
return "Install ruff: pip install ruff"
|
|
888
|
+
except subprocess.TimeoutExpired:
|
|
889
|
+
return "Timeout applying fix"
|
|
890
|
+
except Exception as e:
|
|
891
|
+
return f"Error: {str(e)}"
|
|
892
|
+
|
|
893
|
+
return f"Code action resolve not implemented for language: {lang}"
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
async def lsp_extract_refactor(
|
|
897
|
+
file_path: str,
|
|
898
|
+
start_line: int,
|
|
899
|
+
start_char: int,
|
|
900
|
+
end_line: int,
|
|
901
|
+
end_char: int,
|
|
902
|
+
new_name: str,
|
|
903
|
+
kind: str = "function",
|
|
904
|
+
) -> str:
|
|
905
|
+
"""
|
|
906
|
+
Extract code to a function or variable.
|
|
907
|
+
"""
|
|
908
|
+
# USER-VISIBLE NOTIFICATION
|
|
909
|
+
print(
|
|
910
|
+
f"🔧 LSP-EXTRACT: {kind} '{new_name}' from {file_path}:{start_line}-{end_line}",
|
|
911
|
+
file=sys.stderr,
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
# This is not a standard LSP method, though some servers support it via CodeActions or commands.
|
|
915
|
+
# Jedi natively supports it via library, so we keep the fallback.
|
|
916
|
+
# CodeAction might return 'refactor.extract'.
|
|
917
|
+
|
|
918
|
+
path = Path(file_path)
|
|
919
|
+
if not path.exists():
|
|
920
|
+
return f"Error: File not found: {file_path}"
|
|
921
|
+
|
|
922
|
+
lang = _get_language_for_file(file_path)
|
|
923
|
+
|
|
924
|
+
if lang == "python":
|
|
925
|
+
try:
|
|
926
|
+
import jedi
|
|
927
|
+
|
|
928
|
+
source = path.read_text()
|
|
929
|
+
script = jedi.Script(source, path=path)
|
|
930
|
+
|
|
931
|
+
if kind == "function":
|
|
932
|
+
refactoring = script.extract_function(
|
|
933
|
+
line=start_line, until_line=end_line, new_name=new_name
|
|
934
|
+
)
|
|
935
|
+
else: # variable
|
|
936
|
+
refactoring = script.extract_variable(
|
|
937
|
+
line=start_line, until_line=end_line, new_name=new_name
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
# Get the diff
|
|
941
|
+
changes = refactoring.get_diff()
|
|
942
|
+
return f"✅ Extract {kind} preview:\n```diff\n{changes}\n```\n\nTo apply: use Edit tool with the changes above"
|
|
943
|
+
|
|
944
|
+
except AttributeError:
|
|
945
|
+
return "Jedi version doesn't support extract refactoring. Upgrade: pip install -U jedi"
|
|
946
|
+
except Exception as e:
|
|
947
|
+
return f"Extract failed: {str(e)}"
|
|
948
|
+
|
|
949
|
+
return f"Extract refactoring not implemented for language: {lang}"
|
|
950
|
+
|
|
951
|
+
|
|
501
952
|
async def lsp_servers() -> str:
|
|
502
953
|
"""
|
|
503
954
|
List available LSP servers and their installation status.
|
|
504
|
-
|
|
505
|
-
Returns:
|
|
506
|
-
Table of available language servers.
|
|
507
955
|
"""
|
|
956
|
+
# USER-VISIBLE NOTIFICATION
|
|
957
|
+
print("🖥️ LSP-SERVERS: listing installed servers", file=sys.stderr)
|
|
958
|
+
|
|
508
959
|
servers = [
|
|
509
960
|
("python", "jedi", "pip install jedi"),
|
|
961
|
+
("python", "jedi-language-server", "pip install jedi-language-server"),
|
|
510
962
|
("python", "ruff", "pip install ruff"),
|
|
511
963
|
("typescript", "typescript-language-server", "npm i -g typescript-language-server"),
|
|
512
964
|
("go", "gopls", "go install golang.org/x/tools/gopls@latest"),
|
|
513
965
|
("rust", "rust-analyzer", "rustup component add rust-analyzer"),
|
|
514
966
|
]
|
|
515
|
-
|
|
967
|
+
|
|
516
968
|
lines = ["| Language | Server | Status | Install |", "|----------|--------|--------|---------|"]
|
|
517
|
-
|
|
969
|
+
|
|
518
970
|
for lang, server, install in servers:
|
|
519
971
|
# Check if installed
|
|
520
972
|
try:
|
|
@@ -524,7 +976,38 @@ async def lsp_servers() -> str:
|
|
|
524
976
|
status = "❌ Not installed"
|
|
525
977
|
except Exception:
|
|
526
978
|
status = "⚠️ Unknown"
|
|
527
|
-
|
|
979
|
+
|
|
528
980
|
lines.append(f"| {lang} | {server} | {status} | `{install}` |")
|
|
529
|
-
|
|
981
|
+
|
|
982
|
+
return "\n".join(lines)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
async def lsp_health() -> str:
|
|
986
|
+
"""
|
|
987
|
+
Check health of persistent LSP servers.
|
|
988
|
+
"""
|
|
989
|
+
manager = get_lsp_manager()
|
|
990
|
+
status = manager.get_status()
|
|
991
|
+
|
|
992
|
+
if not status:
|
|
993
|
+
return "No LSP servers configured"
|
|
994
|
+
|
|
995
|
+
lines = [
|
|
996
|
+
"**LSP Server Health:**",
|
|
997
|
+
"| Language | Status | PID | Restarts | Command |",
|
|
998
|
+
"|---|---|---|---|---|",
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
for lang, info in status.items():
|
|
1002
|
+
state = "✅ Running" if info["running"] else "❌ Stopped"
|
|
1003
|
+
pid = info["pid"] or "-"
|
|
1004
|
+
restarts = info["restarts"]
|
|
1005
|
+
cmd = info["command"]
|
|
1006
|
+
|
|
1007
|
+
# Truncate command if too long
|
|
1008
|
+
if len(cmd) > 30:
|
|
1009
|
+
cmd = cmd[:27] + "..."
|
|
1010
|
+
|
|
1011
|
+
lines.append(f"| {lang} | {state} | {pid} | {restarts} | `{cmd}` |")
|
|
1012
|
+
|
|
530
1013
|
return "\n".join(lines)
|