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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +235 -14
- ripperdoc/cli/commands/__init__.py +2 -0
- ripperdoc/cli/commands/agents_cmd.py +132 -5
- ripperdoc/cli/commands/clear_cmd.py +8 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/models_cmd.py +3 -3
- ripperdoc/cli/commands/resume_cmd.py +4 -0
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/rich_ui.py +295 -24
- ripperdoc/cli/ui/spinner.py +30 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/wizard.py +6 -8
- ripperdoc/core/agents.py +10 -3
- ripperdoc/core/config.py +3 -6
- ripperdoc/core/default_tools.py +90 -10
- ripperdoc/core/hooks/events.py +4 -0
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/permissions.py +78 -4
- ripperdoc/core/providers/openai.py +29 -19
- ripperdoc/core/query.py +192 -31
- ripperdoc/core/tool.py +9 -4
- ripperdoc/sdk/client.py +77 -2
- ripperdoc/tools/background_shell.py +305 -134
- ripperdoc/tools/bash_tool.py +42 -13
- ripperdoc/tools/file_edit_tool.py +159 -50
- ripperdoc/tools/file_read_tool.py +20 -0
- ripperdoc/tools/file_write_tool.py +7 -8
- ripperdoc/tools/lsp_tool.py +615 -0
- ripperdoc/tools/task_tool.py +514 -65
- ripperdoc/utils/conversation_compaction.py +1 -1
- ripperdoc/utils/file_watch.py +206 -3
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/message_formatting.py +5 -2
- ripperdoc/utils/messages.py +21 -1
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_stats.py +293 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/RECORD +45 -39
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {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)
|