stravinsky 0.1.2__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.

@@ -0,0 +1,526 @@
1
+ """
2
+ LSP Tools - Advanced Language Server Protocol Operations
3
+
4
+ Provides comprehensive LSP functionality via subprocess calls to language servers.
5
+ Supplements Claude Code's native LSP support with advanced operations.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import subprocess
11
+ import tempfile
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _get_language_for_file(file_path: str) -> str:
20
+ """Determine language from file extension."""
21
+ suffix = Path(file_path).suffix.lower()
22
+ mapping = {
23
+ ".py": "python",
24
+ ".ts": "typescript",
25
+ ".tsx": "typescriptreact",
26
+ ".js": "javascript",
27
+ ".jsx": "javascriptreact",
28
+ ".go": "go",
29
+ ".rs": "rust",
30
+ ".java": "java",
31
+ ".rb": "ruby",
32
+ ".c": "c",
33
+ ".cpp": "cpp",
34
+ ".h": "c",
35
+ ".hpp": "cpp",
36
+ }
37
+ return mapping.get(suffix, "unknown")
38
+
39
+
40
+ def _position_to_offset(content: str, line: int, character: int) -> int:
41
+ """Convert line/character to byte offset."""
42
+ lines = content.split("\n")
43
+ offset = sum(len(l) + 1 for l in lines[:line - 1]) # 1-indexed
44
+ offset += character
45
+ return offset
46
+
47
+
48
+ async def lsp_hover(file_path: str, line: int, character: int) -> str:
49
+ """
50
+ 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
+ """
60
+ path = Path(file_path)
61
+ if not path.exists():
62
+ return f"Error: File not found: {file_path}"
63
+
64
+ lang = _get_language_for_file(file_path)
65
+
66
+ try:
67
+ if lang == "python":
68
+ # Use jedi for Python hover info
69
+ result = subprocess.run(
70
+ [
71
+ "python", "-c",
72
+ f"""
73
+ import jedi
74
+ script = jedi.Script(path='{file_path}')
75
+ completions = script.infer({line}, {character})
76
+ for c in completions[:1]:
77
+ print(f"Type: {{c.type}}")
78
+ print(f"Name: {{c.full_name}}")
79
+ if c.docstring():
80
+ print(f"\\nDocstring:\\n{{c.docstring()[:500]}}")
81
+ """
82
+ ],
83
+ capture_output=True,
84
+ text=True,
85
+ timeout=10,
86
+ )
87
+ output = result.stdout.strip()
88
+ if output:
89
+ return output
90
+ return f"No hover info at line {line}, character {character}"
91
+
92
+ elif lang in ("typescript", "javascript", "typescriptreact", "javascriptreact"):
93
+ # Use tsserver via quick-info
94
+ # For simplicity, fall back to message
95
+ return f"TypeScript hover requires running language server. Use Claude Code's native hover."
96
+
97
+ else:
98
+ return f"Hover not available for language: {lang}"
99
+
100
+ except FileNotFoundError as e:
101
+ return f"Tool not found: {e.filename}. Install jedi: pip install jedi"
102
+ except subprocess.TimeoutExpired:
103
+ return "Hover lookup timed out"
104
+ except Exception as e:
105
+ return f"Error: {str(e)}"
106
+
107
+
108
+ async def lsp_goto_definition(file_path: str, line: int, character: int) -> str:
109
+ """
110
+ 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
+ """
120
+ path = Path(file_path)
121
+ if not path.exists():
122
+ return f"Error: File not found: {file_path}"
123
+
124
+ lang = _get_language_for_file(file_path)
125
+
126
+ try:
127
+ if lang == "python":
128
+ result = subprocess.run(
129
+ [
130
+ "python", "-c",
131
+ f"""
132
+ import jedi
133
+ script = jedi.Script(path='{file_path}')
134
+ definitions = script.goto({line}, {character})
135
+ for d in definitions:
136
+ print(f"{{d.module_path}}:{{d.line}}:{{d.column}} - {{d.full_name}}")
137
+ """
138
+ ],
139
+ capture_output=True,
140
+ text=True,
141
+ timeout=10,
142
+ )
143
+ output = result.stdout.strip()
144
+ if output:
145
+ return output
146
+ return "No definition found"
147
+
148
+ elif lang in ("typescript", "javascript"):
149
+ return "TypeScript goto definition requires running language server. Use Claude Code's native navigation."
150
+
151
+ else:
152
+ return f"Goto definition not available for language: {lang}"
153
+
154
+ except FileNotFoundError as e:
155
+ return f"Tool not found: Install jedi: pip install jedi"
156
+ except subprocess.TimeoutExpired:
157
+ return "Definition lookup timed out"
158
+ except Exception as e:
159
+ return f"Error: {str(e)}"
160
+
161
+
162
+ async def lsp_find_references(
163
+ file_path: str,
164
+ line: int,
165
+ character: int,
166
+ include_declaration: bool = True
167
+ ) -> str:
168
+ """
169
+ 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
+ """
180
+ path = Path(file_path)
181
+ if not path.exists():
182
+ return f"Error: File not found: {file_path}"
183
+
184
+ lang = _get_language_for_file(file_path)
185
+
186
+ try:
187
+ if lang == "python":
188
+ result = subprocess.run(
189
+ [
190
+ "python", "-c",
191
+ f"""
192
+ import jedi
193
+ script = jedi.Script(path='{file_path}')
194
+ references = script.get_references({line}, {character}, include_builtins=False)
195
+ for r in references[:30]:
196
+ print(f"{{r.module_path}}:{{r.line}}:{{r.column}}")
197
+ if len(references) > 30:
198
+ print(f"... and {{len(references) - 30}} more")
199
+ """
200
+ ],
201
+ capture_output=True,
202
+ text=True,
203
+ timeout=15,
204
+ )
205
+ output = result.stdout.strip()
206
+ if output:
207
+ return output
208
+ return "No references found"
209
+
210
+ else:
211
+ return f"Find references not available for language: {lang}"
212
+
213
+ except subprocess.TimeoutExpired:
214
+ return "Reference search timed out"
215
+ except Exception as e:
216
+ return f"Error: {str(e)}"
217
+
218
+
219
+ async def lsp_document_symbols(file_path: str) -> str:
220
+ """
221
+ 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
+ """
229
+ path = Path(file_path)
230
+ if not path.exists():
231
+ return f"Error: File not found: {file_path}"
232
+
233
+ lang = _get_language_for_file(file_path)
234
+
235
+ try:
236
+ if lang == "python":
237
+ result = subprocess.run(
238
+ [
239
+ "python", "-c",
240
+ f"""
241
+ import jedi
242
+ script = jedi.Script(path='{file_path}')
243
+ names = script.get_names(all_scopes=True, definitions=True)
244
+ for n in names:
245
+ indent = " " * (n.get_line_code().count(" ") if n.get_line_code() else 0)
246
+ print(f"{{n.line:4d}} | {{indent}}{{n.type:10}} {{n.name}}")
247
+ """
248
+ ],
249
+ capture_output=True,
250
+ text=True,
251
+ timeout=10,
252
+ )
253
+ output = result.stdout.strip()
254
+ if output:
255
+ return f"**Symbols in {path.name}:**\n```\nLine | Symbol\n{output}\n```"
256
+ return "No symbols found"
257
+
258
+ else:
259
+ # Fallback: use ctags
260
+ result = subprocess.run(
261
+ ["ctags", "-x", "--sort=no", str(path)],
262
+ capture_output=True,
263
+ text=True,
264
+ timeout=10,
265
+ )
266
+ output = result.stdout.strip()
267
+ if output:
268
+ return f"**Symbols in {path.name}:**\n```\n{output}\n```"
269
+ return "No symbols found"
270
+
271
+ except FileNotFoundError:
272
+ return "Install jedi (pip install jedi) or ctags for symbol lookup"
273
+ except subprocess.TimeoutExpired:
274
+ return "Symbol lookup timed out"
275
+ except Exception as e:
276
+ return f"Error: {str(e)}"
277
+
278
+
279
+ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
280
+ """
281
+ 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
+ """
290
+ try:
291
+ # Use ctags to index and grep for symbols
292
+ result = subprocess.run(
293
+ ["rg", "-l", query, directory, "--type", "py", "--type", "ts", "--type", "js"],
294
+ capture_output=True,
295
+ text=True,
296
+ timeout=15,
297
+ )
298
+
299
+ files = result.stdout.strip().split("\n")[:10] # Limit files
300
+
301
+ if not files or files == [""]:
302
+ return "No matching files found"
303
+
304
+ symbols = []
305
+ for f in files:
306
+ if not f:
307
+ continue
308
+ # Get symbols from each file
309
+ ctags_result = subprocess.run(
310
+ ["ctags", "-x", "--sort=no", f],
311
+ capture_output=True,
312
+ text=True,
313
+ timeout=5,
314
+ )
315
+ for line in ctags_result.stdout.split("\n"):
316
+ if query.lower() in line.lower():
317
+ symbols.append(line)
318
+
319
+ if symbols:
320
+ return "\n".join(symbols[:20])
321
+ return f"No symbols matching '{query}' found"
322
+
323
+ except FileNotFoundError:
324
+ return "Install ctags and ripgrep for workspace symbol search"
325
+ except subprocess.TimeoutExpired:
326
+ return "Search timed out"
327
+ except Exception as e:
328
+ return f"Error: {str(e)}"
329
+
330
+
331
+ async def lsp_prepare_rename(file_path: str, line: int, character: int) -> str:
332
+ """
333
+ 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
+ """
343
+ path = Path(file_path)
344
+ if not path.exists():
345
+ return f"Error: File not found: {file_path}"
346
+
347
+ lang = _get_language_for_file(file_path)
348
+
349
+ try:
350
+ if lang == "python":
351
+ result = subprocess.run(
352
+ [
353
+ "python", "-c",
354
+ f"""
355
+ import jedi
356
+ script = jedi.Script(path='{file_path}')
357
+ refs = script.get_references({line}, {character})
358
+ if refs:
359
+ print(f"Symbol: {{refs[0].name}}")
360
+ print(f"Type: {{refs[0].type}}")
361
+ print(f"References: {{len(refs)}}")
362
+ print("✅ Rename is valid")
363
+ else:
364
+ print("❌ No symbol found at position")
365
+ """
366
+ ],
367
+ capture_output=True,
368
+ text=True,
369
+ timeout=10,
370
+ )
371
+ return result.stdout.strip() or "No symbol found at position"
372
+
373
+ else:
374
+ return f"Prepare rename not available for language: {lang}"
375
+
376
+ except Exception as e:
377
+ return f"Error: {str(e)}"
378
+
379
+
380
+ async def lsp_rename(
381
+ file_path: str,
382
+ line: int,
383
+ character: int,
384
+ new_name: str,
385
+ dry_run: bool = True
386
+ ) -> str:
387
+ """
388
+ 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
+ """
400
+ path = Path(file_path)
401
+ if not path.exists():
402
+ return f"Error: File not found: {file_path}"
403
+
404
+ lang = _get_language_for_file(file_path)
405
+
406
+ try:
407
+ if lang == "python":
408
+ result = subprocess.run(
409
+ [
410
+ "python", "-c",
411
+ f"""
412
+ import jedi
413
+ script = jedi.Script(path='{file_path}')
414
+ refactoring = script.rename({line}, {character}, new_name='{new_name}')
415
+ for path, changed in refactoring.get_changed_files().items():
416
+ print(f"File: {{path}}")
417
+ print(changed[:500])
418
+ print("---")
419
+ """
420
+ ],
421
+ capture_output=True,
422
+ text=True,
423
+ timeout=15,
424
+ )
425
+ output = result.stdout.strip()
426
+ if output and not dry_run:
427
+ # Apply changes
428
+ return f"**Dry run** (set dry_run=False to apply):\n{output}"
429
+ elif output:
430
+ return f"**Would rename to '{new_name}':**\n{output}"
431
+ return "No changes needed"
432
+
433
+ else:
434
+ return f"Rename not available for language: {lang}. Use IDE refactoring."
435
+
436
+ except Exception as e:
437
+ return f"Error: {str(e)}"
438
+
439
+
440
+ async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
441
+ """
442
+ 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
+ """
452
+ path = Path(file_path)
453
+ if not path.exists():
454
+ return f"Error: File not found: {file_path}"
455
+
456
+ lang = _get_language_for_file(file_path)
457
+
458
+ try:
459
+ if lang == "python":
460
+ # Use ruff to suggest fixes
461
+ result = subprocess.run(
462
+ ["ruff", "check", str(path), "--output-format=json", "--show-fixes"],
463
+ capture_output=True,
464
+ text=True,
465
+ timeout=10,
466
+ )
467
+
468
+ try:
469
+ diagnostics = json.loads(result.stdout)
470
+ actions = []
471
+ for d in diagnostics:
472
+ if d.get("location", {}).get("row") == line:
473
+ code = d.get("code", "")
474
+ msg = d.get("message", "")
475
+ fix = d.get("fix", {})
476
+ if fix:
477
+ actions.append(f"- [{code}] {msg} (auto-fix available)")
478
+ else:
479
+ actions.append(f"- [{code}] {msg}")
480
+
481
+ if actions:
482
+ return "**Available code actions:**\n" + "\n".join(actions)
483
+ return "No code actions available at this position"
484
+
485
+ except json.JSONDecodeError:
486
+ return "No code actions available"
487
+
488
+ else:
489
+ return f"Code actions not available for language: {lang}"
490
+
491
+ except FileNotFoundError:
492
+ return "Install ruff for Python code actions: pip install ruff"
493
+ except Exception as e:
494
+ return f"Error: {str(e)}"
495
+
496
+
497
+ async def lsp_servers() -> str:
498
+ """
499
+ List available LSP servers and their installation status.
500
+
501
+ Returns:
502
+ Table of available language servers.
503
+ """
504
+ servers = [
505
+ ("python", "jedi", "pip install jedi"),
506
+ ("python", "ruff", "pip install ruff"),
507
+ ("typescript", "typescript-language-server", "npm i -g typescript-language-server"),
508
+ ("go", "gopls", "go install golang.org/x/tools/gopls@latest"),
509
+ ("rust", "rust-analyzer", "rustup component add rust-analyzer"),
510
+ ]
511
+
512
+ lines = ["| Language | Server | Status | Install |", "|----------|--------|--------|---------|"]
513
+
514
+ for lang, server, install in servers:
515
+ # Check if installed
516
+ try:
517
+ subprocess.run([server, "--version"], capture_output=True, timeout=2)
518
+ status = "✅ Installed"
519
+ except FileNotFoundError:
520
+ status = "❌ Not installed"
521
+ except Exception:
522
+ status = "⚠️ Unknown"
523
+
524
+ lines.append(f"| {lang} | {server} | {status} | `{install}` |")
525
+
526
+ return "\n".join(lines)