agentpool 2.1.9__py3-none-any.whl → 2.5.0__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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1593 @@
|
|
|
1
|
+
"""LSP Manager - Orchestrates LSP servers across execution environments.
|
|
2
|
+
|
|
3
|
+
This module manages LSP server lifecycle:
|
|
4
|
+
- Starting LSP proxy processes via the execution environment
|
|
5
|
+
- Tracking running servers by language/server-id
|
|
6
|
+
- Sending requests via one-shot Python scripts
|
|
7
|
+
- Handling initialization handshake
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import contextlib
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
import time
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import anyenv
|
|
19
|
+
|
|
20
|
+
from agentpool.diagnostics.models import (
|
|
21
|
+
COMPLETION_KIND_MAP,
|
|
22
|
+
SYMBOL_KIND_MAP,
|
|
23
|
+
CallHierarchyCall,
|
|
24
|
+
CallHierarchyItem,
|
|
25
|
+
CodeAction,
|
|
26
|
+
CompletionItem,
|
|
27
|
+
Diagnostic,
|
|
28
|
+
DiagnosticsResult,
|
|
29
|
+
HoverInfo,
|
|
30
|
+
Location,
|
|
31
|
+
LSPServerState,
|
|
32
|
+
Position,
|
|
33
|
+
Range,
|
|
34
|
+
RenameResult,
|
|
35
|
+
SignatureInfo,
|
|
36
|
+
SymbolInfo,
|
|
37
|
+
TypeHierarchyItem,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from anyenv.lsp_servers import LSPServerInfo
|
|
43
|
+
from exxec import ExecutionEnvironment
|
|
44
|
+
|
|
45
|
+
from agentpool.diagnostics.models import (
|
|
46
|
+
HoverContents,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# One-shot script template for sending LSP requests via TCP
|
|
51
|
+
LSP_CLIENT_SCRIPT = '''
|
|
52
|
+
"""One-shot LSP client - sends request and prints JSON response."""
|
|
53
|
+
import asyncio
|
|
54
|
+
import json
|
|
55
|
+
|
|
56
|
+
PORT = {port!r}
|
|
57
|
+
METHOD = {method!r}
|
|
58
|
+
PARAMS = {params!r}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def send_request() -> None:
|
|
62
|
+
"""Connect to LSP proxy via TCP, send request, print response."""
|
|
63
|
+
try:
|
|
64
|
+
reader, writer = await asyncio.open_connection("127.0.0.1", PORT)
|
|
65
|
+
|
|
66
|
+
# Build JSON-RPC request
|
|
67
|
+
request = {{"jsonrpc": "2.0", "id": 1, "method": METHOD, "params": PARAMS}}
|
|
68
|
+
payload = json.dumps(request)
|
|
69
|
+
header = f"Content-Length: {{len(payload)}}\\r\\n\\r\\n"
|
|
70
|
+
|
|
71
|
+
# Send request
|
|
72
|
+
writer.write(header.encode() + payload.encode())
|
|
73
|
+
await writer.drain()
|
|
74
|
+
|
|
75
|
+
# Read response headers
|
|
76
|
+
headers = b""
|
|
77
|
+
while b"\\r\\n\\r\\n" not in headers:
|
|
78
|
+
chunk = await reader.read(1)
|
|
79
|
+
if not chunk:
|
|
80
|
+
print(json.dumps({{"error": "Connection closed"}}))
|
|
81
|
+
return
|
|
82
|
+
headers += chunk
|
|
83
|
+
|
|
84
|
+
# Parse Content-Length
|
|
85
|
+
header_str = headers.decode()
|
|
86
|
+
length = None
|
|
87
|
+
for line in header_str.split("\\r\\n"):
|
|
88
|
+
if line.startswith("Content-Length:"):
|
|
89
|
+
length = int(line.split(":")[1].strip())
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
if length is None:
|
|
93
|
+
print(json.dumps({{"error": "No Content-Length header"}}))
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Read response body
|
|
97
|
+
body = await reader.read(length)
|
|
98
|
+
response = json.loads(body)
|
|
99
|
+
|
|
100
|
+
# Print result (or error)
|
|
101
|
+
print(json.dumps(response))
|
|
102
|
+
|
|
103
|
+
writer.close()
|
|
104
|
+
await writer.wait_closed()
|
|
105
|
+
|
|
106
|
+
except ConnectionRefusedError:
|
|
107
|
+
print(json.dumps({{"error": f"Connection refused on port {{PORT}}"}}))
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(json.dumps({{"error": str(e)}}))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
asyncio.run(send_request())
|
|
113
|
+
'''
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class LSPManager:
|
|
118
|
+
"""Manages LSP servers for an execution environment.
|
|
119
|
+
|
|
120
|
+
Handles:
|
|
121
|
+
- Starting LSP proxy processes that wrap stdio-based servers
|
|
122
|
+
- Tracking running servers by ID
|
|
123
|
+
- Sending requests via one-shot scripts or direct sockets
|
|
124
|
+
- LSP initialization handshake
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
env: ExecutionEnvironment
|
|
128
|
+
"""The execution environment to run servers in."""
|
|
129
|
+
|
|
130
|
+
port_file_dir: str = "/tmp/lsp-ports"
|
|
131
|
+
"""Directory for port files."""
|
|
132
|
+
|
|
133
|
+
_servers: dict[str, LSPServerState] = field(default_factory=dict)
|
|
134
|
+
"""Running servers by server_id."""
|
|
135
|
+
|
|
136
|
+
_server_configs: dict[str, LSPServerInfo] = field(default_factory=dict)
|
|
137
|
+
"""Server configurations by server_id."""
|
|
138
|
+
|
|
139
|
+
_starting: set[str] = field(default_factory=set)
|
|
140
|
+
"""Server IDs currently being started (to prevent concurrent start attempts)."""
|
|
141
|
+
|
|
142
|
+
def __post_init__(self) -> None:
|
|
143
|
+
"""Initialize internal state."""
|
|
144
|
+
# dataclass default_factory doesn't work with mutable defaults in __init__
|
|
145
|
+
if not hasattr(self, "_servers") or self._servers is None:
|
|
146
|
+
self._servers = {}
|
|
147
|
+
if not hasattr(self, "_server_configs") or self._server_configs is None:
|
|
148
|
+
self._server_configs = {}
|
|
149
|
+
if not hasattr(self, "_starting") or self._starting is None:
|
|
150
|
+
self._starting = set()
|
|
151
|
+
|
|
152
|
+
def register_server(self, config: LSPServerInfo) -> None:
|
|
153
|
+
"""Register an LSP server configuration.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
config: LSP server configuration from anyenv
|
|
157
|
+
"""
|
|
158
|
+
self._server_configs[config.id] = config
|
|
159
|
+
|
|
160
|
+
def register_defaults(self) -> None:
|
|
161
|
+
"""Register all default LSP servers from anyenv."""
|
|
162
|
+
from anyenv.lsp_servers import LSPServerRegistry
|
|
163
|
+
|
|
164
|
+
registry = LSPServerRegistry()
|
|
165
|
+
registry.register_defaults()
|
|
166
|
+
for server in registry.all_servers:
|
|
167
|
+
self.register_server(server)
|
|
168
|
+
|
|
169
|
+
def get_server_for_file(self, path: str) -> LSPServerInfo | None:
|
|
170
|
+
"""Get the best LSP server for a file path.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
path: File path to check
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
LSPServerInfo if found, None otherwise
|
|
177
|
+
"""
|
|
178
|
+
import posixpath
|
|
179
|
+
|
|
180
|
+
ext = posixpath.splitext(path)[1].lower()
|
|
181
|
+
for config in self._server_configs.values():
|
|
182
|
+
if config.can_handle(ext):
|
|
183
|
+
return config
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def is_running(self, server_id: str) -> bool:
|
|
187
|
+
"""Check if a server is currently running or starting.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
server_id: Server identifier
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if server is running or being started
|
|
194
|
+
"""
|
|
195
|
+
if server_id in self._starting:
|
|
196
|
+
return True # Treat "starting" as running to prevent concurrent starts
|
|
197
|
+
return server_id in self._servers and self._servers[server_id].initialized
|
|
198
|
+
|
|
199
|
+
async def start_server(
|
|
200
|
+
self,
|
|
201
|
+
server_id: str,
|
|
202
|
+
root_uri: str | None = None,
|
|
203
|
+
) -> LSPServerState:
|
|
204
|
+
"""Start an LSP server.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
server_id: Server identifier (e.g., 'pyright', 'rust-analyzer')
|
|
208
|
+
root_uri: Workspace root URI (e.g., 'file:///path/to/project')
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
LSPServerState with server info
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If server_id not registered
|
|
215
|
+
RuntimeError: If server fails to start
|
|
216
|
+
"""
|
|
217
|
+
if server_id in self._servers:
|
|
218
|
+
return self._servers[server_id]
|
|
219
|
+
|
|
220
|
+
# Check if already being started (prevent concurrent starts)
|
|
221
|
+
if server_id in self._starting:
|
|
222
|
+
# Wait for the other start to complete
|
|
223
|
+
for _ in range(100): # 10 second timeout
|
|
224
|
+
await asyncio.sleep(0.1)
|
|
225
|
+
if server_id in self._servers:
|
|
226
|
+
return self._servers[server_id]
|
|
227
|
+
msg = f"Timeout waiting for {server_id} to start"
|
|
228
|
+
raise RuntimeError(msg)
|
|
229
|
+
|
|
230
|
+
config = self._server_configs.get(server_id)
|
|
231
|
+
if not config:
|
|
232
|
+
msg = f"Server {server_id!r} not registered"
|
|
233
|
+
raise ValueError(msg)
|
|
234
|
+
|
|
235
|
+
# Mark as starting to prevent concurrent attempts
|
|
236
|
+
self._starting.add(server_id)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
# Build the command
|
|
240
|
+
command = config.get_full_command()
|
|
241
|
+
command_str = " ".join(command)
|
|
242
|
+
|
|
243
|
+
# Port file path for this server
|
|
244
|
+
port_file = f"{self.port_file_dir}/{server_id}.port"
|
|
245
|
+
|
|
246
|
+
# Start the LSP proxy process
|
|
247
|
+
from agentpool.diagnostics.lsp_proxy import LSPProxy
|
|
248
|
+
|
|
249
|
+
proxy_cmd = LSPProxy.get_start_command(command_str, port_file)
|
|
250
|
+
|
|
251
|
+
# Ensure port file directory exists
|
|
252
|
+
await self.env.execute_command(f"mkdir -p {self.port_file_dir}")
|
|
253
|
+
|
|
254
|
+
# Start proxy as background process
|
|
255
|
+
process_id = await self.env.process_manager.start_process(
|
|
256
|
+
command=proxy_cmd[0],
|
|
257
|
+
args=proxy_cmd[1:],
|
|
258
|
+
env=config.get_env(),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Wait for server to be ready (check for .ready marker file)
|
|
262
|
+
ready_path = f"{port_file}.ready"
|
|
263
|
+
for _ in range(50): # 5 second timeout
|
|
264
|
+
result = await self.env.execute_command(f"test -f {ready_path} && echo ready")
|
|
265
|
+
if result.stdout and "ready" in result.stdout:
|
|
266
|
+
break
|
|
267
|
+
await asyncio.sleep(0.1)
|
|
268
|
+
else:
|
|
269
|
+
# Cleanup on failure
|
|
270
|
+
await self.env.process_manager.kill_process(process_id)
|
|
271
|
+
msg = f"LSP proxy for {server_id} failed to start (not ready)"
|
|
272
|
+
raise RuntimeError(msg)
|
|
273
|
+
|
|
274
|
+
# Read the port from the port file
|
|
275
|
+
port_result = await self.env.execute_command(f"cat {port_file}")
|
|
276
|
+
if not port_result.stdout or port_result.exit_code != 0:
|
|
277
|
+
await self.env.process_manager.kill_process(process_id)
|
|
278
|
+
msg = f"LSP proxy for {server_id} failed to start (no port file)"
|
|
279
|
+
raise RuntimeError(msg)
|
|
280
|
+
port = int(port_result.stdout.strip())
|
|
281
|
+
|
|
282
|
+
# Create server state
|
|
283
|
+
state = LSPServerState(
|
|
284
|
+
server_id=server_id,
|
|
285
|
+
process_id=process_id,
|
|
286
|
+
port=port,
|
|
287
|
+
language=config.extensions[0] if config.extensions else "unknown",
|
|
288
|
+
root_uri=root_uri,
|
|
289
|
+
initialized=False,
|
|
290
|
+
)
|
|
291
|
+
self._servers[server_id] = state
|
|
292
|
+
|
|
293
|
+
# Run LSP initialize handshake
|
|
294
|
+
try:
|
|
295
|
+
await self._initialize_server(state, config, root_uri)
|
|
296
|
+
except Exception:
|
|
297
|
+
# Initialization failed - remove from servers dict and re-raise
|
|
298
|
+
self._servers.pop(server_id, None)
|
|
299
|
+
# Kill the process
|
|
300
|
+
await self.env.process_manager.kill_process(process_id)
|
|
301
|
+
raise
|
|
302
|
+
|
|
303
|
+
return state
|
|
304
|
+
finally:
|
|
305
|
+
# Always remove from starting set
|
|
306
|
+
self._starting.discard(server_id)
|
|
307
|
+
|
|
308
|
+
async def _initialize_server(
|
|
309
|
+
self,
|
|
310
|
+
state: LSPServerState,
|
|
311
|
+
config: LSPServerInfo,
|
|
312
|
+
root_uri: str | None,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Run LSP initialize/initialized handshake.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
state: Server state to update
|
|
318
|
+
config: Server configuration
|
|
319
|
+
root_uri: Workspace root URI
|
|
320
|
+
"""
|
|
321
|
+
# Get initialization options
|
|
322
|
+
init_options = dict(config.initialization)
|
|
323
|
+
|
|
324
|
+
# Build initialize params
|
|
325
|
+
init_params: dict[str, Any] = {
|
|
326
|
+
"processId": None, # We don't have a process ID in the traditional sense
|
|
327
|
+
"rootUri": root_uri,
|
|
328
|
+
"capabilities": {
|
|
329
|
+
"textDocument": {
|
|
330
|
+
"publishDiagnostics": {
|
|
331
|
+
"relatedInformation": True,
|
|
332
|
+
"tagSupport": {"valueSet": [1, 2]},
|
|
333
|
+
},
|
|
334
|
+
"synchronization": {
|
|
335
|
+
"dynamicRegistration": False,
|
|
336
|
+
"willSave": False,
|
|
337
|
+
"willSaveWaitUntil": False,
|
|
338
|
+
"didSave": True,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
"workspace": {
|
|
342
|
+
"workspaceFolders": True,
|
|
343
|
+
"configuration": True,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
"initializationOptions": init_options,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if root_uri:
|
|
350
|
+
init_params["workspaceFolders"] = [{"uri": root_uri, "name": "workspace"}]
|
|
351
|
+
|
|
352
|
+
# Send initialize request
|
|
353
|
+
response = await self._send_request(state.port, "initialize", init_params)
|
|
354
|
+
|
|
355
|
+
if "error" in response:
|
|
356
|
+
msg = f"LSP initialize failed: {response['error']}"
|
|
357
|
+
raise RuntimeError(msg)
|
|
358
|
+
|
|
359
|
+
# Store capabilities
|
|
360
|
+
if "result" in response:
|
|
361
|
+
state.capabilities = response["result"].get("capabilities", {})
|
|
362
|
+
|
|
363
|
+
# Send initialized notification (no response expected)
|
|
364
|
+
await self._send_notification(state.port, "initialized", {})
|
|
365
|
+
|
|
366
|
+
state.initialized = True
|
|
367
|
+
|
|
368
|
+
async def _send_request(
|
|
369
|
+
self,
|
|
370
|
+
port: int,
|
|
371
|
+
method: str,
|
|
372
|
+
params: dict[str, Any],
|
|
373
|
+
retries: int = 3,
|
|
374
|
+
) -> dict[str, Any]:
|
|
375
|
+
"""Send an LSP request via execution environment.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
port: TCP port to connect to
|
|
379
|
+
method: LSP method name
|
|
380
|
+
params: Method parameters
|
|
381
|
+
retries: Number of retries for transient failures
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
JSON-RPC response dict
|
|
385
|
+
"""
|
|
386
|
+
# Generate client script
|
|
387
|
+
script = LSP_CLIENT_SCRIPT.format(
|
|
388
|
+
port=port,
|
|
389
|
+
method=method,
|
|
390
|
+
params=params,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Execute via environment with retries for connection refused
|
|
394
|
+
cmd = f"python3 -c {_shell_quote(script)}"
|
|
395
|
+
|
|
396
|
+
last_result = None
|
|
397
|
+
for attempt in range(retries):
|
|
398
|
+
result = await self.env.execute_command(cmd)
|
|
399
|
+
last_result = result
|
|
400
|
+
|
|
401
|
+
if result.exit_code != 0:
|
|
402
|
+
return {"error": f"Script failed: {result.stderr}"}
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
response: dict[str, Any] = anyenv.load_json(result.stdout or "{}", return_type=dict)
|
|
406
|
+
# Check if it's a connection refused error - retry
|
|
407
|
+
if (
|
|
408
|
+
"error" in response
|
|
409
|
+
and "Connection refused" in str(response["error"])
|
|
410
|
+
and attempt < retries - 1
|
|
411
|
+
):
|
|
412
|
+
await asyncio.sleep(0.5)
|
|
413
|
+
continue
|
|
414
|
+
except anyenv.JsonLoadError as e:
|
|
415
|
+
return {"error": f"Invalid JSON response: {e}"}
|
|
416
|
+
else:
|
|
417
|
+
return response
|
|
418
|
+
|
|
419
|
+
# Should not reach here, but return last result error if we do
|
|
420
|
+
return {"error": f"Failed after {retries} retries: {last_result}"}
|
|
421
|
+
|
|
422
|
+
async def _send_notification(
|
|
423
|
+
self,
|
|
424
|
+
port: int,
|
|
425
|
+
method: str,
|
|
426
|
+
params: dict[str, Any],
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Send an LSP notification (no response expected).
|
|
429
|
+
|
|
430
|
+
For now, we use the same mechanism as requests but ignore the response.
|
|
431
|
+
"""
|
|
432
|
+
# For notifications, we'd need a different script that doesn't wait
|
|
433
|
+
# For simplicity, we'll skip actual notification sending for now
|
|
434
|
+
# The initialize/initialized handshake works without waiting for response
|
|
435
|
+
|
|
436
|
+
async def stop_server(self, server_id: str) -> None:
|
|
437
|
+
"""Stop an LSP server.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
server_id: Server identifier
|
|
441
|
+
"""
|
|
442
|
+
if server_id not in self._servers:
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
state = self._servers[server_id]
|
|
446
|
+
|
|
447
|
+
# Send shutdown request
|
|
448
|
+
with contextlib.suppress(Exception):
|
|
449
|
+
await self._send_request(state.port, "shutdown", {})
|
|
450
|
+
|
|
451
|
+
# Kill the process
|
|
452
|
+
with contextlib.suppress(Exception):
|
|
453
|
+
await self.env.process_manager.kill_process(state.process_id)
|
|
454
|
+
|
|
455
|
+
# Cleanup port files
|
|
456
|
+
port_file = f"{self.port_file_dir}/{server_id}.port"
|
|
457
|
+
with contextlib.suppress(Exception):
|
|
458
|
+
await self.env.execute_command(f"rm -f {port_file} {port_file}.ready")
|
|
459
|
+
|
|
460
|
+
del self._servers[server_id]
|
|
461
|
+
|
|
462
|
+
async def stop_all(self) -> None:
|
|
463
|
+
"""Stop all running LSP servers."""
|
|
464
|
+
server_ids = list(self._servers.keys())
|
|
465
|
+
for server_id in server_ids:
|
|
466
|
+
await self.stop_server(server_id)
|
|
467
|
+
|
|
468
|
+
async def get_diagnostics(
|
|
469
|
+
self,
|
|
470
|
+
server_id: str,
|
|
471
|
+
file_uri: str,
|
|
472
|
+
content: str,
|
|
473
|
+
) -> DiagnosticsResult:
|
|
474
|
+
"""Get diagnostics for a file.
|
|
475
|
+
|
|
476
|
+
This opens the file in the LSP server, waits for diagnostics,
|
|
477
|
+
and returns them.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
server_id: Server identifier
|
|
481
|
+
file_uri: File URI (e.g., 'file:///path/to/file.py')
|
|
482
|
+
content: File content
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
DiagnosticsResult with parsed diagnostics
|
|
486
|
+
"""
|
|
487
|
+
start_time = time.perf_counter()
|
|
488
|
+
|
|
489
|
+
if server_id not in self._servers:
|
|
490
|
+
return DiagnosticsResult(
|
|
491
|
+
success=False,
|
|
492
|
+
error=f"Server {server_id} not running",
|
|
493
|
+
duration=time.perf_counter() - start_time,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
state = self._servers[server_id]
|
|
497
|
+
|
|
498
|
+
# Send textDocument/didOpen
|
|
499
|
+
open_params = {
|
|
500
|
+
"textDocument": {
|
|
501
|
+
"uri": file_uri,
|
|
502
|
+
"languageId": _uri_to_language_id(file_uri),
|
|
503
|
+
"version": 1,
|
|
504
|
+
"text": content,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
await self._send_notification(state.port, "textDocument/didOpen", open_params)
|
|
508
|
+
|
|
509
|
+
# For proper diagnostic collection, we'd need to listen for
|
|
510
|
+
# textDocument/publishDiagnostics notifications from the server.
|
|
511
|
+
# This requires a more complex architecture with persistent connections.
|
|
512
|
+
#
|
|
513
|
+
# For now, we'll use the CLI fallback approach which is more reliable
|
|
514
|
+
# for one-shot diagnostic runs.
|
|
515
|
+
|
|
516
|
+
return DiagnosticsResult(
|
|
517
|
+
success=True,
|
|
518
|
+
diagnostics=[],
|
|
519
|
+
duration=time.perf_counter() - start_time,
|
|
520
|
+
server_id=server_id,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
async def run_cli_diagnostics(
|
|
524
|
+
self,
|
|
525
|
+
server_id: str,
|
|
526
|
+
files: list[str],
|
|
527
|
+
) -> DiagnosticsResult:
|
|
528
|
+
"""Run CLI diagnostics using the server's CLI fallback.
|
|
529
|
+
|
|
530
|
+
This is more reliable for one-shot diagnostic runs than the full
|
|
531
|
+
LSP protocol, as it doesn't require persistent connections.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
server_id: Server identifier
|
|
535
|
+
files: File paths to check
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
DiagnosticsResult with parsed diagnostics
|
|
539
|
+
"""
|
|
540
|
+
start_time = time.perf_counter()
|
|
541
|
+
|
|
542
|
+
config = self._server_configs.get(server_id)
|
|
543
|
+
if not config:
|
|
544
|
+
return DiagnosticsResult(
|
|
545
|
+
success=False,
|
|
546
|
+
error=f"Server {server_id} not registered",
|
|
547
|
+
duration=time.perf_counter() - start_time,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if not config.has_cli_diagnostics:
|
|
551
|
+
return DiagnosticsResult(
|
|
552
|
+
success=False,
|
|
553
|
+
error=f"Server {server_id} has no CLI diagnostic support",
|
|
554
|
+
duration=time.perf_counter() - start_time,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Build and run the diagnostic command
|
|
558
|
+
command = config.build_diagnostic_command(files)
|
|
559
|
+
result = await self.env.execute_command(command)
|
|
560
|
+
|
|
561
|
+
# Parse the output
|
|
562
|
+
diagnostics = config.parse_diagnostics(result.stdout or "", result.stderr or "")
|
|
563
|
+
|
|
564
|
+
return DiagnosticsResult(
|
|
565
|
+
diagnostics=[_convert_diagnostic(d, server_id) for d in diagnostics],
|
|
566
|
+
success=True,
|
|
567
|
+
duration=time.perf_counter() - start_time,
|
|
568
|
+
server_id=server_id,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# =========================================================================
|
|
572
|
+
# Document Operations
|
|
573
|
+
# =========================================================================
|
|
574
|
+
|
|
575
|
+
async def hover(
|
|
576
|
+
self,
|
|
577
|
+
server_id: str,
|
|
578
|
+
file_uri: str,
|
|
579
|
+
line: int,
|
|
580
|
+
character: int,
|
|
581
|
+
) -> HoverInfo | None:
|
|
582
|
+
"""Get hover information at a position.
|
|
583
|
+
|
|
584
|
+
Returns type information, documentation, and other details
|
|
585
|
+
for the symbol at the given position.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
server_id: Server identifier
|
|
589
|
+
file_uri: File URI (e.g., 'file:///path/to/file.py')
|
|
590
|
+
line: 0-based line number
|
|
591
|
+
character: 0-based character offset
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
HoverInfo if available, None otherwise
|
|
595
|
+
"""
|
|
596
|
+
if server_id not in self._servers:
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
state = self._servers[server_id]
|
|
600
|
+
params = {
|
|
601
|
+
"textDocument": {"uri": file_uri},
|
|
602
|
+
"position": {"line": line, "character": character},
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
response = await self._send_request(state.port, "textDocument/hover", params)
|
|
606
|
+
|
|
607
|
+
if "error" in response or not response.get("result"):
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
result = response["result"]
|
|
611
|
+
contents = _extract_hover_contents(result.get("contents", ""))
|
|
612
|
+
range_ = _parse_range(result.get("range")) if result.get("range") else None
|
|
613
|
+
|
|
614
|
+
return HoverInfo(contents=contents, range=range_)
|
|
615
|
+
|
|
616
|
+
async def goto_definition(
|
|
617
|
+
self,
|
|
618
|
+
server_id: str,
|
|
619
|
+
file_uri: str,
|
|
620
|
+
line: int,
|
|
621
|
+
character: int,
|
|
622
|
+
) -> list[Location]:
|
|
623
|
+
"""Go to definition of symbol at position.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
server_id: Server identifier
|
|
627
|
+
file_uri: File URI
|
|
628
|
+
line: 0-based line number
|
|
629
|
+
character: 0-based character offset
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
List of definition locations
|
|
633
|
+
"""
|
|
634
|
+
if server_id not in self._servers:
|
|
635
|
+
return []
|
|
636
|
+
|
|
637
|
+
state = self._servers[server_id]
|
|
638
|
+
params = {
|
|
639
|
+
"textDocument": {"uri": file_uri},
|
|
640
|
+
"position": {"line": line, "character": character},
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
response = await self._send_request(state.port, "textDocument/definition", params)
|
|
644
|
+
|
|
645
|
+
if "error" in response or not response.get("result"):
|
|
646
|
+
return []
|
|
647
|
+
|
|
648
|
+
return _parse_locations(response["result"])
|
|
649
|
+
|
|
650
|
+
async def goto_type_definition(
|
|
651
|
+
self,
|
|
652
|
+
server_id: str,
|
|
653
|
+
file_uri: str,
|
|
654
|
+
line: int,
|
|
655
|
+
character: int,
|
|
656
|
+
) -> list[Location]:
|
|
657
|
+
"""Go to type definition of symbol at position.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
server_id: Server identifier
|
|
661
|
+
file_uri: File URI
|
|
662
|
+
line: 0-based line number
|
|
663
|
+
character: 0-based character offset
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
List of type definition locations
|
|
667
|
+
"""
|
|
668
|
+
if server_id not in self._servers:
|
|
669
|
+
return []
|
|
670
|
+
|
|
671
|
+
state = self._servers[server_id]
|
|
672
|
+
params = {
|
|
673
|
+
"textDocument": {"uri": file_uri},
|
|
674
|
+
"position": {"line": line, "character": character},
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
response = await self._send_request(state.port, "textDocument/typeDefinition", params)
|
|
678
|
+
|
|
679
|
+
if "error" in response or not response.get("result"):
|
|
680
|
+
return []
|
|
681
|
+
|
|
682
|
+
return _parse_locations(response["result"])
|
|
683
|
+
|
|
684
|
+
async def goto_implementation(
|
|
685
|
+
self,
|
|
686
|
+
server_id: str,
|
|
687
|
+
file_uri: str,
|
|
688
|
+
line: int,
|
|
689
|
+
character: int,
|
|
690
|
+
) -> list[Location]:
|
|
691
|
+
"""Go to implementation of symbol at position.
|
|
692
|
+
|
|
693
|
+
Useful for finding implementations of interfaces/abstract methods.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
server_id: Server identifier
|
|
697
|
+
file_uri: File URI
|
|
698
|
+
line: 0-based line number
|
|
699
|
+
character: 0-based character offset
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
List of implementation locations
|
|
703
|
+
"""
|
|
704
|
+
if server_id not in self._servers:
|
|
705
|
+
return []
|
|
706
|
+
|
|
707
|
+
state = self._servers[server_id]
|
|
708
|
+
params = {
|
|
709
|
+
"textDocument": {"uri": file_uri},
|
|
710
|
+
"position": {"line": line, "character": character},
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
response = await self._send_request(state.port, "textDocument/implementation", params)
|
|
714
|
+
|
|
715
|
+
if "error" in response or not response.get("result"):
|
|
716
|
+
return []
|
|
717
|
+
|
|
718
|
+
return _parse_locations(response["result"])
|
|
719
|
+
|
|
720
|
+
async def find_references(
|
|
721
|
+
self,
|
|
722
|
+
server_id: str,
|
|
723
|
+
file_uri: str,
|
|
724
|
+
line: int,
|
|
725
|
+
character: int,
|
|
726
|
+
include_declaration: bool = True,
|
|
727
|
+
) -> list[Location]:
|
|
728
|
+
"""Find all references to symbol at position.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
server_id: Server identifier
|
|
732
|
+
file_uri: File URI
|
|
733
|
+
line: 0-based line number
|
|
734
|
+
character: 0-based character offset
|
|
735
|
+
include_declaration: Whether to include the declaration itself
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
List of reference locations
|
|
739
|
+
"""
|
|
740
|
+
if server_id not in self._servers:
|
|
741
|
+
return []
|
|
742
|
+
|
|
743
|
+
state = self._servers[server_id]
|
|
744
|
+
params = {
|
|
745
|
+
"textDocument": {"uri": file_uri},
|
|
746
|
+
"position": {"line": line, "character": character},
|
|
747
|
+
"context": {"includeDeclaration": include_declaration},
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
response = await self._send_request(state.port, "textDocument/references", params)
|
|
751
|
+
|
|
752
|
+
if "error" in response or not response.get("result"):
|
|
753
|
+
return []
|
|
754
|
+
|
|
755
|
+
return _parse_locations(response["result"])
|
|
756
|
+
|
|
757
|
+
async def get_document_symbols(
|
|
758
|
+
self,
|
|
759
|
+
server_id: str,
|
|
760
|
+
file_uri: str,
|
|
761
|
+
) -> list[SymbolInfo]:
|
|
762
|
+
"""Get all symbols in a document (outline).
|
|
763
|
+
|
|
764
|
+
Returns a hierarchical list of symbols (classes, functions, etc.)
|
|
765
|
+
in the document.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
server_id: Server identifier
|
|
769
|
+
file_uri: File URI
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
List of symbols with hierarchy
|
|
773
|
+
"""
|
|
774
|
+
if server_id not in self._servers:
|
|
775
|
+
return []
|
|
776
|
+
|
|
777
|
+
state = self._servers[server_id]
|
|
778
|
+
params = {"textDocument": {"uri": file_uri}}
|
|
779
|
+
|
|
780
|
+
response = await self._send_request(state.port, "textDocument/documentSymbol", params)
|
|
781
|
+
|
|
782
|
+
if "error" in response or not response.get("result"):
|
|
783
|
+
return []
|
|
784
|
+
|
|
785
|
+
return _parse_document_symbols(response["result"], file_uri)
|
|
786
|
+
|
|
787
|
+
async def search_workspace_symbols(
|
|
788
|
+
self,
|
|
789
|
+
server_id: str,
|
|
790
|
+
query: str,
|
|
791
|
+
) -> list[SymbolInfo]:
|
|
792
|
+
"""Search for symbols in the workspace.
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
server_id: Server identifier
|
|
796
|
+
query: Search query (fuzzy matching)
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
List of matching symbols
|
|
800
|
+
"""
|
|
801
|
+
if server_id not in self._servers:
|
|
802
|
+
return []
|
|
803
|
+
|
|
804
|
+
state = self._servers[server_id]
|
|
805
|
+
params = {"query": query}
|
|
806
|
+
|
|
807
|
+
response = await self._send_request(state.port, "workspace/symbol", params)
|
|
808
|
+
|
|
809
|
+
if "error" in response or not response.get("result"):
|
|
810
|
+
return []
|
|
811
|
+
|
|
812
|
+
return _parse_workspace_symbols(response["result"])
|
|
813
|
+
|
|
814
|
+
async def get_completions(
|
|
815
|
+
self,
|
|
816
|
+
server_id: str,
|
|
817
|
+
file_uri: str,
|
|
818
|
+
line: int,
|
|
819
|
+
character: int,
|
|
820
|
+
) -> list[CompletionItem]:
|
|
821
|
+
"""Get completion suggestions at position.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
server_id: Server identifier
|
|
825
|
+
file_uri: File URI
|
|
826
|
+
line: 0-based line number
|
|
827
|
+
character: 0-based character offset
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
List of completion items
|
|
831
|
+
"""
|
|
832
|
+
if server_id not in self._servers:
|
|
833
|
+
return []
|
|
834
|
+
|
|
835
|
+
state = self._servers[server_id]
|
|
836
|
+
params = {
|
|
837
|
+
"textDocument": {"uri": file_uri},
|
|
838
|
+
"position": {"line": line, "character": character},
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
response = await self._send_request(state.port, "textDocument/completion", params)
|
|
842
|
+
|
|
843
|
+
if "error" in response or not response.get("result"):
|
|
844
|
+
return []
|
|
845
|
+
|
|
846
|
+
result = response["result"]
|
|
847
|
+
# Result can be CompletionList or CompletionItem[]
|
|
848
|
+
items = result.get("items", result) if isinstance(result, dict) else result
|
|
849
|
+
|
|
850
|
+
return [_parse_completion_item(item) for item in items]
|
|
851
|
+
|
|
852
|
+
async def get_signature_help(
|
|
853
|
+
self,
|
|
854
|
+
server_id: str,
|
|
855
|
+
file_uri: str,
|
|
856
|
+
line: int,
|
|
857
|
+
character: int,
|
|
858
|
+
) -> SignatureInfo | None:
|
|
859
|
+
"""Get signature help at position.
|
|
860
|
+
|
|
861
|
+
Useful when cursor is inside function call parentheses.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
server_id: Server identifier
|
|
865
|
+
file_uri: File URI
|
|
866
|
+
line: 0-based line number
|
|
867
|
+
character: 0-based character offset
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
SignatureInfo if available, None otherwise
|
|
871
|
+
"""
|
|
872
|
+
if server_id not in self._servers:
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
state = self._servers[server_id]
|
|
876
|
+
params = {
|
|
877
|
+
"textDocument": {"uri": file_uri},
|
|
878
|
+
"position": {"line": line, "character": character},
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
response = await self._send_request(state.port, "textDocument/signatureHelp", params)
|
|
882
|
+
|
|
883
|
+
if "error" in response or not response.get("result"):
|
|
884
|
+
return None
|
|
885
|
+
|
|
886
|
+
result = response["result"]
|
|
887
|
+
signatures = result.get("signatures", [])
|
|
888
|
+
if not signatures:
|
|
889
|
+
return None
|
|
890
|
+
|
|
891
|
+
active_sig = result.get("activeSignature", 0)
|
|
892
|
+
sig = signatures[min(active_sig, len(signatures) - 1)]
|
|
893
|
+
|
|
894
|
+
return SignatureInfo(
|
|
895
|
+
label=sig.get("label", ""),
|
|
896
|
+
documentation=_extract_documentation(sig.get("documentation")),
|
|
897
|
+
parameters=sig.get("parameters", []),
|
|
898
|
+
active_parameter=result.get("activeParameter"),
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
async def get_code_actions(
|
|
902
|
+
self,
|
|
903
|
+
server_id: str,
|
|
904
|
+
file_uri: str,
|
|
905
|
+
start_line: int,
|
|
906
|
+
start_character: int,
|
|
907
|
+
end_line: int,
|
|
908
|
+
end_character: int,
|
|
909
|
+
diagnostics: list[Diagnostic] | None = None,
|
|
910
|
+
) -> list[CodeAction]:
|
|
911
|
+
"""Get available code actions for a range.
|
|
912
|
+
|
|
913
|
+
Code actions include quick fixes, refactorings, and source actions.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
server_id: Server identifier
|
|
917
|
+
file_uri: File URI
|
|
918
|
+
start_line: Start line (0-based)
|
|
919
|
+
start_character: Start character
|
|
920
|
+
end_line: End line (0-based)
|
|
921
|
+
end_character: End character
|
|
922
|
+
diagnostics: Optional diagnostics to get fixes for
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
List of available code actions
|
|
926
|
+
"""
|
|
927
|
+
if server_id not in self._servers:
|
|
928
|
+
return []
|
|
929
|
+
|
|
930
|
+
state = self._servers[server_id]
|
|
931
|
+
|
|
932
|
+
# Convert our Diagnostic to LSP format
|
|
933
|
+
lsp_diagnostics = [
|
|
934
|
+
{
|
|
935
|
+
"range": {
|
|
936
|
+
"start": {"line": d.line - 1, "character": d.column - 1},
|
|
937
|
+
"end": {
|
|
938
|
+
"line": (d.end_line or d.line) - 1,
|
|
939
|
+
"character": (d.end_column or d.column) - 1,
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
"message": d.message,
|
|
943
|
+
"severity": _severity_to_lsp(d.severity),
|
|
944
|
+
"source": d.source,
|
|
945
|
+
"code": d.code,
|
|
946
|
+
}
|
|
947
|
+
for d in (diagnostics or [])
|
|
948
|
+
]
|
|
949
|
+
|
|
950
|
+
params = {
|
|
951
|
+
"textDocument": {"uri": file_uri},
|
|
952
|
+
"range": {
|
|
953
|
+
"start": {"line": start_line, "character": start_character},
|
|
954
|
+
"end": {"line": end_line, "character": end_character},
|
|
955
|
+
},
|
|
956
|
+
"context": {
|
|
957
|
+
"diagnostics": lsp_diagnostics,
|
|
958
|
+
"only": ["quickfix", "refactor", "source"],
|
|
959
|
+
},
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
response = await self._send_request(state.port, "textDocument/codeAction", params)
|
|
963
|
+
|
|
964
|
+
if "error" in response or not response.get("result"):
|
|
965
|
+
return []
|
|
966
|
+
|
|
967
|
+
return [_parse_code_action(action) for action in response["result"]]
|
|
968
|
+
|
|
969
|
+
async def rename_symbol(
|
|
970
|
+
self,
|
|
971
|
+
server_id: str,
|
|
972
|
+
file_uri: str,
|
|
973
|
+
line: int,
|
|
974
|
+
character: int,
|
|
975
|
+
new_name: str,
|
|
976
|
+
) -> RenameResult:
|
|
977
|
+
"""Rename a symbol across the workspace.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
server_id: Server identifier
|
|
981
|
+
file_uri: File URI
|
|
982
|
+
line: 0-based line number
|
|
983
|
+
character: 0-based character offset
|
|
984
|
+
new_name: New name for the symbol
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
RenameResult with the edits to apply
|
|
988
|
+
"""
|
|
989
|
+
if server_id not in self._servers:
|
|
990
|
+
return RenameResult(changes={}, success=False, error="Server not running")
|
|
991
|
+
|
|
992
|
+
state = self._servers[server_id]
|
|
993
|
+
|
|
994
|
+
# First check if rename is valid
|
|
995
|
+
prepare_params = {
|
|
996
|
+
"textDocument": {"uri": file_uri},
|
|
997
|
+
"position": {"line": line, "character": character},
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
prepare_response = await self._send_request(
|
|
1001
|
+
state.port, "textDocument/prepareRename", prepare_params
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
if "error" in prepare_response:
|
|
1005
|
+
return RenameResult(
|
|
1006
|
+
changes={},
|
|
1007
|
+
success=False,
|
|
1008
|
+
error=str(prepare_response["error"]),
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
# Now do the rename
|
|
1012
|
+
rename_params = {
|
|
1013
|
+
"textDocument": {"uri": file_uri},
|
|
1014
|
+
"position": {"line": line, "character": character},
|
|
1015
|
+
"newName": new_name,
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
response = await self._send_request(state.port, "textDocument/rename", rename_params)
|
|
1019
|
+
|
|
1020
|
+
if "error" in response:
|
|
1021
|
+
return RenameResult(
|
|
1022
|
+
changes={},
|
|
1023
|
+
success=False,
|
|
1024
|
+
error=str(response["error"]),
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
result = response.get("result", {})
|
|
1028
|
+
changes = result.get("changes", {})
|
|
1029
|
+
document_changes = result.get("documentChanges", [])
|
|
1030
|
+
|
|
1031
|
+
# Normalize to changes format
|
|
1032
|
+
if document_changes and not changes:
|
|
1033
|
+
changes = {}
|
|
1034
|
+
for doc_change in document_changes:
|
|
1035
|
+
if "textDocument" in doc_change:
|
|
1036
|
+
uri = doc_change["textDocument"]["uri"]
|
|
1037
|
+
changes[uri] = doc_change.get("edits", [])
|
|
1038
|
+
|
|
1039
|
+
return RenameResult(changes=changes, success=True)
|
|
1040
|
+
|
|
1041
|
+
async def format_document(
|
|
1042
|
+
self,
|
|
1043
|
+
server_id: str,
|
|
1044
|
+
file_uri: str,
|
|
1045
|
+
tab_size: int = 4,
|
|
1046
|
+
insert_spaces: bool = True,
|
|
1047
|
+
) -> list[dict[str, Any]]:
|
|
1048
|
+
"""Format an entire document.
|
|
1049
|
+
|
|
1050
|
+
Args:
|
|
1051
|
+
server_id: Server identifier
|
|
1052
|
+
file_uri: File URI
|
|
1053
|
+
tab_size: Tab size in spaces
|
|
1054
|
+
insert_spaces: Use spaces instead of tabs
|
|
1055
|
+
|
|
1056
|
+
Returns:
|
|
1057
|
+
List of text edits to apply
|
|
1058
|
+
"""
|
|
1059
|
+
if server_id not in self._servers:
|
|
1060
|
+
return []
|
|
1061
|
+
|
|
1062
|
+
state = self._servers[server_id]
|
|
1063
|
+
params = {
|
|
1064
|
+
"textDocument": {"uri": file_uri},
|
|
1065
|
+
"options": {
|
|
1066
|
+
"tabSize": tab_size,
|
|
1067
|
+
"insertSpaces": insert_spaces,
|
|
1068
|
+
},
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
response = await self._send_request(state.port, "textDocument/formatting", params)
|
|
1072
|
+
|
|
1073
|
+
if "error" in response or not response.get("result"):
|
|
1074
|
+
return []
|
|
1075
|
+
|
|
1076
|
+
return response["result"] # type: ignore[no-any-return]
|
|
1077
|
+
|
|
1078
|
+
# =========================================================================
|
|
1079
|
+
# Call Hierarchy
|
|
1080
|
+
# =========================================================================
|
|
1081
|
+
|
|
1082
|
+
async def prepare_call_hierarchy(
|
|
1083
|
+
self,
|
|
1084
|
+
server_id: str,
|
|
1085
|
+
file_uri: str,
|
|
1086
|
+
line: int,
|
|
1087
|
+
character: int,
|
|
1088
|
+
) -> list[CallHierarchyItem]:
|
|
1089
|
+
"""Prepare call hierarchy at position.
|
|
1090
|
+
|
|
1091
|
+
This returns the item(s) at the position that can be used
|
|
1092
|
+
to query incoming/outgoing calls.
|
|
1093
|
+
|
|
1094
|
+
Args:
|
|
1095
|
+
server_id: Server identifier
|
|
1096
|
+
file_uri: File URI
|
|
1097
|
+
line: 0-based line number
|
|
1098
|
+
character: 0-based character offset
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
List of call hierarchy items
|
|
1102
|
+
"""
|
|
1103
|
+
if server_id not in self._servers:
|
|
1104
|
+
return []
|
|
1105
|
+
|
|
1106
|
+
state = self._servers[server_id]
|
|
1107
|
+
params = {
|
|
1108
|
+
"textDocument": {"uri": file_uri},
|
|
1109
|
+
"position": {"line": line, "character": character},
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
response = await self._send_request(state.port, "textDocument/prepareCallHierarchy", params)
|
|
1113
|
+
|
|
1114
|
+
if "error" in response or not response.get("result"):
|
|
1115
|
+
return []
|
|
1116
|
+
|
|
1117
|
+
return [_parse_call_hierarchy_item(item) for item in response["result"]]
|
|
1118
|
+
|
|
1119
|
+
async def get_incoming_calls(
|
|
1120
|
+
self,
|
|
1121
|
+
server_id: str,
|
|
1122
|
+
item: CallHierarchyItem,
|
|
1123
|
+
) -> list[CallHierarchyCall]:
|
|
1124
|
+
"""Get incoming calls (callers) for a call hierarchy item.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
server_id: Server identifier
|
|
1128
|
+
item: Call hierarchy item from prepare_call_hierarchy
|
|
1129
|
+
|
|
1130
|
+
Returns:
|
|
1131
|
+
List of incoming calls
|
|
1132
|
+
"""
|
|
1133
|
+
if server_id not in self._servers:
|
|
1134
|
+
return []
|
|
1135
|
+
|
|
1136
|
+
state = self._servers[server_id]
|
|
1137
|
+
params = {"item": _call_hierarchy_item_to_lsp(item)}
|
|
1138
|
+
|
|
1139
|
+
response = await self._send_request(state.port, "callHierarchy/incomingCalls", params)
|
|
1140
|
+
|
|
1141
|
+
if "error" in response or not response.get("result"):
|
|
1142
|
+
return []
|
|
1143
|
+
|
|
1144
|
+
return [
|
|
1145
|
+
CallHierarchyCall(
|
|
1146
|
+
item=_parse_call_hierarchy_item(call["from"]),
|
|
1147
|
+
from_ranges=[_parse_range(r) for r in call.get("fromRanges", [])],
|
|
1148
|
+
)
|
|
1149
|
+
for call in response["result"]
|
|
1150
|
+
]
|
|
1151
|
+
|
|
1152
|
+
async def get_outgoing_calls(
|
|
1153
|
+
self,
|
|
1154
|
+
server_id: str,
|
|
1155
|
+
item: CallHierarchyItem,
|
|
1156
|
+
) -> list[CallHierarchyCall]:
|
|
1157
|
+
"""Get outgoing calls (callees) for a call hierarchy item.
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
server_id: Server identifier
|
|
1161
|
+
item: Call hierarchy item from prepare_call_hierarchy
|
|
1162
|
+
|
|
1163
|
+
Returns:
|
|
1164
|
+
List of outgoing calls
|
|
1165
|
+
"""
|
|
1166
|
+
if server_id not in self._servers:
|
|
1167
|
+
return []
|
|
1168
|
+
|
|
1169
|
+
state = self._servers[server_id]
|
|
1170
|
+
params = {"item": _call_hierarchy_item_to_lsp(item)}
|
|
1171
|
+
|
|
1172
|
+
response = await self._send_request(state.port, "callHierarchy/outgoingCalls", params)
|
|
1173
|
+
|
|
1174
|
+
if "error" in response or not response.get("result"):
|
|
1175
|
+
return []
|
|
1176
|
+
|
|
1177
|
+
return [
|
|
1178
|
+
CallHierarchyCall(
|
|
1179
|
+
item=_parse_call_hierarchy_item(call["to"]),
|
|
1180
|
+
from_ranges=[_parse_range(r) for r in call.get("fromRanges", [])],
|
|
1181
|
+
)
|
|
1182
|
+
for call in response["result"]
|
|
1183
|
+
]
|
|
1184
|
+
|
|
1185
|
+
# =========================================================================
|
|
1186
|
+
# Type Hierarchy
|
|
1187
|
+
# =========================================================================
|
|
1188
|
+
|
|
1189
|
+
async def prepare_type_hierarchy(
|
|
1190
|
+
self,
|
|
1191
|
+
server_id: str,
|
|
1192
|
+
file_uri: str,
|
|
1193
|
+
line: int,
|
|
1194
|
+
character: int,
|
|
1195
|
+
) -> list[TypeHierarchyItem]:
|
|
1196
|
+
"""Prepare type hierarchy at position.
|
|
1197
|
+
|
|
1198
|
+
Args:
|
|
1199
|
+
server_id: Server identifier
|
|
1200
|
+
file_uri: File URI
|
|
1201
|
+
line: 0-based line number
|
|
1202
|
+
character: 0-based character offset
|
|
1203
|
+
|
|
1204
|
+
Returns:
|
|
1205
|
+
List of type hierarchy items
|
|
1206
|
+
"""
|
|
1207
|
+
if server_id not in self._servers:
|
|
1208
|
+
return []
|
|
1209
|
+
|
|
1210
|
+
state = self._servers[server_id]
|
|
1211
|
+
params = {
|
|
1212
|
+
"textDocument": {"uri": file_uri},
|
|
1213
|
+
"position": {"line": line, "character": character},
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
response = await self._send_request(state.port, "textDocument/prepareTypeHierarchy", params)
|
|
1217
|
+
|
|
1218
|
+
if "error" in response or not response.get("result"):
|
|
1219
|
+
return []
|
|
1220
|
+
|
|
1221
|
+
return [_parse_type_hierarchy_item(item) for item in response["result"]]
|
|
1222
|
+
|
|
1223
|
+
async def get_supertypes(
|
|
1224
|
+
self,
|
|
1225
|
+
server_id: str,
|
|
1226
|
+
item: TypeHierarchyItem,
|
|
1227
|
+
) -> list[TypeHierarchyItem]:
|
|
1228
|
+
"""Get supertypes (base classes/interfaces) for a type.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
server_id: Server identifier
|
|
1232
|
+
item: Type hierarchy item from prepare_type_hierarchy
|
|
1233
|
+
|
|
1234
|
+
Returns:
|
|
1235
|
+
List of supertype items
|
|
1236
|
+
"""
|
|
1237
|
+
if server_id not in self._servers:
|
|
1238
|
+
return []
|
|
1239
|
+
|
|
1240
|
+
state = self._servers[server_id]
|
|
1241
|
+
params = {"item": _type_hierarchy_item_to_lsp(item)}
|
|
1242
|
+
|
|
1243
|
+
response = await self._send_request(state.port, "typeHierarchy/supertypes", params)
|
|
1244
|
+
|
|
1245
|
+
if "error" in response or not response.get("result"):
|
|
1246
|
+
return []
|
|
1247
|
+
|
|
1248
|
+
return [_parse_type_hierarchy_item(item) for item in response["result"]]
|
|
1249
|
+
|
|
1250
|
+
async def get_subtypes(
|
|
1251
|
+
self,
|
|
1252
|
+
server_id: str,
|
|
1253
|
+
item: TypeHierarchyItem,
|
|
1254
|
+
) -> list[TypeHierarchyItem]:
|
|
1255
|
+
"""Get subtypes (derived classes/implementations) for a type.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
server_id: Server identifier
|
|
1259
|
+
item: Type hierarchy item from prepare_type_hierarchy
|
|
1260
|
+
|
|
1261
|
+
Returns:
|
|
1262
|
+
List of subtype items
|
|
1263
|
+
"""
|
|
1264
|
+
if server_id not in self._servers:
|
|
1265
|
+
return []
|
|
1266
|
+
|
|
1267
|
+
state = self._servers[server_id]
|
|
1268
|
+
params = {"item": _type_hierarchy_item_to_lsp(item)}
|
|
1269
|
+
|
|
1270
|
+
response = await self._send_request(state.port, "typeHierarchy/subtypes", params)
|
|
1271
|
+
|
|
1272
|
+
if "error" in response or not response.get("result"):
|
|
1273
|
+
return []
|
|
1274
|
+
|
|
1275
|
+
return [_parse_type_hierarchy_item(item) for item in response["result"]]
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def _shell_quote(s: str) -> str:
|
|
1279
|
+
"""Quote a string for shell use."""
|
|
1280
|
+
import shlex
|
|
1281
|
+
|
|
1282
|
+
return shlex.quote(s)
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _uri_to_language_id(uri: str) -> str:
|
|
1286
|
+
"""Convert file URI to LSP language ID."""
|
|
1287
|
+
import posixpath
|
|
1288
|
+
|
|
1289
|
+
ext = posixpath.splitext(uri)[1].lower()
|
|
1290
|
+
language_map = {
|
|
1291
|
+
".py": "python",
|
|
1292
|
+
".pyi": "python",
|
|
1293
|
+
".js": "javascript",
|
|
1294
|
+
".jsx": "javascriptreact",
|
|
1295
|
+
".ts": "typescript",
|
|
1296
|
+
".tsx": "typescriptreact",
|
|
1297
|
+
".rs": "rust",
|
|
1298
|
+
".go": "go",
|
|
1299
|
+
".c": "c",
|
|
1300
|
+
".cpp": "cpp",
|
|
1301
|
+
".h": "c",
|
|
1302
|
+
".hpp": "cpp",
|
|
1303
|
+
".java": "java",
|
|
1304
|
+
".rb": "ruby",
|
|
1305
|
+
".lua": "lua",
|
|
1306
|
+
".zig": "zig",
|
|
1307
|
+
".swift": "swift",
|
|
1308
|
+
".ex": "elixir",
|
|
1309
|
+
".exs": "elixir",
|
|
1310
|
+
".php": "php",
|
|
1311
|
+
".dart": "dart",
|
|
1312
|
+
".yaml": "yaml",
|
|
1313
|
+
".yml": "yaml",
|
|
1314
|
+
".cs": "csharp",
|
|
1315
|
+
}
|
|
1316
|
+
return language_map.get(ext, "plaintext")
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _convert_diagnostic(diag: Any, server_id: str) -> Diagnostic:
|
|
1320
|
+
"""Convert anyenv Diagnostic to agentpool Diagnostic."""
|
|
1321
|
+
return Diagnostic(
|
|
1322
|
+
file=diag.file,
|
|
1323
|
+
line=diag.line,
|
|
1324
|
+
column=diag.column,
|
|
1325
|
+
severity=diag.severity,
|
|
1326
|
+
message=diag.message,
|
|
1327
|
+
source=server_id,
|
|
1328
|
+
code=diag.code,
|
|
1329
|
+
end_line=diag.end_line,
|
|
1330
|
+
end_column=diag.end_column,
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
# =============================================================================
|
|
1335
|
+
# LSP Response Parsing Helpers
|
|
1336
|
+
# =============================================================================
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _parse_position(pos: dict[str, Any]) -> Position:
|
|
1340
|
+
"""Parse LSP Position."""
|
|
1341
|
+
return Position(line=pos["line"], character=pos["character"])
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _parse_range(range_: dict[str, Any]) -> Range:
|
|
1345
|
+
"""Parse LSP Range."""
|
|
1346
|
+
return Range(
|
|
1347
|
+
start=_parse_position(range_["start"]),
|
|
1348
|
+
end=_parse_position(range_["end"]),
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _parse_location(loc: dict[str, Any]) -> Location:
|
|
1353
|
+
"""Parse LSP Location."""
|
|
1354
|
+
return Location(
|
|
1355
|
+
uri=loc["uri"],
|
|
1356
|
+
range=_parse_range(loc["range"]),
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def _parse_locations(result: Any) -> list[Location]:
|
|
1361
|
+
"""Parse LSP definition/references result (can be Location, Location[], or LocationLink[])."""
|
|
1362
|
+
if not result:
|
|
1363
|
+
return []
|
|
1364
|
+
|
|
1365
|
+
# Single location
|
|
1366
|
+
if isinstance(result, dict) and "uri" in result:
|
|
1367
|
+
return [_parse_location(result)]
|
|
1368
|
+
|
|
1369
|
+
# Array of locations or location links
|
|
1370
|
+
locations = []
|
|
1371
|
+
for item in result:
|
|
1372
|
+
if "targetUri" in item: # LocationLink
|
|
1373
|
+
locations.append(
|
|
1374
|
+
Location(
|
|
1375
|
+
uri=item["targetUri"],
|
|
1376
|
+
range=_parse_range(item["targetRange"]),
|
|
1377
|
+
)
|
|
1378
|
+
)
|
|
1379
|
+
elif "uri" in item: # Location
|
|
1380
|
+
locations.append(_parse_location(item))
|
|
1381
|
+
|
|
1382
|
+
return locations
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def _extract_hover_contents(contents: HoverContents) -> str:
|
|
1386
|
+
"""Extract string from hover contents."""
|
|
1387
|
+
if isinstance(contents, str):
|
|
1388
|
+
return contents
|
|
1389
|
+
|
|
1390
|
+
if isinstance(contents, dict):
|
|
1391
|
+
# MarkupContent or MarkedString with language
|
|
1392
|
+
if "value" in contents:
|
|
1393
|
+
return contents["value"]
|
|
1394
|
+
if "kind" in contents:
|
|
1395
|
+
return contents.get("value", "")
|
|
1396
|
+
|
|
1397
|
+
if isinstance(contents, list):
|
|
1398
|
+
# Array of MarkedString
|
|
1399
|
+
parts = []
|
|
1400
|
+
for item in contents:
|
|
1401
|
+
if isinstance(item, str):
|
|
1402
|
+
parts.append(item)
|
|
1403
|
+
elif isinstance(item, dict) and "value" in item:
|
|
1404
|
+
parts.append(item["value"])
|
|
1405
|
+
return "\n\n".join(parts)
|
|
1406
|
+
|
|
1407
|
+
return str(contents)
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def _extract_documentation(doc: Any) -> str | None:
|
|
1411
|
+
"""Extract documentation string."""
|
|
1412
|
+
if doc is None:
|
|
1413
|
+
return None
|
|
1414
|
+
if isinstance(doc, str):
|
|
1415
|
+
return doc
|
|
1416
|
+
if isinstance(doc, dict):
|
|
1417
|
+
return doc.get("value")
|
|
1418
|
+
return str(doc)
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def _parse_document_symbols(result: list[Any], file_uri: str) -> list[SymbolInfo]:
|
|
1422
|
+
"""Parse document symbols (can be DocumentSymbol[] or SymbolInformation[])."""
|
|
1423
|
+
symbols = []
|
|
1424
|
+
|
|
1425
|
+
for item in result:
|
|
1426
|
+
if "location" in item:
|
|
1427
|
+
# SymbolInformation (flat)
|
|
1428
|
+
symbols.append(
|
|
1429
|
+
SymbolInfo(
|
|
1430
|
+
name=item["name"],
|
|
1431
|
+
kind=SYMBOL_KIND_MAP.get(item.get("kind", 0), "unknown"),
|
|
1432
|
+
location=_parse_location(item["location"]),
|
|
1433
|
+
container_name=item.get("containerName"),
|
|
1434
|
+
)
|
|
1435
|
+
)
|
|
1436
|
+
else:
|
|
1437
|
+
# DocumentSymbol (hierarchical)
|
|
1438
|
+
symbols.append(_parse_document_symbol(item, file_uri))
|
|
1439
|
+
|
|
1440
|
+
return symbols
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
def _parse_document_symbol(item: dict[str, Any], file_uri: str) -> SymbolInfo:
|
|
1444
|
+
"""Parse a single DocumentSymbol with children."""
|
|
1445
|
+
children = [_parse_document_symbol(child, file_uri) for child in item.get("children", [])]
|
|
1446
|
+
|
|
1447
|
+
return SymbolInfo(
|
|
1448
|
+
name=item["name"],
|
|
1449
|
+
kind=SYMBOL_KIND_MAP.get(item.get("kind", 0), "unknown"),
|
|
1450
|
+
location=Location(
|
|
1451
|
+
uri=file_uri,
|
|
1452
|
+
range=_parse_range(item["range"]),
|
|
1453
|
+
),
|
|
1454
|
+
detail=item.get("detail"),
|
|
1455
|
+
children=children,
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _parse_workspace_symbols(result: list[Any]) -> list[SymbolInfo]:
|
|
1460
|
+
"""Parse workspace symbols."""
|
|
1461
|
+
return [
|
|
1462
|
+
SymbolInfo(
|
|
1463
|
+
name=item["name"],
|
|
1464
|
+
kind=SYMBOL_KIND_MAP.get(item.get("kind", 0), "unknown"),
|
|
1465
|
+
location=_parse_location(item["location"]),
|
|
1466
|
+
container_name=item.get("containerName"),
|
|
1467
|
+
)
|
|
1468
|
+
for item in result
|
|
1469
|
+
if "location" in item
|
|
1470
|
+
]
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _parse_completion_item(item: dict[str, Any]) -> CompletionItem:
|
|
1474
|
+
"""Parse a completion item."""
|
|
1475
|
+
return CompletionItem(
|
|
1476
|
+
label=item.get("label", ""),
|
|
1477
|
+
kind=COMPLETION_KIND_MAP.get(item.get("kind", 0)),
|
|
1478
|
+
detail=item.get("detail"),
|
|
1479
|
+
documentation=_extract_documentation(item.get("documentation")),
|
|
1480
|
+
insert_text=item.get("insertText"),
|
|
1481
|
+
sort_text=item.get("sortText"),
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def _parse_code_action(action: dict[str, Any]) -> CodeAction:
|
|
1486
|
+
"""Parse a code action."""
|
|
1487
|
+
return CodeAction(
|
|
1488
|
+
title=action.get("title", ""),
|
|
1489
|
+
kind=action.get("kind"),
|
|
1490
|
+
is_preferred=action.get("isPreferred", False),
|
|
1491
|
+
edit=action.get("edit"),
|
|
1492
|
+
command=action.get("command"),
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def _parse_call_hierarchy_item(item: dict[str, Any]) -> CallHierarchyItem:
|
|
1497
|
+
"""Parse a call hierarchy item."""
|
|
1498
|
+
return CallHierarchyItem(
|
|
1499
|
+
name=item["name"],
|
|
1500
|
+
kind=SYMBOL_KIND_MAP.get(item.get("kind", 0), "unknown"),
|
|
1501
|
+
uri=item["uri"],
|
|
1502
|
+
range=_parse_range(item["range"]),
|
|
1503
|
+
selection_range=_parse_range(item["selectionRange"]),
|
|
1504
|
+
detail=item.get("detail"),
|
|
1505
|
+
data=item.get("data"),
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
def _call_hierarchy_item_to_lsp(item: CallHierarchyItem) -> dict[str, Any]:
|
|
1510
|
+
"""Convert CallHierarchyItem back to LSP format."""
|
|
1511
|
+
# Find the numeric kind
|
|
1512
|
+
kind_num = 12 # function default
|
|
1513
|
+
for num, name in SYMBOL_KIND_MAP.items():
|
|
1514
|
+
if name == item.kind:
|
|
1515
|
+
kind_num = num
|
|
1516
|
+
break
|
|
1517
|
+
|
|
1518
|
+
return {
|
|
1519
|
+
"name": item.name,
|
|
1520
|
+
"kind": kind_num,
|
|
1521
|
+
"uri": item.uri,
|
|
1522
|
+
"range": {
|
|
1523
|
+
"start": {"line": item.range.start.line, "character": item.range.start.character},
|
|
1524
|
+
"end": {"line": item.range.end.line, "character": item.range.end.character},
|
|
1525
|
+
},
|
|
1526
|
+
"selectionRange": {
|
|
1527
|
+
"start": {
|
|
1528
|
+
"line": item.selection_range.start.line,
|
|
1529
|
+
"character": item.selection_range.start.character,
|
|
1530
|
+
},
|
|
1531
|
+
"end": {
|
|
1532
|
+
"line": item.selection_range.end.line,
|
|
1533
|
+
"character": item.selection_range.end.character,
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
"detail": item.detail,
|
|
1537
|
+
"data": item.data,
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
def _parse_type_hierarchy_item(item: dict[str, Any]) -> TypeHierarchyItem:
|
|
1542
|
+
"""Parse a type hierarchy item."""
|
|
1543
|
+
return TypeHierarchyItem(
|
|
1544
|
+
name=item["name"],
|
|
1545
|
+
kind=SYMBOL_KIND_MAP.get(item.get("kind", 0), "unknown"),
|
|
1546
|
+
uri=item["uri"],
|
|
1547
|
+
range=_parse_range(item["range"]),
|
|
1548
|
+
selection_range=_parse_range(item["selectionRange"]),
|
|
1549
|
+
detail=item.get("detail"),
|
|
1550
|
+
data=item.get("data"),
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
def _type_hierarchy_item_to_lsp(item: TypeHierarchyItem) -> dict[str, Any]:
|
|
1555
|
+
"""Convert TypeHierarchyItem back to LSP format."""
|
|
1556
|
+
# Find the numeric kind
|
|
1557
|
+
kind_num = 5 # class default
|
|
1558
|
+
for num, name in SYMBOL_KIND_MAP.items():
|
|
1559
|
+
if name == item.kind:
|
|
1560
|
+
kind_num = num
|
|
1561
|
+
break
|
|
1562
|
+
|
|
1563
|
+
return {
|
|
1564
|
+
"name": item.name,
|
|
1565
|
+
"kind": kind_num,
|
|
1566
|
+
"uri": item.uri,
|
|
1567
|
+
"range": {
|
|
1568
|
+
"start": {"line": item.range.start.line, "character": item.range.start.character},
|
|
1569
|
+
"end": {"line": item.range.end.line, "character": item.range.end.character},
|
|
1570
|
+
},
|
|
1571
|
+
"selectionRange": {
|
|
1572
|
+
"start": {
|
|
1573
|
+
"line": item.selection_range.start.line,
|
|
1574
|
+
"character": item.selection_range.start.character,
|
|
1575
|
+
},
|
|
1576
|
+
"end": {
|
|
1577
|
+
"line": item.selection_range.end.line,
|
|
1578
|
+
"character": item.selection_range.end.character,
|
|
1579
|
+
},
|
|
1580
|
+
},
|
|
1581
|
+
"detail": item.detail,
|
|
1582
|
+
"data": item.data,
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def _severity_to_lsp(severity: str) -> int:
|
|
1587
|
+
"""Convert severity string to LSP DiagnosticSeverity."""
|
|
1588
|
+
return {
|
|
1589
|
+
"error": 1,
|
|
1590
|
+
"warning": 2,
|
|
1591
|
+
"info": 3,
|
|
1592
|
+
"hint": 4,
|
|
1593
|
+
}.get(severity, 1)
|