stravinsky 0.2.40__py3-none-any.whl → 0.3.4__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.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/token_refresh.py +130 -0
- mcp_bridge/cli/__init__.py +6 -0
- mcp_bridge/cli/install_hooks.py +1265 -0
- mcp_bridge/cli/session_report.py +585 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
- mcp_bridge/hooks/README.md +215 -0
- mcp_bridge/hooks/__init__.py +119 -43
- mcp_bridge/hooks/edit_recovery.py +42 -37
- mcp_bridge/hooks/git_noninteractive.py +89 -0
- mcp_bridge/hooks/keyword_detector.py +30 -0
- mcp_bridge/hooks/manager.py +50 -0
- mcp_bridge/hooks/notification_hook.py +103 -0
- mcp_bridge/hooks/parallel_enforcer.py +127 -0
- mcp_bridge/hooks/parallel_execution.py +111 -0
- mcp_bridge/hooks/pre_compact.py +123 -0
- mcp_bridge/hooks/preemptive_compaction.py +81 -7
- mcp_bridge/hooks/rules_injector.py +507 -0
- mcp_bridge/hooks/session_idle.py +116 -0
- mcp_bridge/hooks/session_notifier.py +125 -0
- mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
- mcp_bridge/hooks/subagent_stop.py +98 -0
- mcp_bridge/hooks/task_validator.py +73 -0
- mcp_bridge/hooks/tmux_manager.py +141 -0
- mcp_bridge/hooks/todo_continuation.py +90 -0
- mcp_bridge/hooks/todo_delegation.py +88 -0
- mcp_bridge/hooks/tool_messaging.py +164 -0
- mcp_bridge/hooks/truncator.py +21 -17
- mcp_bridge/notifications.py +151 -0
- mcp_bridge/prompts/__init__.py +3 -1
- mcp_bridge/prompts/dewey.py +30 -20
- mcp_bridge/prompts/explore.py +46 -8
- mcp_bridge/prompts/multimodal.py +24 -3
- mcp_bridge/prompts/planner.py +222 -0
- mcp_bridge/prompts/stravinsky.py +107 -28
- mcp_bridge/server.py +170 -10
- mcp_bridge/server_tools.py +554 -32
- mcp_bridge/tools/agent_manager.py +316 -106
- mcp_bridge/tools/background_tasks.py +2 -1
- mcp_bridge/tools/code_search.py +97 -11
- mcp_bridge/tools/lsp/__init__.py +7 -0
- mcp_bridge/tools/lsp/manager.py +448 -0
- mcp_bridge/tools/lsp/tools.py +637 -150
- mcp_bridge/tools/model_invoke.py +270 -47
- mcp_bridge/tools/semantic_search.py +2492 -0
- mcp_bridge/tools/templates.py +32 -18
- stravinsky-0.3.4.dist-info/METADATA +420 -0
- stravinsky-0.3.4.dist-info/RECORD +79 -0
- stravinsky-0.3.4.dist-info/entry_points.txt +5 -0
- mcp_bridge/native_hooks/edit_recovery.py +0 -46
- mcp_bridge/native_hooks/truncator.py +0 -23
- stravinsky-0.2.40.dist-info/METADATA +0 -204
- stravinsky-0.2.40.dist-info/RECORD +0 -57
- stravinsky-0.2.40.dist-info/entry_points.txt +0 -3
- /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
- {stravinsky-0.2.40.dist-info → stravinsky-0.3.4.dist-info}/WHEEL +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,38 +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
|
"""
|
|
112
|
+
# USER-VISIBLE NOTIFICATION
|
|
113
|
+
print(f"📍 LSP-HOVER: {file_path}:{line}:{character}", file=sys.stderr)
|
|
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
|
|
60
144
|
path = Path(file_path)
|
|
61
145
|
if not path.exists():
|
|
62
146
|
return f"Error: File not found: {file_path}"
|
|
63
|
-
|
|
64
|
-
lang = _get_language_for_file(file_path)
|
|
65
|
-
|
|
147
|
+
|
|
66
148
|
try:
|
|
67
149
|
if lang == "python":
|
|
68
150
|
# Use jedi for Python hover info
|
|
69
151
|
result = subprocess.run(
|
|
70
152
|
[
|
|
71
|
-
"python",
|
|
153
|
+
"python",
|
|
154
|
+
"-c",
|
|
72
155
|
f"""
|
|
73
156
|
import jedi
|
|
74
157
|
script = jedi.Script(path='{file_path}')
|
|
@@ -78,7 +161,7 @@ for c in completions[:1]:
|
|
|
78
161
|
logger.info(f"Name: {{c.full_name}}")
|
|
79
162
|
if c.docstring():
|
|
80
163
|
logger.info(f"\\nDocstring:\\n{{c.docstring()[:500]}}")
|
|
81
|
-
"""
|
|
164
|
+
""",
|
|
82
165
|
],
|
|
83
166
|
capture_output=True,
|
|
84
167
|
text=True,
|
|
@@ -88,15 +171,13 @@ for c in completions[:1]:
|
|
|
88
171
|
if output:
|
|
89
172
|
return output
|
|
90
173
|
return f"No hover info at line {line}, character {character}"
|
|
91
|
-
|
|
174
|
+
|
|
92
175
|
elif lang in ("typescript", "javascript", "typescriptreact", "javascriptreact"):
|
|
93
|
-
# Use tsserver via quick-info
|
|
94
|
-
# For simplicity, fall back to message
|
|
95
176
|
return f"TypeScript hover requires running language server. Use Claude Code's native hover."
|
|
96
|
-
|
|
177
|
+
|
|
97
178
|
else:
|
|
98
179
|
return f"Hover not available for language: {lang}"
|
|
99
|
-
|
|
180
|
+
|
|
100
181
|
except FileNotFoundError as e:
|
|
101
182
|
return f"Tool not found: {e.filename}. Install jedi: pip install jedi"
|
|
102
183
|
except subprocess.TimeoutExpired:
|
|
@@ -108,33 +189,68 @@ for c in completions[:1]:
|
|
|
108
189
|
async def lsp_goto_definition(file_path: str, line: int, character: int) -> str:
|
|
109
190
|
"""
|
|
110
191
|
Find where a symbol is defined.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
file_path: Absolute path to the file
|
|
114
|
-
line: Line number (1-indexed)
|
|
115
|
-
character: Character position (0-indexed)
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
Location(s) where the symbol is defined.
|
|
119
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)
|
|
120
237
|
path = Path(file_path)
|
|
121
238
|
if not path.exists():
|
|
122
239
|
return f"Error: File not found: {file_path}"
|
|
123
|
-
|
|
124
|
-
lang = _get_language_for_file(file_path)
|
|
125
|
-
|
|
240
|
+
|
|
126
241
|
try:
|
|
127
242
|
if lang == "python":
|
|
128
243
|
result = subprocess.run(
|
|
129
244
|
[
|
|
130
|
-
"python",
|
|
245
|
+
"python",
|
|
246
|
+
"-c",
|
|
131
247
|
f"""
|
|
132
248
|
import jedi
|
|
133
249
|
script = jedi.Script(path='{file_path}')
|
|
134
250
|
definitions = script.goto({line}, {character})
|
|
135
251
|
for d in definitions:
|
|
136
252
|
logger.info(f"{{d.module_path}}:{{d.line}}:{{d.column}} - {{d.full_name}}")
|
|
137
|
-
"""
|
|
253
|
+
""",
|
|
138
254
|
],
|
|
139
255
|
capture_output=True,
|
|
140
256
|
text=True,
|
|
@@ -144,13 +260,13 @@ for d in definitions:
|
|
|
144
260
|
if output:
|
|
145
261
|
return output
|
|
146
262
|
return "No definition found"
|
|
147
|
-
|
|
263
|
+
|
|
148
264
|
elif lang in ("typescript", "javascript"):
|
|
149
265
|
return "TypeScript goto definition requires running language server. Use Claude Code's native navigation."
|
|
150
|
-
|
|
266
|
+
|
|
151
267
|
else:
|
|
152
268
|
return f"Goto definition not available for language: {lang}"
|
|
153
|
-
|
|
269
|
+
|
|
154
270
|
except FileNotFoundError as e:
|
|
155
271
|
return f"Tool not found: Install jedi: pip install jedi"
|
|
156
272
|
except subprocess.TimeoutExpired:
|
|
@@ -160,34 +276,62 @@ for d in definitions:
|
|
|
160
276
|
|
|
161
277
|
|
|
162
278
|
async def lsp_find_references(
|
|
163
|
-
file_path: str,
|
|
164
|
-
line: int,
|
|
165
|
-
character: int,
|
|
166
|
-
include_declaration: bool = True
|
|
279
|
+
file_path: str, line: int, character: int, include_declaration: bool = True
|
|
167
280
|
) -> str:
|
|
168
281
|
"""
|
|
169
282
|
Find all references to a symbol across the workspace.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
file_path: Absolute path to the file
|
|
173
|
-
line: Line number (1-indexed)
|
|
174
|
-
character: Character position (0-indexed)
|
|
175
|
-
include_declaration: Include the declaration itself
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
All locations where the symbol is used.
|
|
179
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...
|
|
180
325
|
path = Path(file_path)
|
|
181
326
|
if not path.exists():
|
|
182
327
|
return f"Error: File not found: {file_path}"
|
|
183
|
-
|
|
184
|
-
lang = _get_language_for_file(file_path)
|
|
185
|
-
|
|
328
|
+
|
|
186
329
|
try:
|
|
187
330
|
if lang == "python":
|
|
188
331
|
result = subprocess.run(
|
|
189
332
|
[
|
|
190
|
-
"python",
|
|
333
|
+
"python",
|
|
334
|
+
"-c",
|
|
191
335
|
f"""
|
|
192
336
|
import jedi
|
|
193
337
|
script = jedi.Script(path='{file_path}')
|
|
@@ -196,7 +340,7 @@ for r in references[:30]:
|
|
|
196
340
|
logger.info(f"{{r.module_path}}:{{r.line}}:{{r.column}}")
|
|
197
341
|
if len(references) > 30:
|
|
198
342
|
logger.info(f"... and {{len(references) - 30}} more")
|
|
199
|
-
"""
|
|
343
|
+
""",
|
|
200
344
|
],
|
|
201
345
|
capture_output=True,
|
|
202
346
|
text=True,
|
|
@@ -206,10 +350,10 @@ if len(references) > 30:
|
|
|
206
350
|
if output:
|
|
207
351
|
return output
|
|
208
352
|
return "No references found"
|
|
209
|
-
|
|
353
|
+
|
|
210
354
|
else:
|
|
211
355
|
return f"Find references not available for language: {lang}"
|
|
212
|
-
|
|
356
|
+
|
|
213
357
|
except subprocess.TimeoutExpired:
|
|
214
358
|
return "Reference search timed out"
|
|
215
359
|
except Exception as e:
|
|
@@ -219,24 +363,72 @@ if len(references) > 30:
|
|
|
219
363
|
async def lsp_document_symbols(file_path: str) -> str:
|
|
220
364
|
"""
|
|
221
365
|
Get hierarchical outline of all symbols in a file.
|
|
222
|
-
|
|
223
|
-
Args:
|
|
224
|
-
file_path: Absolute path to the file
|
|
225
|
-
|
|
226
|
-
Returns:
|
|
227
|
-
Structured list of functions, classes, methods in the file.
|
|
228
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...
|
|
229
422
|
path = Path(file_path)
|
|
230
423
|
if not path.exists():
|
|
231
424
|
return f"Error: File not found: {file_path}"
|
|
232
|
-
|
|
233
|
-
lang = _get_language_for_file(file_path)
|
|
234
|
-
|
|
425
|
+
|
|
235
426
|
try:
|
|
236
427
|
if lang == "python":
|
|
237
428
|
result = subprocess.run(
|
|
238
429
|
[
|
|
239
|
-
"python",
|
|
430
|
+
"python",
|
|
431
|
+
"-c",
|
|
240
432
|
f"""
|
|
241
433
|
import jedi
|
|
242
434
|
script = jedi.Script(path='{file_path}')
|
|
@@ -244,7 +436,7 @@ names = script.get_names(all_scopes=True, definitions=True)
|
|
|
244
436
|
for n in names:
|
|
245
437
|
indent = " " * (n.get_line_code().count(" ") if n.get_line_code() else 0)
|
|
246
438
|
logger.info(f"{{n.line:4d}} | {{indent}}{{n.type:10}} {{n.name}}")
|
|
247
|
-
"""
|
|
439
|
+
""",
|
|
248
440
|
],
|
|
249
441
|
capture_output=True,
|
|
250
442
|
text=True,
|
|
@@ -254,7 +446,7 @@ for n in names:
|
|
|
254
446
|
if output:
|
|
255
447
|
return f"**Symbols in {path.name}:**\n```\nLine | Symbol\n{output}\n```"
|
|
256
448
|
return "No symbols found"
|
|
257
|
-
|
|
449
|
+
|
|
258
450
|
else:
|
|
259
451
|
# Fallback: use ctags
|
|
260
452
|
result = subprocess.run(
|
|
@@ -267,7 +459,7 @@ for n in names:
|
|
|
267
459
|
if output:
|
|
268
460
|
return f"**Symbols in {path.name}:**\n```\n{output}\n```"
|
|
269
461
|
return "No symbols found"
|
|
270
|
-
|
|
462
|
+
|
|
271
463
|
except FileNotFoundError:
|
|
272
464
|
return "Install jedi (pip install jedi) or ctags for symbol lookup"
|
|
273
465
|
except subprocess.TimeoutExpired:
|
|
@@ -279,14 +471,40 @@ for n in names:
|
|
|
279
471
|
async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
280
472
|
"""
|
|
281
473
|
Search for symbols by name across the entire workspace.
|
|
282
|
-
|
|
283
|
-
Args:
|
|
284
|
-
query: Symbol name to search for (fuzzy match)
|
|
285
|
-
directory: Workspace directory
|
|
286
|
-
|
|
287
|
-
Returns:
|
|
288
|
-
Matching symbols with their locations.
|
|
289
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
|
|
290
508
|
try:
|
|
291
509
|
# Use ctags to index and grep for symbols
|
|
292
510
|
result = subprocess.run(
|
|
@@ -295,12 +513,12 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
295
513
|
text=True,
|
|
296
514
|
timeout=15,
|
|
297
515
|
)
|
|
298
|
-
|
|
516
|
+
|
|
299
517
|
files = result.stdout.strip().split("\n")[:10] # Limit files
|
|
300
|
-
|
|
518
|
+
|
|
301
519
|
if not files or files == [""]:
|
|
302
520
|
return "No matching files found"
|
|
303
|
-
|
|
521
|
+
|
|
304
522
|
symbols = []
|
|
305
523
|
for f in files:
|
|
306
524
|
if not f:
|
|
@@ -315,11 +533,11 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
315
533
|
for line in ctags_result.stdout.split("\n"):
|
|
316
534
|
if query.lower() in line.lower():
|
|
317
535
|
symbols.append(line)
|
|
318
|
-
|
|
536
|
+
|
|
319
537
|
if symbols:
|
|
320
538
|
return "\n".join(symbols[:20])
|
|
321
539
|
return f"No symbols matching '{query}' found"
|
|
322
|
-
|
|
540
|
+
|
|
323
541
|
except FileNotFoundError:
|
|
324
542
|
return "Install ctags and ripgrep for workspace symbol search"
|
|
325
543
|
except subprocess.TimeoutExpired:
|
|
@@ -331,26 +549,48 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
331
549
|
async def lsp_prepare_rename(file_path: str, line: int, character: int) -> str:
|
|
332
550
|
"""
|
|
333
551
|
Check if a symbol at position can be renamed.
|
|
334
|
-
|
|
335
|
-
Args:
|
|
336
|
-
file_path: Absolute path to the file
|
|
337
|
-
line: Line number (1-indexed)
|
|
338
|
-
character: Character position (0-indexed)
|
|
339
|
-
|
|
340
|
-
Returns:
|
|
341
|
-
The symbol that would be renamed and validation status.
|
|
342
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
|
|
343
584
|
path = Path(file_path)
|
|
344
585
|
if not path.exists():
|
|
345
586
|
return f"Error: File not found: {file_path}"
|
|
346
|
-
|
|
347
|
-
lang = _get_language_for_file(file_path)
|
|
348
|
-
|
|
587
|
+
|
|
349
588
|
try:
|
|
350
589
|
if lang == "python":
|
|
351
590
|
result = subprocess.run(
|
|
352
591
|
[
|
|
353
|
-
"python",
|
|
592
|
+
"python",
|
|
593
|
+
"-c",
|
|
354
594
|
f"""
|
|
355
595
|
import jedi
|
|
356
596
|
script = jedi.Script(path='{file_path}')
|
|
@@ -362,52 +602,97 @@ if refs:
|
|
|
362
602
|
logger.info("✅ Rename is valid")
|
|
363
603
|
else:
|
|
364
604
|
logger.info("❌ No symbol found at position")
|
|
365
|
-
"""
|
|
605
|
+
""",
|
|
366
606
|
],
|
|
367
607
|
capture_output=True,
|
|
368
608
|
text=True,
|
|
369
609
|
timeout=10,
|
|
370
610
|
)
|
|
371
611
|
return result.stdout.strip() or "No symbol found at position"
|
|
372
|
-
|
|
612
|
+
|
|
373
613
|
else:
|
|
374
614
|
return f"Prepare rename not available for language: {lang}"
|
|
375
|
-
|
|
615
|
+
|
|
376
616
|
except Exception as e:
|
|
377
617
|
return f"Error: {str(e)}"
|
|
378
618
|
|
|
379
619
|
|
|
380
620
|
async def lsp_rename(
|
|
381
|
-
file_path: str,
|
|
382
|
-
line: int,
|
|
383
|
-
character: int,
|
|
384
|
-
new_name: str,
|
|
385
|
-
dry_run: bool = True
|
|
621
|
+
file_path: str, line: int, character: int, new_name: str, dry_run: bool = True
|
|
386
622
|
) -> str:
|
|
387
623
|
"""
|
|
388
624
|
Rename a symbol across the workspace.
|
|
389
|
-
|
|
390
|
-
Args:
|
|
391
|
-
file_path: Absolute path to the file
|
|
392
|
-
line: Line number (1-indexed)
|
|
393
|
-
character: Character position (0-indexed)
|
|
394
|
-
new_name: New name for the symbol
|
|
395
|
-
dry_run: If True, only show what would be changed
|
|
396
|
-
|
|
397
|
-
Returns:
|
|
398
|
-
List of changes that would be made (or were made if not dry_run).
|
|
399
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
|
|
400
686
|
path = Path(file_path)
|
|
401
687
|
if not path.exists():
|
|
402
688
|
return f"Error: File not found: {file_path}"
|
|
403
|
-
|
|
404
|
-
lang = _get_language_for_file(file_path)
|
|
405
|
-
|
|
689
|
+
|
|
406
690
|
try:
|
|
407
691
|
if lang == "python":
|
|
408
692
|
result = subprocess.run(
|
|
409
693
|
[
|
|
410
|
-
"python",
|
|
694
|
+
"python",
|
|
695
|
+
"-c",
|
|
411
696
|
f"""
|
|
412
697
|
import jedi
|
|
413
698
|
script = jedi.Script(path='{file_path}')
|
|
@@ -416,7 +701,7 @@ for path, changed in refactoring.get_changed_files().items():
|
|
|
416
701
|
logger.info(f"File: {{path}}")
|
|
417
702
|
logger.info(changed[:500])
|
|
418
703
|
logger.info("---")
|
|
419
|
-
"""
|
|
704
|
+
""",
|
|
420
705
|
],
|
|
421
706
|
capture_output=True,
|
|
422
707
|
text=True,
|
|
@@ -424,37 +709,103 @@ for path, changed in refactoring.get_changed_files().items():
|
|
|
424
709
|
)
|
|
425
710
|
output = result.stdout.strip()
|
|
426
711
|
if output and not dry_run:
|
|
427
|
-
# Apply changes
|
|
712
|
+
# Apply changes - Jedi handles this? No, get_changed_files returns the content.
|
|
428
713
|
return f"**Dry run** (set dry_run=False to apply):\n{output}"
|
|
429
714
|
elif output:
|
|
430
715
|
return f"**Would rename to '{new_name}':**\n{output}"
|
|
431
716
|
return "No changes needed"
|
|
432
|
-
|
|
717
|
+
|
|
433
718
|
else:
|
|
434
719
|
return f"Rename not available for language: {lang}. Use IDE refactoring."
|
|
435
|
-
|
|
720
|
+
|
|
436
721
|
except Exception as e:
|
|
437
722
|
return f"Error: {str(e)}"
|
|
438
723
|
|
|
439
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
|
+
|
|
440
766
|
async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
441
767
|
"""
|
|
442
768
|
Get available quick fixes and refactorings at a position.
|
|
443
|
-
|
|
444
|
-
Args:
|
|
445
|
-
file_path: Absolute path to the file
|
|
446
|
-
line: Line number (1-indexed)
|
|
447
|
-
character: Character position (0-indexed)
|
|
448
|
-
|
|
449
|
-
Returns:
|
|
450
|
-
List of available code actions.
|
|
451
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
|
|
452
805
|
path = Path(file_path)
|
|
453
806
|
if not path.exists():
|
|
454
807
|
return f"Error: File not found: {file_path}"
|
|
455
|
-
|
|
456
|
-
lang = _get_language_for_file(file_path)
|
|
457
|
-
|
|
808
|
+
|
|
458
809
|
try:
|
|
459
810
|
if lang == "python":
|
|
460
811
|
# Use ruff to suggest fixes
|
|
@@ -464,7 +815,7 @@ async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
|
464
815
|
text=True,
|
|
465
816
|
timeout=10,
|
|
466
817
|
)
|
|
467
|
-
|
|
818
|
+
|
|
468
819
|
try:
|
|
469
820
|
diagnostics = json.loads(result.stdout)
|
|
470
821
|
actions = []
|
|
@@ -477,40 +828,145 @@ async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
|
477
828
|
actions.append(f"- [{code}] {msg} (auto-fix available)")
|
|
478
829
|
else:
|
|
479
830
|
actions.append(f"- [{code}] {msg}")
|
|
480
|
-
|
|
831
|
+
|
|
481
832
|
if actions:
|
|
482
833
|
return "**Available code actions:**\n" + "\n".join(actions)
|
|
483
834
|
return "No code actions available at this position"
|
|
484
|
-
|
|
835
|
+
|
|
485
836
|
except json.JSONDecodeError:
|
|
486
837
|
return "No code actions available"
|
|
487
|
-
|
|
838
|
+
|
|
488
839
|
else:
|
|
489
840
|
return f"Code actions not available for language: {lang}"
|
|
490
|
-
|
|
841
|
+
|
|
491
842
|
except FileNotFoundError:
|
|
492
843
|
return "Install ruff for Python code actions: pip install ruff"
|
|
493
844
|
except Exception as e:
|
|
494
845
|
return f"Error: {str(e)}"
|
|
495
846
|
|
|
496
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
|
+
|
|
497
952
|
async def lsp_servers() -> str:
|
|
498
953
|
"""
|
|
499
954
|
List available LSP servers and their installation status.
|
|
500
|
-
|
|
501
|
-
Returns:
|
|
502
|
-
Table of available language servers.
|
|
503
955
|
"""
|
|
956
|
+
# USER-VISIBLE NOTIFICATION
|
|
957
|
+
print("🖥️ LSP-SERVERS: listing installed servers", file=sys.stderr)
|
|
958
|
+
|
|
504
959
|
servers = [
|
|
505
960
|
("python", "jedi", "pip install jedi"),
|
|
961
|
+
("python", "jedi-language-server", "pip install jedi-language-server"),
|
|
506
962
|
("python", "ruff", "pip install ruff"),
|
|
507
963
|
("typescript", "typescript-language-server", "npm i -g typescript-language-server"),
|
|
508
964
|
("go", "gopls", "go install golang.org/x/tools/gopls@latest"),
|
|
509
965
|
("rust", "rust-analyzer", "rustup component add rust-analyzer"),
|
|
510
966
|
]
|
|
511
|
-
|
|
967
|
+
|
|
512
968
|
lines = ["| Language | Server | Status | Install |", "|----------|--------|--------|---------|"]
|
|
513
|
-
|
|
969
|
+
|
|
514
970
|
for lang, server, install in servers:
|
|
515
971
|
# Check if installed
|
|
516
972
|
try:
|
|
@@ -520,7 +976,38 @@ async def lsp_servers() -> str:
|
|
|
520
976
|
status = "❌ Not installed"
|
|
521
977
|
except Exception:
|
|
522
978
|
status = "⚠️ Unknown"
|
|
523
|
-
|
|
979
|
+
|
|
524
980
|
lines.append(f"| {lang} | {server} | {status} | `{install}` |")
|
|
525
|
-
|
|
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
|
+
|
|
526
1013
|
return "\n".join(lines)
|