ripperdoc 0.2.9__py3-none-any.whl → 0.2.10__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.
Files changed (45) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +235 -14
  3. ripperdoc/cli/commands/__init__.py +2 -0
  4. ripperdoc/cli/commands/agents_cmd.py +132 -5
  5. ripperdoc/cli/commands/clear_cmd.py +8 -0
  6. ripperdoc/cli/commands/exit_cmd.py +1 -0
  7. ripperdoc/cli/commands/models_cmd.py +3 -3
  8. ripperdoc/cli/commands/resume_cmd.py +4 -0
  9. ripperdoc/cli/commands/stats_cmd.py +244 -0
  10. ripperdoc/cli/ui/panels.py +1 -0
  11. ripperdoc/cli/ui/rich_ui.py +295 -24
  12. ripperdoc/cli/ui/spinner.py +30 -18
  13. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  14. ripperdoc/cli/ui/wizard.py +6 -8
  15. ripperdoc/core/agents.py +10 -3
  16. ripperdoc/core/config.py +3 -6
  17. ripperdoc/core/default_tools.py +90 -10
  18. ripperdoc/core/hooks/events.py +4 -0
  19. ripperdoc/core/hooks/llm_callback.py +59 -0
  20. ripperdoc/core/permissions.py +78 -4
  21. ripperdoc/core/providers/openai.py +29 -19
  22. ripperdoc/core/query.py +192 -31
  23. ripperdoc/core/tool.py +9 -4
  24. ripperdoc/sdk/client.py +77 -2
  25. ripperdoc/tools/background_shell.py +305 -134
  26. ripperdoc/tools/bash_tool.py +42 -13
  27. ripperdoc/tools/file_edit_tool.py +159 -50
  28. ripperdoc/tools/file_read_tool.py +20 -0
  29. ripperdoc/tools/file_write_tool.py +7 -8
  30. ripperdoc/tools/lsp_tool.py +615 -0
  31. ripperdoc/tools/task_tool.py +514 -65
  32. ripperdoc/utils/conversation_compaction.py +1 -1
  33. ripperdoc/utils/file_watch.py +206 -3
  34. ripperdoc/utils/lsp.py +806 -0
  35. ripperdoc/utils/message_formatting.py +5 -2
  36. ripperdoc/utils/messages.py +21 -1
  37. ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
  38. ripperdoc/utils/session_heatmap.py +244 -0
  39. ripperdoc/utils/session_stats.py +293 -0
  40. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
  41. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/RECORD +45 -39
  42. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
  43. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
  44. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
  45. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,615 @@
1
+ """LSP tool for code intelligence queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ from ripperdoc.core.tool import (
11
+ Tool,
12
+ ToolOutput,
13
+ ToolResult,
14
+ ToolUseContext,
15
+ ToolUseExample,
16
+ ValidationResult,
17
+ )
18
+ from ripperdoc.utils.log import get_logger
19
+ from ripperdoc.utils.path_ignore import check_path_for_tool
20
+ from ripperdoc.utils.lsp import (
21
+ LspLaunchError,
22
+ LspProtocolError,
23
+ LspRequestError,
24
+ ensure_lsp_manager,
25
+ uri_to_path,
26
+ )
27
+
28
+
29
+ logger = get_logger()
30
+
31
+ LSP_USAGE = (
32
+ "Interact with Language Server Protocol (LSP) servers to get code intelligence features.\n\n"
33
+ "Supported operations:\n"
34
+ "- goToDefinition: Find where a symbol is defined\n"
35
+ "- findReferences: Find all references to a symbol\n"
36
+ "- hover: Get hover information (documentation, type info) for a symbol\n"
37
+ "- documentSymbol: Get all symbols (functions, classes, variables) in a document\n"
38
+ "- workspaceSymbol: Search for symbols across the entire workspace\n"
39
+ "- goToImplementation: Find implementations of an interface or abstract method\n\n"
40
+ "All operations require:\n"
41
+ "- filePath: The file to operate on\n"
42
+ "- line: The line number (1-based, as shown in editors)\n"
43
+ "- character: The character offset (1-based, as shown in editors)\n\n"
44
+ "Note: LSP servers must be configured for the file type. "
45
+ "If no server is available, an error will be returned."
46
+ )
47
+
48
+ MAX_RESULTS = 50
49
+
50
+ SYMBOL_KIND_NAMES = {
51
+ 1: "File",
52
+ 2: "Module",
53
+ 3: "Namespace",
54
+ 4: "Package",
55
+ 5: "Class",
56
+ 6: "Method",
57
+ 7: "Property",
58
+ 8: "Field",
59
+ 9: "Constructor",
60
+ 10: "Enum",
61
+ 11: "Interface",
62
+ 12: "Function",
63
+ 13: "Variable",
64
+ 14: "Constant",
65
+ 15: "String",
66
+ 16: "Number",
67
+ 17: "Boolean",
68
+ 18: "Array",
69
+ 19: "Object",
70
+ 20: "Key",
71
+ 21: "Null",
72
+ 22: "EnumMember",
73
+ 23: "Struct",
74
+ 24: "Event",
75
+ 25: "Operator",
76
+ 26: "TypeParameter",
77
+ }
78
+
79
+
80
+ def _resolve_file_path(raw_path: str) -> Path:
81
+ candidate = Path(raw_path).expanduser()
82
+ if not candidate.is_absolute():
83
+ candidate = (Path.cwd() / candidate).resolve()
84
+ else:
85
+ candidate = candidate.resolve()
86
+ return candidate
87
+
88
+
89
+ def _display_path(file_path: Path, verbose: bool) -> str:
90
+ if verbose:
91
+ return str(file_path)
92
+ try:
93
+ rel = file_path.resolve().relative_to(Path.cwd().resolve())
94
+ except (ValueError, OSError):
95
+ return str(file_path)
96
+ rel_str = str(rel)
97
+ return rel_str if rel_str != "." else str(file_path)
98
+
99
+
100
+ def _read_text(file_path: Path) -> str:
101
+ return file_path.read_text(encoding="utf-8", errors="replace")
102
+
103
+
104
+ def _normalize_position(
105
+ lines: List[str], line: int, character: int
106
+ ) -> Tuple[int, int, str]:
107
+ if not lines:
108
+ return 0, 0, ""
109
+ line_index = max(0, min(line - 1, len(lines) - 1))
110
+ line_text = lines[line_index]
111
+ char_index = max(0, min(character - 1, len(line_text)))
112
+ return line_index, char_index, line_text
113
+
114
+
115
+ def _extract_symbol_at_position(line_text: str, char_index: int) -> Optional[str]:
116
+ if not line_text:
117
+ return None
118
+ if char_index >= len(line_text):
119
+ char_index = len(line_text) - 1
120
+ if char_index < 0:
121
+ return None
122
+
123
+ if not line_text[char_index].isalnum() and line_text[char_index] != "_":
124
+ if char_index > 0 and (line_text[char_index - 1].isalnum() or line_text[char_index - 1] == "_"):
125
+ char_index -= 1
126
+ else:
127
+ return None
128
+
129
+ start = char_index
130
+ while start > 0 and (line_text[start - 1].isalnum() or line_text[start - 1] == "_"):
131
+ start -= 1
132
+ end = char_index
133
+ while end + 1 < len(line_text) and (line_text[end + 1].isalnum() or line_text[end + 1] == "_"):
134
+ end += 1
135
+ symbol = line_text[start : end + 1].strip()
136
+ return symbol or None
137
+
138
+
139
+ def _symbol_kind_name(kind: Any) -> str:
140
+ try:
141
+ kind_value = int(kind)
142
+ except (TypeError, ValueError):
143
+ return "Unknown"
144
+ return SYMBOL_KIND_NAMES.get(kind_value, "Unknown")
145
+
146
+
147
+ def _location_to_path_line_char(location: Optional[Dict[str, Any]]) -> Tuple[str, int, int]:
148
+ if not location:
149
+ return "<unknown>", 0, 0
150
+ uri = location.get("uri") or location.get("targetUri")
151
+ range_info = location.get("range") or location.get("targetRange") or location.get(
152
+ "targetSelectionRange"
153
+ )
154
+ path = "<unknown>"
155
+ if isinstance(uri, str):
156
+ file_path = uri_to_path(uri)
157
+ if file_path:
158
+ path = str(file_path)
159
+ line = 0
160
+ character = 0
161
+ if isinstance(range_info, dict):
162
+ start = range_info.get("start")
163
+ if isinstance(start, dict):
164
+ line = int(start.get("line", 0)) + 1
165
+ character = int(start.get("character", 0)) + 1
166
+ return path, line, character
167
+
168
+
169
+ def _format_locations(
170
+ label: str, locations: List[Dict[str, Any]]
171
+ ) -> Tuple[str, int, int]:
172
+ if not locations:
173
+ return f"No {label} found.", 0, 0
174
+
175
+ unique_files = set()
176
+ lines: List[str] = []
177
+ for loc in locations[:MAX_RESULTS]:
178
+ path, line, char = _location_to_path_line_char(loc)
179
+ unique_files.add(path)
180
+ lines.append(f"{path}:{line}:{char}")
181
+
182
+ omitted = len(locations) - len(lines)
183
+ if omitted > 0:
184
+ lines.append(f"... {omitted} more result(s) not shown")
185
+
186
+ summary = f"{len(locations)} {label} found in {len(unique_files)} file(s)."
187
+ return f"{summary}\n\n" + "\n".join(lines), len(locations), len(unique_files)
188
+
189
+
190
+ def _format_hover(result: Any) -> Tuple[str, int, int]:
191
+ if not result:
192
+ return "No hover information found.", 0, 0
193
+
194
+ if isinstance(result, str):
195
+ text = result.strip()
196
+ return (text, 1, 1) if text else ("No hover information found.", 0, 0)
197
+
198
+ contents = result.get("contents") if isinstance(result, dict) else None
199
+ if not contents:
200
+ return "No hover information found.", 0, 0
201
+
202
+ if isinstance(contents, dict):
203
+ value = contents.get("value")
204
+ text = value if isinstance(value, str) else str(contents)
205
+ elif isinstance(contents, list):
206
+ parts = []
207
+ for item in contents:
208
+ if isinstance(item, str):
209
+ parts.append(item)
210
+ elif isinstance(item, dict):
211
+ value = item.get("value")
212
+ parts.append(value if isinstance(value, str) else str(item))
213
+ else:
214
+ parts.append(str(item))
215
+ text = "\n".join([part for part in parts if part])
216
+ else:
217
+ text = str(contents)
218
+
219
+ text = text.strip()
220
+ if not text:
221
+ return "No hover information found.", 0, 0
222
+ return text, 1, 1
223
+
224
+
225
+ def _flatten_document_symbols(
226
+ symbols: List[Dict[str, Any]],
227
+ depth: int = 0,
228
+ lines: Optional[List[str]] = None,
229
+ ) -> Tuple[List[str], int]:
230
+ if lines is None:
231
+ lines = []
232
+ count = 0
233
+
234
+ for symbol in symbols:
235
+ count += 1
236
+ name = symbol.get("name", "<unknown>")
237
+ detail = symbol.get("detail")
238
+ kind = _symbol_kind_name(symbol.get("kind"))
239
+ selection = symbol.get("selectionRange") or symbol.get("range") or {}
240
+ start = selection.get("start") if isinstance(selection, dict) else {}
241
+ line = int(start.get("line", 0)) + 1 if isinstance(start, dict) else 0
242
+ char = int(start.get("character", 0)) + 1 if isinstance(start, dict) else 0
243
+ prefix = " " * depth
244
+ detail_text = f" - {detail}" if detail else ""
245
+ lines.append(f"{prefix}{name}{detail_text} ({kind}) @ {line}:{char}")
246
+
247
+ children = symbol.get("children")
248
+ if isinstance(children, list) and children:
249
+ child_lines, child_count = _flatten_document_symbols(children, depth + 1, lines)
250
+ count += child_count
251
+ lines = child_lines
252
+
253
+ return lines, count
254
+
255
+
256
+ def _format_document_symbols(result: Any) -> Tuple[str, int, int]:
257
+ if not result:
258
+ return "No document symbols found.", 0, 0
259
+
260
+ symbols: List[Dict[str, Any]] = []
261
+ if isinstance(result, list):
262
+ symbols = [s for s in result if isinstance(s, dict)]
263
+ if not symbols:
264
+ return "No document symbols found.", 0, 0
265
+
266
+ lines, count = _flatten_document_symbols(symbols)
267
+ if len(lines) > MAX_RESULTS:
268
+ omitted = len(lines) - MAX_RESULTS
269
+ lines = lines[:MAX_RESULTS] + [f"... {omitted} more result(s) not shown"]
270
+
271
+ summary = f"{count} symbol(s) found in document."
272
+ return f"{summary}\n\n" + "\n".join(lines), count, 1
273
+
274
+
275
+ def _format_workspace_symbols(result: Any) -> Tuple[str, int, int]:
276
+ if not result:
277
+ return "No workspace symbols found.", 0, 0
278
+
279
+ symbols: List[Dict[str, Any]] = []
280
+ if isinstance(result, list):
281
+ symbols = [s for s in result if isinstance(s, dict)]
282
+ if not symbols:
283
+ return "No workspace symbols found.", 0, 0
284
+
285
+ unique_files = set()
286
+ lines: List[str] = []
287
+ for symbol in symbols[:MAX_RESULTS]:
288
+ name = symbol.get("name", "<unknown>")
289
+ kind = _symbol_kind_name(symbol.get("kind"))
290
+ container = symbol.get("containerName")
291
+ location = None
292
+ if isinstance(symbol.get("location"), dict):
293
+ location = symbol.get("location")
294
+ else:
295
+ locations = symbol.get("locations")
296
+ if isinstance(locations, list) and locations:
297
+ first = locations[0]
298
+ if isinstance(first, dict):
299
+ location = first
300
+ path, line, char = _location_to_path_line_char(location)
301
+ unique_files.add(path)
302
+ container_text = f" ({container})" if container else ""
303
+ lines.append(f"{name}{container_text} ({kind}) {path}:{line}:{char}")
304
+
305
+ omitted = len(symbols) - len(lines)
306
+ if omitted > 0:
307
+ lines.append(f"... {omitted} more result(s) not shown")
308
+
309
+ summary = f"{len(symbols)} symbol(s) found in {len(unique_files)} file(s)."
310
+ return f"{summary}\n\n" + "\n".join(lines), len(symbols), len(unique_files)
311
+
312
+
313
+ class LspToolInput(BaseModel):
314
+ """Input schema for LspTool."""
315
+
316
+ model_config = ConfigDict(populate_by_name=True)
317
+
318
+ operation: Literal[
319
+ "goToDefinition",
320
+ "findReferences",
321
+ "hover",
322
+ "documentSymbol",
323
+ "workspaceSymbol",
324
+ "goToImplementation",
325
+ ] = Field(description="The LSP operation to perform.")
326
+ file_path: str = Field(
327
+ validation_alias="filePath",
328
+ serialization_alias="filePath",
329
+ description="The absolute or relative path to the file",
330
+ )
331
+ line: int = Field(ge=1, description="The line number (1-based, as shown in editors)")
332
+ character: int = Field(ge=1, description="The character offset (1-based, as shown in editors)")
333
+
334
+
335
+ class LspToolOutput(BaseModel):
336
+ """Output from LspTool."""
337
+
338
+ model_config = ConfigDict(populate_by_name=True)
339
+
340
+ operation: str
341
+ result: str
342
+ file_path: str = Field(validation_alias="filePath", serialization_alias="filePath")
343
+ is_error: bool = Field(
344
+ default=False,
345
+ validation_alias="is_error",
346
+ serialization_alias="is_error",
347
+ description="Whether the LSP operation failed.",
348
+ )
349
+ result_count: Optional[int] = Field(
350
+ default=None,
351
+ validation_alias="resultCount",
352
+ serialization_alias="resultCount",
353
+ )
354
+ file_count: Optional[int] = Field(
355
+ default=None,
356
+ validation_alias="fileCount",
357
+ serialization_alias="fileCount",
358
+ )
359
+
360
+
361
+ class LspTool(Tool[LspToolInput, LspToolOutput]):
362
+ """Tool for LSP-backed code intelligence."""
363
+
364
+ @property
365
+ def name(self) -> str:
366
+ return "LSP"
367
+
368
+ async def description(self) -> str:
369
+ return LSP_USAGE
370
+
371
+ @property
372
+ def input_schema(self) -> type[LspToolInput]:
373
+ return LspToolInput
374
+
375
+ def input_examples(self) -> List[ToolUseExample]:
376
+ return [
377
+ ToolUseExample(
378
+ description="Jump to a symbol definition",
379
+ example={
380
+ "operation": "goToDefinition",
381
+ "filePath": "src/main.py",
382
+ "line": 12,
383
+ "character": 8,
384
+ },
385
+ ),
386
+ ToolUseExample(
387
+ description="Find references to a function",
388
+ example={
389
+ "operation": "findReferences",
390
+ "filePath": "src/main.py",
391
+ "line": 12,
392
+ "character": 8,
393
+ },
394
+ ),
395
+ ToolUseExample(
396
+ description="List document symbols",
397
+ example={
398
+ "operation": "documentSymbol",
399
+ "filePath": "src/main.py",
400
+ "line": 1,
401
+ "character": 1,
402
+ },
403
+ ),
404
+ ]
405
+
406
+ async def prompt(self, _yolo_mode: bool = False) -> str:
407
+ return LSP_USAGE
408
+
409
+ def is_read_only(self) -> bool:
410
+ return True
411
+
412
+ def is_concurrency_safe(self) -> bool:
413
+ return True
414
+
415
+ def needs_permissions(self, _input_data: Optional[LspToolInput] = None) -> bool:
416
+ return False
417
+
418
+ async def validate_input(
419
+ self, input_data: LspToolInput, _context: Optional[ToolUseContext] = None
420
+ ) -> ValidationResult:
421
+ try:
422
+ resolved_path = _resolve_file_path(input_data.file_path)
423
+ except (OSError, RuntimeError, ValueError) as exc:
424
+ return ValidationResult(result=False, message=str(exc))
425
+
426
+ if not resolved_path.exists():
427
+ return ValidationResult(result=False, message=f"File not found: {input_data.file_path}")
428
+ if not resolved_path.is_file():
429
+ return ValidationResult(
430
+ result=False, message=f"Path is not a file: {input_data.file_path}"
431
+ )
432
+
433
+ should_proceed, warning_msg = check_path_for_tool(
434
+ resolved_path, tool_name="LSP", warn_only=True
435
+ )
436
+ if warning_msg:
437
+ logger.info("[lsp_tool] %s", warning_msg)
438
+ if not should_proceed:
439
+ return ValidationResult(result=False, message=warning_msg or "Access denied.")
440
+
441
+ return ValidationResult(result=True)
442
+
443
+ def render_result_for_assistant(self, output: LspToolOutput) -> str:
444
+ return output.result
445
+
446
+ def render_tool_use_message(self, input_data: LspToolInput, verbose: bool = False) -> str:
447
+ try:
448
+ file_path = _resolve_file_path(input_data.file_path)
449
+ except (OSError, RuntimeError, ValueError):
450
+ file_path = Path(input_data.file_path)
451
+
452
+ symbol = None
453
+ if input_data.operation in {
454
+ "goToDefinition",
455
+ "findReferences",
456
+ "hover",
457
+ "goToImplementation",
458
+ "workspaceSymbol",
459
+ }:
460
+ try:
461
+ text = _read_text(file_path)
462
+ lines = text.splitlines()
463
+ _line_index, char_index, line_text = _normalize_position(
464
+ lines, input_data.line, input_data.character
465
+ )
466
+ symbol = _extract_symbol_at_position(line_text, char_index)
467
+ except (OSError, RuntimeError, UnicodeDecodeError):
468
+ symbol = None
469
+
470
+ parts = [f'operation: "{input_data.operation}"']
471
+ if symbol:
472
+ parts.append(f'symbol: "{symbol}"')
473
+ parts.append(f'file: "{_display_path(file_path, verbose)}"')
474
+ if not symbol:
475
+ parts.append(f"position: {input_data.line}:{input_data.character}")
476
+ return ", ".join(parts)
477
+
478
+ async def call(
479
+ self, input_data: LspToolInput, _context: ToolUseContext
480
+ ) -> AsyncGenerator[ToolOutput, None]:
481
+ try:
482
+ file_path = _resolve_file_path(input_data.file_path)
483
+ text = _read_text(file_path)
484
+ lines = text.splitlines()
485
+ line_index, char_index, line_text = _normalize_position(
486
+ lines, input_data.line, input_data.character
487
+ )
488
+ symbol = _extract_symbol_at_position(line_text, char_index)
489
+ except (OSError, RuntimeError, UnicodeDecodeError, ValueError) as exc:
490
+ output = LspToolOutput(
491
+ operation=input_data.operation,
492
+ result=f"Error reading file for LSP: {exc}",
493
+ file_path=input_data.file_path,
494
+ is_error=True,
495
+ )
496
+ yield ToolResult(data=output, result_for_assistant=output.result)
497
+ return
498
+
499
+ operation = input_data.operation
500
+ method: Optional[str] = None
501
+ params: Optional[Dict[str, Any]] = None
502
+
503
+ position = {"line": line_index, "character": char_index}
504
+ text_document = {"uri": file_path.resolve().as_uri()}
505
+
506
+ if operation == "goToDefinition":
507
+ method = "textDocument/definition"
508
+ params = {"textDocument": text_document, "position": position}
509
+ elif operation == "findReferences":
510
+ method = "textDocument/references"
511
+ params = {
512
+ "textDocument": text_document,
513
+ "position": position,
514
+ "context": {"includeDeclaration": True},
515
+ }
516
+ elif operation == "hover":
517
+ method = "textDocument/hover"
518
+ params = {"textDocument": text_document, "position": position}
519
+ elif operation == "documentSymbol":
520
+ method = "textDocument/documentSymbol"
521
+ params = {"textDocument": text_document}
522
+ elif operation == "workspaceSymbol":
523
+ if not symbol:
524
+ output = LspToolOutput(
525
+ operation=operation,
526
+ result="No symbol found at the given position to search in workspace.",
527
+ file_path=input_data.file_path,
528
+ )
529
+ yield ToolResult(data=output, result_for_assistant=output.result)
530
+ return
531
+ method = "workspace/symbol"
532
+ params = {"query": symbol}
533
+ elif operation == "goToImplementation":
534
+ method = "textDocument/implementation"
535
+ params = {"textDocument": text_document, "position": position}
536
+ else:
537
+ output = LspToolOutput(
538
+ operation=operation,
539
+ result=f"Unknown LSP operation: {operation}",
540
+ file_path=input_data.file_path,
541
+ is_error=True,
542
+ )
543
+ yield ToolResult(data=output, result_for_assistant=output.result)
544
+ return
545
+
546
+ manager = await ensure_lsp_manager(Path.cwd())
547
+ server_info = await manager.server_for_path(file_path)
548
+ if not server_info:
549
+ output = LspToolOutput(
550
+ operation=operation,
551
+ result=(
552
+ f"No LSP server available for file type: {file_path.suffix or 'unknown'}. "
553
+ "Configure servers in ~/.ripperdoc/lsp.json, ~/.lsp.json, "
554
+ ".ripperdoc/lsp.json, or .lsp.json."
555
+ ),
556
+ file_path=input_data.file_path,
557
+ is_error=True,
558
+ )
559
+ yield ToolResult(data=output, result_for_assistant=output.result)
560
+ return
561
+
562
+ server, _config, language_id = server_info
563
+
564
+ try:
565
+ await server.ensure_initialized()
566
+ if method.startswith("textDocument/"):
567
+ await server.ensure_document_open(file_path, text, language_id)
568
+ result = await server.request(method, params)
569
+ except (LspLaunchError, LspProtocolError, LspRequestError) as exc:
570
+ output = LspToolOutput(
571
+ operation=operation,
572
+ result=f"Error performing {operation}: {exc}",
573
+ file_path=input_data.file_path,
574
+ is_error=True,
575
+ )
576
+ yield ToolResult(data=output, result_for_assistant=output.result)
577
+ return
578
+
579
+ formatted: str
580
+ result_count: Optional[int] = None
581
+ file_count: Optional[int] = None
582
+
583
+ if operation == "goToDefinition":
584
+ if isinstance(result, dict):
585
+ result = [result]
586
+ formatted, result_count, file_count = _format_locations(
587
+ "definition(s)", result or []
588
+ )
589
+ elif operation == "findReferences":
590
+ formatted, result_count, file_count = _format_locations(
591
+ "reference(s)", result or []
592
+ )
593
+ elif operation == "hover":
594
+ formatted, result_count, file_count = _format_hover(result or {})
595
+ elif operation == "documentSymbol":
596
+ formatted, result_count, file_count = _format_document_symbols(result)
597
+ elif operation == "workspaceSymbol":
598
+ formatted, result_count, file_count = _format_workspace_symbols(result)
599
+ elif operation == "goToImplementation":
600
+ if isinstance(result, dict):
601
+ result = [result]
602
+ formatted, result_count, file_count = _format_locations(
603
+ "implementation(s)", result or []
604
+ )
605
+ else:
606
+ formatted = str(result)
607
+
608
+ output = LspToolOutput(
609
+ operation=operation,
610
+ result=formatted,
611
+ file_path=input_data.file_path,
612
+ result_count=result_count,
613
+ file_count=file_count,
614
+ )
615
+ yield ToolResult(data=output, result_for_assistant=output.result)