hf-agentfinder 0.1.0__tar.gz
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.
- hf_agentfinder-0.1.0/.fast-agent/.check_for_update_done +0 -0
- hf_agentfinder-0.1.0/.fast-agent/agent-cards/dev.md +42 -0
- hf_agentfinder-0.1.0/.fast-agent/agent-cards/multilspy_tools.py +421 -0
- hf_agentfinder-0.1.0/.fast-agent/fast-agent.yaml +3 -0
- hf_agentfinder-0.1.0/.fast-agent/sessions/2605121105-LFZaCv/history_agent.json +1348 -0
- hf_agentfinder-0.1.0/.fast-agent/sessions/2605121105-LFZaCv/history_agent_previous.json +1256 -0
- hf_agentfinder-0.1.0/.fast-agent/sessions/2605121105-LFZaCv/session.json +61 -0
- hf_agentfinder-0.1.0/.fast-agent/sessions/2605121135-j3aamD/history_dev.json +4135 -0
- hf_agentfinder-0.1.0/.fast-agent/sessions/2605121135-j3aamD/history_dev_previous.json +4053 -0
- hf_agentfinder-0.1.0/.fast-agent/sessions/2605121135-j3aamD/session.json +65 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/.skill-source.json +14 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/SKILL.md +195 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/python/dev.md +32 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/python/multilspy_tools.py +421 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/rust/dev.md +32 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/rust/rust_lsp_tools.py +589 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/typescript/dev.md +32 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/typescript/multilspy_tools.py +424 -0
- hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/references/rust.md +131 -0
- hf_agentfinder-0.1.0/.github/workflows/ci.yml +45 -0
- hf_agentfinder-0.1.0/.github/workflows/release.yml +50 -0
- hf_agentfinder-0.1.0/.gitignore +8 -0
- hf_agentfinder-0.1.0/AGENTS.md +3 -0
- hf_agentfinder-0.1.0/PKG-INFO +91 -0
- hf_agentfinder-0.1.0/README.md +78 -0
- hf_agentfinder-0.1.0/pyproject.toml +91 -0
- hf_agentfinder-0.1.0/scripts/release-local.sh +98 -0
- hf_agentfinder-0.1.0/spec/agentfinder.md +567 -0
- hf_agentfinder-0.1.0/src/agentfinder/__init__.py +1 -0
- hf_agentfinder-0.1.0/src/agentfinder/cli.py +152 -0
- hf_agentfinder-0.1.0/src/agentfinder/hf_spaces.py +269 -0
- hf_agentfinder-0.1.0/src/agentfinder/models.py +56 -0
- hf_agentfinder-0.1.0/src/agentfinder/server.py +121 -0
- hf_agentfinder-0.1.0/tests/test_hf_spaces.py +173 -0
- hf_agentfinder-0.1.0/typesafe.md +25 -0
- hf_agentfinder-0.1.0/uv.lock +523 -0
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev
|
|
3
|
+
# Leave this at the configured system default unless you want to pin a model.
|
|
4
|
+
model: $system.default
|
|
5
|
+
default: true
|
|
6
|
+
shell: true
|
|
7
|
+
function_tools:
|
|
8
|
+
- multilspy_tools.py:lsp_hover
|
|
9
|
+
- multilspy_tools.py:lsp_definition
|
|
10
|
+
- multilspy_tools.py:lsp_references
|
|
11
|
+
- multilspy_tools.py:lsp_document_symbols
|
|
12
|
+
- multilspy_tools.py:lsp_workspace_symbols
|
|
13
|
+
- multilspy_tools.py:lsp_diagnostics
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are a development assistant for this Python project.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Code Navigation
|
|
20
|
+
|
|
21
|
+
Use LSP tools for structural queries: definitions, references, symbols, hover info, diagnostics.
|
|
22
|
+
For broad text discovery or file operations, use whatever search tool or card is already available in this environment.
|
|
23
|
+
|
|
24
|
+
{{serverInstructions}}
|
|
25
|
+
{{agentSkills}}
|
|
26
|
+
{{env}}
|
|
27
|
+
|
|
28
|
+
Note any project specific instructions or details here:
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
{{file_silent:AGENTS.md}}
|
|
33
|
+
|
|
34
|
+
{{file_silent:pyproject.toml}}
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
Mermaid diagrams between code fences are supported.
|
|
39
|
+
|
|
40
|
+
{{model_specific}}
|
|
41
|
+
|
|
42
|
+
The current date is {{currentDate}}.
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Function tools for ty-backed MultiLSPy queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from shutil import which
|
|
12
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, TypeVar
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
from multilspy.language_server import LanguageServer
|
|
16
|
+
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
|
|
17
|
+
from multilspy.multilspy_config import Language, MultilspyConfig
|
|
18
|
+
from multilspy.multilspy_exceptions import MultilspyException
|
|
19
|
+
from multilspy.multilspy_logger import MultilspyLogger
|
|
20
|
+
|
|
21
|
+
# REQUIRED: Adjust parents[] if the card is not stored at .fast-agent/agent-cards/.
|
|
22
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
23
|
+
|
|
24
|
+
# RECOMMENDED: Narrow LSP access to the parts of the repo you actually want queried.
|
|
25
|
+
# Use {"."} to allow the entire repo.
|
|
26
|
+
_ALLOWED_DIRS = {"src", "tests", "spec"}
|
|
27
|
+
_ALLOWED_FILES: set[str] = set()
|
|
28
|
+
|
|
29
|
+
_server_lock = asyncio.Lock()
|
|
30
|
+
_server_stack: AsyncExitStack | None = None
|
|
31
|
+
_server: "TyServer" | None = None
|
|
32
|
+
|
|
33
|
+
_CONTENT_MODIFIED_RETRY_ATTEMPTS = 2
|
|
34
|
+
_CONTENT_MODIFIED_BASE_DELAY_SECONDS = 0.05
|
|
35
|
+
|
|
36
|
+
_ReturnT = TypeVar("_ReturnT")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TyServer(LanguageServer):
|
|
40
|
+
"""Language server wrapper for ty language server."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
|
|
43
|
+
ty_cmd = _resolve_ty_cmd()
|
|
44
|
+
super().__init__(
|
|
45
|
+
config,
|
|
46
|
+
logger,
|
|
47
|
+
repository_root_path,
|
|
48
|
+
ProcessLaunchInfo(cmd=ty_cmd, cwd=repository_root_path),
|
|
49
|
+
"python",
|
|
50
|
+
)
|
|
51
|
+
self.diagnostics: dict[str, list[dict[str, Any]]] = {}
|
|
52
|
+
|
|
53
|
+
def _get_initialize_params(self, repository_absolute_path: str) -> dict[str, Any]:
|
|
54
|
+
root_uri = Path(repository_absolute_path).as_uri()
|
|
55
|
+
return {
|
|
56
|
+
"processId": os.getpid(),
|
|
57
|
+
"rootPath": repository_absolute_path,
|
|
58
|
+
"rootUri": root_uri,
|
|
59
|
+
"workspaceFolders": [
|
|
60
|
+
{
|
|
61
|
+
"uri": root_uri,
|
|
62
|
+
"name": Path(repository_absolute_path).name,
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"capabilities": {
|
|
66
|
+
"workspace": {"workspaceFolders": True},
|
|
67
|
+
"textDocument": {"hover": {"contentFormat": ["markdown", "plaintext"]}},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@asynccontextmanager
|
|
72
|
+
async def start_server(self) -> "AsyncIterator[TyServer]":
|
|
73
|
+
async def do_nothing(params: Any) -> None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
async def window_log_message(msg: Any) -> None:
|
|
77
|
+
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
|
78
|
+
|
|
79
|
+
async def publish_diagnostics(params: dict[str, Any]) -> None:
|
|
80
|
+
uri = params.get("uri")
|
|
81
|
+
if not uri:
|
|
82
|
+
return
|
|
83
|
+
self.diagnostics[uri] = params.get("diagnostics", [])
|
|
84
|
+
|
|
85
|
+
self.server.on_notification("window/logMessage", window_log_message)
|
|
86
|
+
self.server.on_request("workspace/executeClientCommand", do_nothing)
|
|
87
|
+
self.server.on_notification("$/progress", do_nothing)
|
|
88
|
+
self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
|
|
89
|
+
|
|
90
|
+
async with super().start_server():
|
|
91
|
+
self.logger.log("Starting ty language server process", logging.INFO)
|
|
92
|
+
await self.server.start()
|
|
93
|
+
initialize_params = self._get_initialize_params(self.repository_root_path)
|
|
94
|
+
self.logger.log(
|
|
95
|
+
"Sending initialize request from LSP client to ty language server", logging.INFO
|
|
96
|
+
)
|
|
97
|
+
await self.server.send.initialize(initialize_params)
|
|
98
|
+
self.server.notify.initialized({})
|
|
99
|
+
yield self
|
|
100
|
+
await self.server.shutdown()
|
|
101
|
+
await self.server.stop()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_ty_cmd() -> str:
|
|
105
|
+
executable = which("ty")
|
|
106
|
+
if executable is None:
|
|
107
|
+
raise MultilspyException("ty is not available on PATH. Install the 'ty' package.")
|
|
108
|
+
return f"{executable} server"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _allow_all_paths() -> bool:
|
|
112
|
+
return "." in _ALLOWED_DIRS
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _allowed_path_error() -> str:
|
|
116
|
+
if _allow_all_paths():
|
|
117
|
+
return ""
|
|
118
|
+
if _ALLOWED_DIRS and _ALLOWED_FILES:
|
|
119
|
+
allowed_dirs = ", ".join(sorted(_ALLOWED_DIRS))
|
|
120
|
+
allowed_files = ", ".join(sorted(_ALLOWED_FILES))
|
|
121
|
+
return f"Path must live under one of: {allowed_dirs}; or be one of: {allowed_files}."
|
|
122
|
+
if _ALLOWED_DIRS:
|
|
123
|
+
allowed_dirs = ", ".join(sorted(_ALLOWED_DIRS))
|
|
124
|
+
return f"Path must live under {allowed_dirs}."
|
|
125
|
+
if _ALLOWED_FILES:
|
|
126
|
+
allowed_files = ", ".join(sorted(_ALLOWED_FILES))
|
|
127
|
+
return f"Path must be one of: {allowed_files}."
|
|
128
|
+
return "Path is not allowed. Configure _ALLOWED_DIRS or _ALLOWED_FILES."
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _path_is_allowed(relative_path: Path) -> bool:
|
|
132
|
+
if _allow_all_paths():
|
|
133
|
+
return True
|
|
134
|
+
if len(relative_path.parts) == 1:
|
|
135
|
+
return relative_path.name in _ALLOWED_FILES
|
|
136
|
+
return relative_path.parts[0] in _ALLOWED_DIRS
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _resolve_relative_path(file_path: str) -> str:
|
|
140
|
+
path = Path(file_path)
|
|
141
|
+
path = (_REPO_ROOT / path).resolve() if not path.is_absolute() else path.resolve()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
relative_path = path.relative_to(_REPO_ROOT)
|
|
145
|
+
except ValueError as exc: # pragma: no cover - defensive guard
|
|
146
|
+
raise ValueError("Path is outside the repository root.") from exc
|
|
147
|
+
|
|
148
|
+
if not relative_path.parts:
|
|
149
|
+
raise ValueError("Path must point to a file within the repository.")
|
|
150
|
+
|
|
151
|
+
if not _path_is_allowed(relative_path):
|
|
152
|
+
raise ValueError(_allowed_path_error())
|
|
153
|
+
|
|
154
|
+
if not path.exists():
|
|
155
|
+
raise ValueError(f"File not found: {path}")
|
|
156
|
+
|
|
157
|
+
return str(relative_path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _ensure_server() -> TyServer:
|
|
161
|
+
global _server_stack, _server
|
|
162
|
+
if _server is not None and _server.server_started:
|
|
163
|
+
return _server
|
|
164
|
+
|
|
165
|
+
async with _server_lock:
|
|
166
|
+
if _server is not None and _server.server_started:
|
|
167
|
+
return _server
|
|
168
|
+
|
|
169
|
+
config = MultilspyConfig(code_language=Language.PYTHON)
|
|
170
|
+
logger = MultilspyLogger()
|
|
171
|
+
server = TyServer(config, logger, str(_REPO_ROOT))
|
|
172
|
+
stack = AsyncExitStack()
|
|
173
|
+
await stack.enter_async_context(server.start_server())
|
|
174
|
+
_server = server
|
|
175
|
+
_server_stack = stack
|
|
176
|
+
return server
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _format_range(range_data: dict[str, Any] | None) -> str:
|
|
180
|
+
if not range_data:
|
|
181
|
+
return ""
|
|
182
|
+
start = range_data.get("start", {})
|
|
183
|
+
line = start.get("line")
|
|
184
|
+
character = start.get("character")
|
|
185
|
+
if line is None or character is None:
|
|
186
|
+
return ""
|
|
187
|
+
return f"{line + 1}:{character + 1}"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _uri_to_relative(uri: str | None) -> str:
|
|
191
|
+
if not uri:
|
|
192
|
+
return ""
|
|
193
|
+
if uri.startswith("file:"):
|
|
194
|
+
parsed = urlparse(uri)
|
|
195
|
+
path = Path(parsed.path)
|
|
196
|
+
try:
|
|
197
|
+
return str(path.relative_to(_REPO_ROOT))
|
|
198
|
+
except ValueError:
|
|
199
|
+
return str(path)
|
|
200
|
+
return uri
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _format_locations(locations: list[dict[str, Any]]) -> str:
|
|
204
|
+
if not locations:
|
|
205
|
+
return "No locations returned."
|
|
206
|
+
|
|
207
|
+
lines = ["| path | line |", "| --- | --- |"]
|
|
208
|
+
for location in locations:
|
|
209
|
+
path = (
|
|
210
|
+
location.get("relativePath")
|
|
211
|
+
or location.get("absolutePath")
|
|
212
|
+
or _uri_to_relative(location.get("uri"))
|
|
213
|
+
)
|
|
214
|
+
line = _format_range(location.get("range"))
|
|
215
|
+
lines.append(f"| {path} | {line} |")
|
|
216
|
+
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _format_hover_contents(contents: Any) -> str:
|
|
220
|
+
if contents is None:
|
|
221
|
+
return "No hover contents returned."
|
|
222
|
+
if isinstance(contents, str):
|
|
223
|
+
return contents
|
|
224
|
+
if isinstance(contents, list):
|
|
225
|
+
return "\n\n".join(_format_hover_contents(item) for item in contents)
|
|
226
|
+
if isinstance(contents, dict):
|
|
227
|
+
value = contents.get("value")
|
|
228
|
+
if isinstance(value, str):
|
|
229
|
+
return value
|
|
230
|
+
return json.dumps(contents, indent=2)
|
|
231
|
+
return str(contents)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _format_symbol_kind(kind: Any) -> str:
|
|
235
|
+
if not isinstance(kind, int):
|
|
236
|
+
return str(kind or "")
|
|
237
|
+
return {
|
|
238
|
+
1: "File",
|
|
239
|
+
2: "Module",
|
|
240
|
+
3: "Namespace",
|
|
241
|
+
4: "Package",
|
|
242
|
+
5: "Class",
|
|
243
|
+
6: "Method",
|
|
244
|
+
7: "Property",
|
|
245
|
+
8: "Field",
|
|
246
|
+
9: "Constructor",
|
|
247
|
+
10: "Enum",
|
|
248
|
+
11: "Interface",
|
|
249
|
+
12: "Function",
|
|
250
|
+
13: "Variable",
|
|
251
|
+
14: "Constant",
|
|
252
|
+
15: "String",
|
|
253
|
+
16: "Number",
|
|
254
|
+
17: "Boolean",
|
|
255
|
+
18: "Array",
|
|
256
|
+
19: "Object",
|
|
257
|
+
20: "Key",
|
|
258
|
+
21: "Null",
|
|
259
|
+
22: "EnumMember",
|
|
260
|
+
23: "Struct",
|
|
261
|
+
24: "Event",
|
|
262
|
+
25: "Operator",
|
|
263
|
+
26: "TypeParameter",
|
|
264
|
+
}.get(kind, str(kind))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _format_symbols(symbols: list[dict[str, Any]], default_path: str | None = None) -> str:
|
|
268
|
+
if not symbols:
|
|
269
|
+
return "No symbols returned."
|
|
270
|
+
lines = ["| name | kind | location | detail |", "| --- | --- | --- | --- |"]
|
|
271
|
+
for symbol in symbols:
|
|
272
|
+
location = symbol.get("location") or {}
|
|
273
|
+
path = (
|
|
274
|
+
location.get("relativePath")
|
|
275
|
+
or location.get("absolutePath")
|
|
276
|
+
or _uri_to_relative(location.get("uri"))
|
|
277
|
+
or ""
|
|
278
|
+
)
|
|
279
|
+
if not path and default_path:
|
|
280
|
+
path = default_path
|
|
281
|
+
range_data = location.get("range") or symbol.get("range") or symbol.get("selectionRange")
|
|
282
|
+
line = _format_range(range_data)
|
|
283
|
+
location_display = f"{path} ({line})" if path and line else path
|
|
284
|
+
lines.append(
|
|
285
|
+
"| {name} | {kind} | {location} | {detail} |".format(
|
|
286
|
+
name=symbol.get("name", ""),
|
|
287
|
+
kind=_format_symbol_kind(symbol.get("kind")),
|
|
288
|
+
location=location_display,
|
|
289
|
+
detail=symbol.get("detail", "") or "",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
return "\n".join(lines)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _is_content_modified_error(exc: Exception) -> bool:
|
|
296
|
+
message = str(exc).lower()
|
|
297
|
+
return "content modified" in message or "-32801" in message
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
async def _retry_on_content_modified(operation: Callable[[], Awaitable[_ReturnT]]) -> _ReturnT:
|
|
301
|
+
for attempt in range(_CONTENT_MODIFIED_RETRY_ATTEMPTS + 1):
|
|
302
|
+
try:
|
|
303
|
+
return await operation()
|
|
304
|
+
except asyncio.CancelledError:
|
|
305
|
+
raise
|
|
306
|
+
except Exception as exc:
|
|
307
|
+
if attempt == _CONTENT_MODIFIED_RETRY_ATTEMPTS or not _is_content_modified_error(exc):
|
|
308
|
+
raise
|
|
309
|
+
await asyncio.sleep(_CONTENT_MODIFIED_BASE_DELAY_SECONDS * (2**attempt))
|
|
310
|
+
raise RuntimeError("Retry loop exhausted unexpectedly.")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def lsp_hover(file_path: str, line: int, character: int) -> str:
|
|
314
|
+
"""Return hover information for a symbol at the given location."""
|
|
315
|
+
try:
|
|
316
|
+
relative_path = _resolve_relative_path(file_path)
|
|
317
|
+
server = await _ensure_server()
|
|
318
|
+
hover = await _retry_on_content_modified(
|
|
319
|
+
lambda: server.request_hover(relative_path, line, character)
|
|
320
|
+
)
|
|
321
|
+
if not hover:
|
|
322
|
+
return "No hover information returned."
|
|
323
|
+
return _format_hover_contents(hover.get("contents"))
|
|
324
|
+
except (ValueError, MultilspyException) as exc:
|
|
325
|
+
return f"Error: {exc}"
|
|
326
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
327
|
+
return f"Error: {exc}"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async def lsp_definition(file_path: str, line: int, character: int) -> str:
|
|
331
|
+
"""Return definition locations for a symbol at the given location."""
|
|
332
|
+
try:
|
|
333
|
+
relative_path = _resolve_relative_path(file_path)
|
|
334
|
+
server = await _ensure_server()
|
|
335
|
+
locations = await _retry_on_content_modified(
|
|
336
|
+
lambda: server.request_definition(relative_path, line, character)
|
|
337
|
+
)
|
|
338
|
+
if not locations:
|
|
339
|
+
return "No locations returned."
|
|
340
|
+
return _format_locations([dict(location) for location in locations])
|
|
341
|
+
except (ValueError, MultilspyException) as exc:
|
|
342
|
+
message = str(exc)
|
|
343
|
+
if "Unexpected response from Language Server" in message:
|
|
344
|
+
return "No locations returned."
|
|
345
|
+
return f"Error: {exc}"
|
|
346
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
347
|
+
message = str(exc)
|
|
348
|
+
if "Unexpected response from Language Server" in message:
|
|
349
|
+
return "No locations returned."
|
|
350
|
+
return f"Error: {exc}"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def lsp_references(file_path: str, line: int, character: int) -> str:
|
|
354
|
+
"""Return reference locations for a symbol at the given location."""
|
|
355
|
+
try:
|
|
356
|
+
relative_path = _resolve_relative_path(file_path)
|
|
357
|
+
server = await _ensure_server()
|
|
358
|
+
locations = await _retry_on_content_modified(
|
|
359
|
+
lambda: server.request_references(relative_path, line, character)
|
|
360
|
+
)
|
|
361
|
+
if not locations:
|
|
362
|
+
return "No locations returned."
|
|
363
|
+
return _format_locations([dict(location) for location in locations])
|
|
364
|
+
except (ValueError, MultilspyException) as exc:
|
|
365
|
+
message = str(exc)
|
|
366
|
+
if "Unexpected response from Language Server" in message:
|
|
367
|
+
return "No locations returned."
|
|
368
|
+
return f"Error: {exc}"
|
|
369
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
370
|
+
message = str(exc)
|
|
371
|
+
if "Unexpected response from Language Server" in message:
|
|
372
|
+
return "No locations returned."
|
|
373
|
+
return f"Error: {exc}"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def lsp_document_symbols(file_path: str) -> str:
|
|
377
|
+
"""Return document symbols for a file."""
|
|
378
|
+
try:
|
|
379
|
+
relative_path = _resolve_relative_path(file_path)
|
|
380
|
+
server = await _ensure_server()
|
|
381
|
+
symbols, _ = await _retry_on_content_modified(
|
|
382
|
+
lambda: server.request_document_symbols(relative_path)
|
|
383
|
+
)
|
|
384
|
+
return _format_symbols([dict(symbol) for symbol in symbols], default_path=relative_path)
|
|
385
|
+
except (ValueError, MultilspyException) as exc:
|
|
386
|
+
return f"Error: {exc}"
|
|
387
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
388
|
+
return f"Error: {exc}"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
async def lsp_workspace_symbols(query: str) -> str:
|
|
392
|
+
"""Return workspace symbols matching a query string."""
|
|
393
|
+
try:
|
|
394
|
+
server = await _ensure_server()
|
|
395
|
+
symbols = await _retry_on_content_modified(lambda: server.request_workspace_symbol(query))
|
|
396
|
+
if symbols is None:
|
|
397
|
+
return "No symbols returned."
|
|
398
|
+
return _format_symbols([dict(symbol) for symbol in symbols])
|
|
399
|
+
except (ValueError, MultilspyException) as exc:
|
|
400
|
+
return f"Error: {exc}"
|
|
401
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
402
|
+
return f"Error: {exc}"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
async def lsp_diagnostics(file_path: str | None = None) -> str:
|
|
406
|
+
"""Return cached diagnostics from ty server."""
|
|
407
|
+
try:
|
|
408
|
+
server = await _ensure_server()
|
|
409
|
+
if file_path is None:
|
|
410
|
+
diagnostics = server.diagnostics
|
|
411
|
+
else:
|
|
412
|
+
relative_path = _resolve_relative_path(file_path)
|
|
413
|
+
uri = Path(_REPO_ROOT / relative_path).as_uri()
|
|
414
|
+
diagnostics = {uri: server.diagnostics.get(uri, [])}
|
|
415
|
+
if not diagnostics:
|
|
416
|
+
return "No diagnostics cached."
|
|
417
|
+
return json.dumps(diagnostics, indent=2)
|
|
418
|
+
except (ValueError, MultilspyException) as exc:
|
|
419
|
+
return f"Error: {exc}"
|
|
420
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
421
|
+
return f"Error: {exc}"
|