crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""LSP manager — main entry point for the LSP subsystem.
|
|
2
|
+
|
|
3
|
+
Responsibilities:
|
|
4
|
+
- Load config and merge built-in + custom LSP server definitions.
|
|
5
|
+
- Lazy-start clients via :meth:`get_clients` which matches file extensions,
|
|
6
|
+
resolves the project root, spawns an :class:`LSPClient`, and caches it.
|
|
7
|
+
- Track broken servers (failed to start — skip retries).
|
|
8
|
+
- Track spawning clients (prevent concurrent starts for the same key).
|
|
9
|
+
- Expose ``touch_file``, ``diagnostics``, ``hover``, ``definition``,
|
|
10
|
+
``references`` methods that fan out to the appropriate clients.
|
|
11
|
+
- ``shutdown()`` to close all active clients.
|
|
12
|
+
|
|
13
|
+
Key invariants:
|
|
14
|
+
- Same ``(root_uri, server_id)`` pair gets only one client instance.
|
|
15
|
+
- One file may match multiple servers (e.g. ``.ts`` gets both *typescript*
|
|
16
|
+
and *eslint*).
|
|
17
|
+
- :class:`LSPManager` is session-scoped (one per :class:`CoreSession`).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from crabcode_core.logging_utils import get_logger
|
|
28
|
+
from crabcode_core.io_worker import run_io
|
|
29
|
+
from crabcode_core.lsp.client import LSPClient, _path_to_uri
|
|
30
|
+
from crabcode_core.types.config import CrabCodeSettings, LspServerConfig
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Built-in LSP server definitions
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
BUILTIN_LSP_SERVERS: dict[str, LspServerConfig] = {
|
|
40
|
+
"python": LspServerConfig(
|
|
41
|
+
command=["pyright-langserver", "--stdio"],
|
|
42
|
+
extensions=[".py", ".pyi", ".pyw"],
|
|
43
|
+
),
|
|
44
|
+
"typescript": LspServerConfig(
|
|
45
|
+
command=["typescript-language-server", "--stdio"],
|
|
46
|
+
extensions=[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
47
|
+
),
|
|
48
|
+
"go": LspServerConfig(
|
|
49
|
+
command=["gopls"],
|
|
50
|
+
extensions=[".go"],
|
|
51
|
+
),
|
|
52
|
+
"rust": LspServerConfig(
|
|
53
|
+
command=["rust-analyzer"],
|
|
54
|
+
extensions=[".rs"],
|
|
55
|
+
),
|
|
56
|
+
"csharp": LspServerConfig(
|
|
57
|
+
command=["omnisharp", "--stdio"],
|
|
58
|
+
extensions=[".cs"],
|
|
59
|
+
),
|
|
60
|
+
"java": LspServerConfig(
|
|
61
|
+
command=["jdtls"],
|
|
62
|
+
extensions=[".java"],
|
|
63
|
+
),
|
|
64
|
+
"cpp": LspServerConfig(
|
|
65
|
+
command=["clangd"],
|
|
66
|
+
extensions=[".c", ".cpp", ".h", ".hpp", ".cc", ".cxx", ".hh", ".hxx"],
|
|
67
|
+
),
|
|
68
|
+
"ruby": LspServerConfig(
|
|
69
|
+
command=["solargraph", "stdio"],
|
|
70
|
+
extensions=[".rb"],
|
|
71
|
+
),
|
|
72
|
+
"php": LspServerConfig(
|
|
73
|
+
command=["phpactor", "language-server"],
|
|
74
|
+
extensions=[".php"],
|
|
75
|
+
),
|
|
76
|
+
"elixir": LspServerConfig(
|
|
77
|
+
command=["lexical"],
|
|
78
|
+
extensions=[".ex", ".exs", ".elixir"],
|
|
79
|
+
),
|
|
80
|
+
"dart": LspServerConfig(
|
|
81
|
+
command=["dart", "language-server", "--protocol=lsp"],
|
|
82
|
+
extensions=[".dart"],
|
|
83
|
+
),
|
|
84
|
+
"lua": LspServerConfig(
|
|
85
|
+
command=["lua-language-server"],
|
|
86
|
+
extensions=[".lua"],
|
|
87
|
+
),
|
|
88
|
+
"kotlin": LspServerConfig(
|
|
89
|
+
command=["kotlin-language-server"],
|
|
90
|
+
extensions=[".kt", ".kts"],
|
|
91
|
+
),
|
|
92
|
+
"swift": LspServerConfig(
|
|
93
|
+
command=["sourcekit-lsp"],
|
|
94
|
+
extensions=[".swift"],
|
|
95
|
+
),
|
|
96
|
+
"zig": LspServerConfig(
|
|
97
|
+
command=["zls"],
|
|
98
|
+
extensions=[".zig"],
|
|
99
|
+
),
|
|
100
|
+
"scala": LspServerConfig(
|
|
101
|
+
command=["metals"],
|
|
102
|
+
extensions=[".scala"],
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Root-finding helpers
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
_ROOT_MARKERS: list[str] = [
|
|
112
|
+
".git",
|
|
113
|
+
".hg",
|
|
114
|
+
".svn",
|
|
115
|
+
# Language-specific
|
|
116
|
+
"pyproject.toml",
|
|
117
|
+
"setup.py",
|
|
118
|
+
"setup.cfg",
|
|
119
|
+
"Cargo.toml",
|
|
120
|
+
"go.mod",
|
|
121
|
+
"package.json",
|
|
122
|
+
"tsconfig.json",
|
|
123
|
+
"jsconfig.json",
|
|
124
|
+
"*.sln",
|
|
125
|
+
"*.csproj",
|
|
126
|
+
"pom.xml",
|
|
127
|
+
"build.gradle",
|
|
128
|
+
"build.gradle.kts",
|
|
129
|
+
"Gemfile",
|
|
130
|
+
"mix.exs",
|
|
131
|
+
"pubspec.yaml",
|
|
132
|
+
"CMakeLists.txt",
|
|
133
|
+
"Makefile",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def find_project_root(file_path: str | Path) -> str:
|
|
138
|
+
"""Walk upward from *file_path* to find the project root directory.
|
|
139
|
+
|
|
140
|
+
The root is the nearest ancestor directory containing one of the
|
|
141
|
+
conventional root markers (``.git``, ``pyproject.toml``, …). Falls back
|
|
142
|
+
to the dirname of *file_path* itself.
|
|
143
|
+
"""
|
|
144
|
+
current = Path(file_path).resolve()
|
|
145
|
+
if current.is_file():
|
|
146
|
+
current = current.parent
|
|
147
|
+
|
|
148
|
+
for _ in range(64): # safety bound
|
|
149
|
+
for marker in _ROOT_MARKERS:
|
|
150
|
+
if marker.startswith("*"):
|
|
151
|
+
# Glob-style marker — e.g. *.sln
|
|
152
|
+
if any(current.glob(marker)):
|
|
153
|
+
return str(current)
|
|
154
|
+
else:
|
|
155
|
+
if (current / marker).exists():
|
|
156
|
+
return str(current)
|
|
157
|
+
parent = current.parent
|
|
158
|
+
if parent == current:
|
|
159
|
+
break
|
|
160
|
+
current = parent
|
|
161
|
+
|
|
162
|
+
return str(current)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# LSPManager
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
# Type alias for the cache key: (root_uri, server_id)
|
|
170
|
+
_ClientKey = tuple[str, str]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class LSPManager:
|
|
174
|
+
"""Session-scoped manager for LSP client instances.
|
|
175
|
+
|
|
176
|
+
Usage::
|
|
177
|
+
|
|
178
|
+
manager = LSPManager(cwd="/path/to/project", settings=settings)
|
|
179
|
+
clients = await manager.get_clients("src/main.py")
|
|
180
|
+
for client in clients:
|
|
181
|
+
await client.touch_file("src/main.py")
|
|
182
|
+
diag = await manager.diagnostics("src/main.py")
|
|
183
|
+
await manager.shutdown()
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
cwd: str = ".",
|
|
189
|
+
settings: CrabCodeSettings | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
self._cwd = os.path.abspath(cwd)
|
|
192
|
+
self._settings = settings or CrabCodeSettings()
|
|
193
|
+
|
|
194
|
+
# Merged server configs: {server_id: LspServerConfig}
|
|
195
|
+
self._servers: dict[str, LspServerConfig] = {}
|
|
196
|
+
self._load_servers()
|
|
197
|
+
|
|
198
|
+
# Active client cache: {(root_uri, server_id): LSPClient}
|
|
199
|
+
self._clients: dict[_ClientKey, LSPClient] = {}
|
|
200
|
+
|
|
201
|
+
# Broken server set: {(root_uri, server_id)} — skip future starts
|
|
202
|
+
self._broken: set[_ClientKey] = set()
|
|
203
|
+
|
|
204
|
+
# Currently spawning: {(root_uri, server_id): asyncio.Task}
|
|
205
|
+
# Prevents concurrent starts for the same key.
|
|
206
|
+
self._spawning: dict[_ClientKey, asyncio.Task[LSPClient]] = {}
|
|
207
|
+
|
|
208
|
+
# -------------------------------------------------------------------
|
|
209
|
+
# Config loading
|
|
210
|
+
# -------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def _load_servers(self) -> None:
|
|
213
|
+
"""Merge built-in and user-configured LSP servers.
|
|
214
|
+
|
|
215
|
+
User config can override built-in entries (same key = override).
|
|
216
|
+
Setting ``disabled: true`` on a built-in server removes it.
|
|
217
|
+
"""
|
|
218
|
+
merged: dict[str, LspServerConfig] = {}
|
|
219
|
+
|
|
220
|
+
# 1. Start with built-in defaults
|
|
221
|
+
for server_id, config in BUILTIN_LSP_SERVERS.items():
|
|
222
|
+
merged[server_id] = config
|
|
223
|
+
|
|
224
|
+
# 2. Merge user/custom from settings
|
|
225
|
+
user_lsp = self._settings.lsp
|
|
226
|
+
if isinstance(user_lsp, bool):
|
|
227
|
+
# ``lsp: false`` → disable all built-in servers
|
|
228
|
+
if not user_lsp:
|
|
229
|
+
merged.clear()
|
|
230
|
+
elif isinstance(user_lsp, dict):
|
|
231
|
+
for server_id, config in user_lsp.items():
|
|
232
|
+
if config.disabled and server_id in merged:
|
|
233
|
+
# Explicitly disable a built-in server
|
|
234
|
+
del merged[server_id]
|
|
235
|
+
continue
|
|
236
|
+
if not config.command and server_id in merged:
|
|
237
|
+
# No command provided — keep built-in unless disabled
|
|
238
|
+
continue
|
|
239
|
+
merged[server_id] = config
|
|
240
|
+
|
|
241
|
+
# Remove any entries with empty command
|
|
242
|
+
self._servers = {
|
|
243
|
+
sid: cfg for sid, cfg in merged.items() if cfg.command
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# -------------------------------------------------------------------
|
|
247
|
+
# Extension matching
|
|
248
|
+
# -------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def _matching_servers(self, file_path: str | Path) -> list[str]:
|
|
251
|
+
"""Return server IDs whose ``extensions`` list matches *file_path*."""
|
|
252
|
+
suffix = Path(file_path).suffix.lower()
|
|
253
|
+
if not suffix:
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
matched: list[str] = []
|
|
257
|
+
for server_id, config in self._servers.items():
|
|
258
|
+
if suffix in config.extensions:
|
|
259
|
+
matched.append(server_id)
|
|
260
|
+
return matched
|
|
261
|
+
|
|
262
|
+
# -------------------------------------------------------------------
|
|
263
|
+
# Client lifecycle
|
|
264
|
+
# -------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
async def get_clients(self, file_path: str | Path) -> list[LSPClient]:
|
|
267
|
+
"""Return all LSP clients relevant to *file_path*.
|
|
268
|
+
|
|
269
|
+
For each matching server:
|
|
270
|
+
1. Compute the project root.
|
|
271
|
+
2. Build the cache key ``(root_uri, server_id)``.
|
|
272
|
+
3. If a client is cached, return it.
|
|
273
|
+
4. If the key is broken, skip it.
|
|
274
|
+
5. If a spawn is in progress, await it.
|
|
275
|
+
6. Otherwise, start a new client and cache it.
|
|
276
|
+
"""
|
|
277
|
+
file_path = str(file_path)
|
|
278
|
+
matching = self._matching_servers(file_path)
|
|
279
|
+
if not matching:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
root = await run_io(__name__, "find_project_root", file_path, timeout=5)
|
|
283
|
+
root_uri = _path_to_uri(root)
|
|
284
|
+
|
|
285
|
+
clients: list[LSPClient] = []
|
|
286
|
+
tasks: list[tuple[_ClientKey, asyncio.Task[LSPClient]]] = []
|
|
287
|
+
|
|
288
|
+
for server_id in matching:
|
|
289
|
+
key = (root_uri, server_id)
|
|
290
|
+
|
|
291
|
+
# Already running
|
|
292
|
+
if key in self._clients:
|
|
293
|
+
clients.append(self._clients[key])
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Previously broken — skip
|
|
297
|
+
if key in self._broken:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
# Currently spawning — wait for it
|
|
301
|
+
if key in self._spawning:
|
|
302
|
+
tasks.append((key, self._spawning[key]))
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# Start a new client
|
|
306
|
+
config = self._servers[server_id]
|
|
307
|
+
|
|
308
|
+
async def _start(
|
|
309
|
+
cfg: LspServerConfig = config,
|
|
310
|
+
k: _ClientKey = key,
|
|
311
|
+
sid: str = server_id,
|
|
312
|
+
ru: str = root_uri,
|
|
313
|
+
) -> LSPClient:
|
|
314
|
+
return await self._start_client(cfg, k, sid, ru)
|
|
315
|
+
|
|
316
|
+
task = asyncio.create_task(_start())
|
|
317
|
+
self._spawning[key] = task
|
|
318
|
+
tasks.append((key, task))
|
|
319
|
+
|
|
320
|
+
# Await all pending spawns
|
|
321
|
+
for key, task in tasks:
|
|
322
|
+
try:
|
|
323
|
+
client = await task
|
|
324
|
+
clients.append(client)
|
|
325
|
+
except Exception:
|
|
326
|
+
logger.warning(
|
|
327
|
+
"LSP client failed to start for %s at %s",
|
|
328
|
+
key[1], key[0],
|
|
329
|
+
exc_info=True,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return clients
|
|
333
|
+
|
|
334
|
+
async def _start_client(
|
|
335
|
+
self,
|
|
336
|
+
config: LspServerConfig,
|
|
337
|
+
key: _ClientKey,
|
|
338
|
+
server_id: str,
|
|
339
|
+
root_uri: str,
|
|
340
|
+
) -> LSPClient:
|
|
341
|
+
"""Start a single LSP client, update caches, and return it."""
|
|
342
|
+
try:
|
|
343
|
+
client = LSPClient(
|
|
344
|
+
command=config.command,
|
|
345
|
+
root_uri=root_uri,
|
|
346
|
+
env=config.env or None,
|
|
347
|
+
initialization_options=config.initialization or None,
|
|
348
|
+
)
|
|
349
|
+
await client.create()
|
|
350
|
+
self._clients[key] = client
|
|
351
|
+
logger.info(
|
|
352
|
+
"LSP client started: %s (root=%s)",
|
|
353
|
+
server_id, root_uri,
|
|
354
|
+
)
|
|
355
|
+
return client
|
|
356
|
+
except Exception:
|
|
357
|
+
self._broken.add(key)
|
|
358
|
+
raise
|
|
359
|
+
finally:
|
|
360
|
+
self._spawning.pop(key, None)
|
|
361
|
+
|
|
362
|
+
# -------------------------------------------------------------------
|
|
363
|
+
# Convenience methods (fan-out to all matching clients)
|
|
364
|
+
# -------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
async def touch_file(
|
|
367
|
+
self,
|
|
368
|
+
file_path: str | Path,
|
|
369
|
+
text: str | None = None,
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Open or update *file_path* in every matching LSP client."""
|
|
372
|
+
clients = await self.get_clients(file_path)
|
|
373
|
+
for client in clients:
|
|
374
|
+
try:
|
|
375
|
+
await client.touch_file(file_path, text=text)
|
|
376
|
+
except Exception:
|
|
377
|
+
logger.warning(
|
|
378
|
+
"LSP touch_file failed for %s", file_path,
|
|
379
|
+
exc_info=True,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
async def diagnostics(
|
|
383
|
+
self,
|
|
384
|
+
file_path: str | Path,
|
|
385
|
+
*,
|
|
386
|
+
timeout: float = 30.0,
|
|
387
|
+
debounce: float = 0.5,
|
|
388
|
+
) -> list[dict[str, Any]]:
|
|
389
|
+
"""Collect diagnostics for *file_path* from all matching clients.
|
|
390
|
+
|
|
391
|
+
Returns a merged list of diagnostic dicts. Each dict includes an
|
|
392
|
+
extra ``"source"`` key indicating the server that produced it.
|
|
393
|
+
"""
|
|
394
|
+
clients = await self.get_clients(file_path)
|
|
395
|
+
all_diags: list[dict[str, Any]] = []
|
|
396
|
+
|
|
397
|
+
for client in clients:
|
|
398
|
+
try:
|
|
399
|
+
diags = await client.wait_for_diagnostics(
|
|
400
|
+
file_path, timeout=timeout, debounce=debounce,
|
|
401
|
+
)
|
|
402
|
+
for d in diags:
|
|
403
|
+
all_diags.append({**d, "source": _client_source(client)})
|
|
404
|
+
except Exception:
|
|
405
|
+
logger.warning(
|
|
406
|
+
"LSP diagnostics failed for %s", file_path,
|
|
407
|
+
exc_info=True,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return all_diags
|
|
411
|
+
|
|
412
|
+
async def hover(
|
|
413
|
+
self,
|
|
414
|
+
file_path: str | Path,
|
|
415
|
+
line: int,
|
|
416
|
+
character: int,
|
|
417
|
+
) -> list[dict[str, Any]]:
|
|
418
|
+
"""Request hover information at (*line*, *character*) from matching clients.
|
|
419
|
+
|
|
420
|
+
*line* and *character* are 0-based as per the LSP spec.
|
|
421
|
+
|
|
422
|
+
Returns a list of hover results (one per server), each with an extra
|
|
423
|
+
``"source"`` key.
|
|
424
|
+
"""
|
|
425
|
+
clients = await self.get_clients(file_path)
|
|
426
|
+
results: list[dict[str, Any]] = []
|
|
427
|
+
|
|
428
|
+
uri = _path_to_uri(str(file_path))
|
|
429
|
+
params = {
|
|
430
|
+
"textDocument": {"uri": uri},
|
|
431
|
+
"position": {"line": line, "character": character},
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for client in clients:
|
|
435
|
+
try:
|
|
436
|
+
result = await client.send_request("textDocument/hover", params)
|
|
437
|
+
if result:
|
|
438
|
+
results.append({**result, "source": _client_source(client)})
|
|
439
|
+
except Exception:
|
|
440
|
+
logger.debug(
|
|
441
|
+
"LSP hover failed for %s at %d:%d",
|
|
442
|
+
file_path, line, character,
|
|
443
|
+
exc_info=True,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return results
|
|
447
|
+
|
|
448
|
+
async def definition(
|
|
449
|
+
self,
|
|
450
|
+
file_path: str | Path,
|
|
451
|
+
line: int,
|
|
452
|
+
character: int,
|
|
453
|
+
) -> list[dict[str, Any]]:
|
|
454
|
+
"""Request go-to-definition at (*line*, *character*) from matching clients.
|
|
455
|
+
|
|
456
|
+
*line* and *character* are 0-based.
|
|
457
|
+
|
|
458
|
+
Returns a list of location / location-link results with ``"source"``.
|
|
459
|
+
"""
|
|
460
|
+
clients = await self.get_clients(file_path)
|
|
461
|
+
results: list[dict[str, Any]] = []
|
|
462
|
+
|
|
463
|
+
uri = _path_to_uri(str(file_path))
|
|
464
|
+
params = {
|
|
465
|
+
"textDocument": {"uri": uri},
|
|
466
|
+
"position": {"line": line, "character": character},
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for client in clients:
|
|
470
|
+
try:
|
|
471
|
+
result = await client.send_request(
|
|
472
|
+
"textDocument/definition", params,
|
|
473
|
+
)
|
|
474
|
+
if result:
|
|
475
|
+
results.append({**result, "source": _client_source(client)})
|
|
476
|
+
except Exception:
|
|
477
|
+
logger.debug(
|
|
478
|
+
"LSP definition failed for %s at %d:%d",
|
|
479
|
+
file_path, line, character,
|
|
480
|
+
exc_info=True,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
return results
|
|
484
|
+
|
|
485
|
+
async def references(
|
|
486
|
+
self,
|
|
487
|
+
file_path: str | Path,
|
|
488
|
+
line: int,
|
|
489
|
+
character: int,
|
|
490
|
+
*,
|
|
491
|
+
include_declaration: bool = True,
|
|
492
|
+
) -> list[dict[str, Any]]:
|
|
493
|
+
"""Request find-references at (*line*, *character*) from matching clients.
|
|
494
|
+
|
|
495
|
+
*line* and *character* are 0-based.
|
|
496
|
+
|
|
497
|
+
Returns a list of reference location results with ``"source"``.
|
|
498
|
+
"""
|
|
499
|
+
clients = await self.get_clients(file_path)
|
|
500
|
+
results: list[dict[str, Any]] = []
|
|
501
|
+
|
|
502
|
+
uri = _path_to_uri(str(file_path))
|
|
503
|
+
params = {
|
|
504
|
+
"textDocument": {"uri": uri},
|
|
505
|
+
"position": {"line": line, "character": character},
|
|
506
|
+
"context": {"includeDeclaration": include_declaration},
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for client in clients:
|
|
510
|
+
try:
|
|
511
|
+
result = await client.send_request(
|
|
512
|
+
"textDocument/references", params,
|
|
513
|
+
)
|
|
514
|
+
if result:
|
|
515
|
+
results.append({**result, "source": _client_source(client)})
|
|
516
|
+
except Exception:
|
|
517
|
+
logger.debug(
|
|
518
|
+
"LSP references failed for %s at %d:%d",
|
|
519
|
+
file_path, line, character,
|
|
520
|
+
exc_info=True,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
return results
|
|
524
|
+
|
|
525
|
+
# -------------------------------------------------------------------
|
|
526
|
+
# Shutdown
|
|
527
|
+
# -------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
async def shutdown(self) -> None:
|
|
530
|
+
"""Close all active LSP clients and clear caches."""
|
|
531
|
+
# Cancel any in-progress spawns
|
|
532
|
+
for key, task in self._spawning.items():
|
|
533
|
+
task.cancel()
|
|
534
|
+
self._spawning.clear()
|
|
535
|
+
|
|
536
|
+
# Shut down active clients
|
|
537
|
+
for key, client in self._clients.items():
|
|
538
|
+
try:
|
|
539
|
+
await client.shutdown()
|
|
540
|
+
except Exception:
|
|
541
|
+
logger.warning(
|
|
542
|
+
"LSP client shutdown failed for %s at %s",
|
|
543
|
+
key[1], key[0],
|
|
544
|
+
exc_info=True,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
self._clients.clear()
|
|
548
|
+
self._broken.clear()
|
|
549
|
+
logger.info("LSP manager shut down")
|
|
550
|
+
|
|
551
|
+
# -------------------------------------------------------------------
|
|
552
|
+
# Introspection
|
|
553
|
+
# -------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def servers(self) -> dict[str, LspServerConfig]:
|
|
557
|
+
"""Read-only view of the merged server configurations."""
|
|
558
|
+
return dict(self._servers)
|
|
559
|
+
|
|
560
|
+
@property
|
|
561
|
+
def active_clients(self) -> dict[str, LSPClient]:
|
|
562
|
+
"""Active clients keyed by ``"root_uri|server_id"``."""
|
|
563
|
+
return {
|
|
564
|
+
f"{root_uri}|{server_id}": client
|
|
565
|
+
for (root_uri, server_id), client in self._clients.items()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
@property
|
|
569
|
+
def broken_servers(self) -> set[str]:
|
|
570
|
+
"""Set of ``"root_uri|server_id"`` strings for broken servers."""
|
|
571
|
+
return {f"{root_uri}|{server_id}" for root_uri, server_id in self._broken}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# ---------------------------------------------------------------------------
|
|
575
|
+
# Helpers
|
|
576
|
+
# ---------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _client_source(client: LSPClient) -> str:
|
|
580
|
+
"""Derive a human-readable source label from a client's command."""
|
|
581
|
+
if client._command:
|
|
582
|
+
return Path(client._command[0]).name
|
|
583
|
+
return "unknown"
|