superqode 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,544 @@
1
+ """
2
+ LSP Client - Language Server Protocol Integration.
3
+
4
+ Provides real-time code diagnostics and intelligence by
5
+ connecting to language servers for various languages.
6
+
7
+ Features:
8
+ - Multi-language support (Python, TypeScript, Go, etc.)
9
+ - Real-time diagnostics
10
+ - Code completion
11
+ - Hover information
12
+ - Go to definition
13
+ - Designed for SuperQode's QE workflow
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import json
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ from dataclasses import dataclass, field
24
+ from datetime import datetime
25
+ from enum import IntEnum
26
+ from pathlib import Path
27
+ from typing import Any, Callable, Dict, List, Optional, Tuple
28
+ import threading
29
+
30
+
31
+ class DiagnosticSeverity(IntEnum):
32
+ """LSP diagnostic severity levels."""
33
+
34
+ ERROR = 1
35
+ WARNING = 2
36
+ INFORMATION = 3
37
+ HINT = 4
38
+
39
+
40
+ @dataclass
41
+ class Position:
42
+ """Position in a text document."""
43
+
44
+ line: int
45
+ character: int
46
+
47
+ def to_dict(self) -> dict:
48
+ return {"line": self.line, "character": self.character}
49
+
50
+ @classmethod
51
+ def from_dict(cls, data: dict) -> "Position":
52
+ return cls(line=data["line"], character=data["character"])
53
+
54
+
55
+ @dataclass
56
+ class Range:
57
+ """Range in a text document."""
58
+
59
+ start: Position
60
+ end: Position
61
+
62
+ def to_dict(self) -> dict:
63
+ return {"start": self.start.to_dict(), "end": self.end.to_dict()}
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: dict) -> "Range":
67
+ return cls(
68
+ start=Position.from_dict(data["start"]),
69
+ end=Position.from_dict(data["end"]),
70
+ )
71
+
72
+
73
+ @dataclass
74
+ class Location:
75
+ """Location in a text document."""
76
+
77
+ uri: str
78
+ range: Range
79
+
80
+ def to_dict(self) -> dict:
81
+ return {"uri": self.uri, "range": self.range.to_dict()}
82
+
83
+ @classmethod
84
+ def from_dict(cls, data: dict) -> "Location":
85
+ return cls(uri=data["uri"], range=Range.from_dict(data["range"]))
86
+
87
+
88
+ @dataclass
89
+ class Diagnostic:
90
+ """A diagnostic (error, warning, etc.)."""
91
+
92
+ range: Range
93
+ message: str
94
+ severity: DiagnosticSeverity = DiagnosticSeverity.ERROR
95
+ code: Optional[str] = None
96
+ source: Optional[str] = None
97
+ related_information: List[dict] = field(default_factory=list)
98
+
99
+ @classmethod
100
+ def from_dict(cls, data: dict) -> "Diagnostic":
101
+ return cls(
102
+ range=Range.from_dict(data["range"]),
103
+ message=data["message"],
104
+ severity=DiagnosticSeverity(data.get("severity", 1)),
105
+ code=data.get("code"),
106
+ source=data.get("source"),
107
+ related_information=data.get("relatedInformation", []),
108
+ )
109
+
110
+ @property
111
+ def severity_name(self) -> str:
112
+ """Get human-readable severity name."""
113
+ names = {
114
+ DiagnosticSeverity.ERROR: "error",
115
+ DiagnosticSeverity.WARNING: "warning",
116
+ DiagnosticSeverity.INFORMATION: "info",
117
+ DiagnosticSeverity.HINT: "hint",
118
+ }
119
+ return names.get(self.severity, "unknown")
120
+
121
+
122
+ @dataclass
123
+ class LSPConfig:
124
+ """Configuration for LSP client."""
125
+
126
+ # Language server commands
127
+ servers: Dict[str, List[str]] = field(
128
+ default_factory=lambda: {
129
+ "python": ["pyright-langserver", "--stdio"],
130
+ "typescript": ["typescript-language-server", "--stdio"],
131
+ "javascript": ["typescript-language-server", "--stdio"],
132
+ "go": ["gopls"],
133
+ "rust": ["rust-analyzer"],
134
+ "c": ["clangd"],
135
+ "cpp": ["clangd"],
136
+ }
137
+ )
138
+
139
+ # File extensions to language mapping
140
+ extensions: Dict[str, str] = field(
141
+ default_factory=lambda: {
142
+ ".py": "python",
143
+ ".pyi": "python",
144
+ ".ts": "typescript",
145
+ ".tsx": "typescript",
146
+ ".js": "javascript",
147
+ ".jsx": "javascript",
148
+ ".go": "go",
149
+ ".rs": "rust",
150
+ ".c": "c",
151
+ ".h": "c",
152
+ ".cpp": "cpp",
153
+ ".hpp": "cpp",
154
+ ".cc": "cpp",
155
+ }
156
+ )
157
+
158
+ # Timeout for requests
159
+ timeout: float = 10.0
160
+
161
+
162
+ class LSPClient:
163
+ """
164
+ Language Server Protocol client.
165
+
166
+ Manages connections to language servers and provides
167
+ code diagnostics and intelligence features.
168
+
169
+ Usage:
170
+ config = LSPConfig()
171
+ client = LSPClient(project_root, config)
172
+
173
+ # Start server for Python files
174
+ await client.start_server("python")
175
+
176
+ # Get diagnostics for a file
177
+ diagnostics = await client.get_diagnostics("src/main.py")
178
+
179
+ # Open a file for tracking
180
+ await client.open_file("src/main.py")
181
+
182
+ # Clean up
183
+ await client.shutdown()
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ project_root: Path,
189
+ config: Optional[LSPConfig] = None,
190
+ ):
191
+ self.project_root = Path(project_root).resolve()
192
+ self.config = config or LSPConfig()
193
+
194
+ # Server processes
195
+ self._processes: Dict[str, subprocess.Popen] = {}
196
+ self._request_id = 0
197
+ self._pending_requests: Dict[int, asyncio.Future] = {}
198
+
199
+ # Reader threads
200
+ self._readers: Dict[str, threading.Thread] = {}
201
+ self._running = False
202
+
203
+ # Diagnostics cache
204
+ self._diagnostics: Dict[str, List[Diagnostic]] = {}
205
+
206
+ # Callbacks
207
+ self._on_diagnostics: Optional[Callable[[str, List[Diagnostic]], None]] = None
208
+
209
+ # Locks
210
+ self._lock = asyncio.Lock()
211
+
212
+ def _get_language(self, file_path: str) -> Optional[str]:
213
+ """Get language ID from file extension."""
214
+ ext = Path(file_path).suffix.lower()
215
+ return self.config.extensions.get(ext)
216
+
217
+ def _next_request_id(self) -> int:
218
+ """Get next request ID."""
219
+ self._request_id += 1
220
+ return self._request_id
221
+
222
+ async def start_server(self, language: str) -> bool:
223
+ """Start a language server."""
224
+ if language in self._processes:
225
+ return True # Already running
226
+
227
+ cmd = self.config.servers.get(language)
228
+ if not cmd:
229
+ return False
230
+
231
+ try:
232
+ # Start the language server process
233
+ process = subprocess.Popen(
234
+ cmd,
235
+ stdin=subprocess.PIPE,
236
+ stdout=subprocess.PIPE,
237
+ stderr=subprocess.PIPE,
238
+ cwd=str(self.project_root),
239
+ )
240
+
241
+ self._processes[language] = process
242
+
243
+ # Start reader thread
244
+ self._running = True
245
+ reader = threading.Thread(
246
+ target=self._read_responses,
247
+ args=(language, process),
248
+ daemon=True,
249
+ )
250
+ reader.start()
251
+ self._readers[language] = reader
252
+
253
+ # Initialize the server
254
+ await self._initialize(language)
255
+
256
+ return True
257
+
258
+ except (FileNotFoundError, OSError) as e:
259
+ return False
260
+
261
+ async def _initialize(self, language: str) -> None:
262
+ """Initialize a language server."""
263
+ process = self._processes.get(language)
264
+ if not process:
265
+ return
266
+
267
+ # Send initialize request
268
+ result = await self._send_request(
269
+ language,
270
+ "initialize",
271
+ {
272
+ "processId": os.getpid(),
273
+ "rootUri": f"file://{self.project_root}",
274
+ "capabilities": {
275
+ "textDocument": {
276
+ "publishDiagnostics": {"relatedInformation": True},
277
+ "completion": {"completionItem": {"snippetSupport": True}},
278
+ "hover": {},
279
+ "definition": {},
280
+ },
281
+ },
282
+ },
283
+ )
284
+
285
+ # Send initialized notification
286
+ await self._send_notification(language, "initialized", {})
287
+
288
+ def _read_responses(self, language: str, process: subprocess.Popen) -> None:
289
+ """Read responses from language server (runs in thread)."""
290
+ while self._running and process.poll() is None:
291
+ try:
292
+ # Read headers
293
+ headers = {}
294
+ while True:
295
+ line = process.stdout.readline().decode("utf-8")
296
+ if not line or line == "\r\n":
297
+ break
298
+ if ":" in line:
299
+ key, value = line.split(":", 1)
300
+ headers[key.strip().lower()] = value.strip()
301
+
302
+ # Read content
303
+ content_length = int(headers.get("content-length", 0))
304
+ if content_length > 0:
305
+ content = process.stdout.read(content_length).decode("utf-8")
306
+ message = json.loads(content)
307
+ self._handle_message(language, message)
308
+
309
+ except Exception:
310
+ break
311
+
312
+ def _handle_message(self, language: str, message: dict) -> None:
313
+ """Handle a message from language server."""
314
+ # Response to request
315
+ if "id" in message and "result" in message:
316
+ request_id = message["id"]
317
+ if request_id in self._pending_requests:
318
+ future = self._pending_requests.pop(request_id)
319
+ if not future.done():
320
+ future.set_result(message.get("result"))
321
+
322
+ # Error response
323
+ elif "id" in message and "error" in message:
324
+ request_id = message["id"]
325
+ if request_id in self._pending_requests:
326
+ future = self._pending_requests.pop(request_id)
327
+ if not future.done():
328
+ future.set_exception(Exception(message["error"].get("message", "LSP Error")))
329
+
330
+ # Notification
331
+ elif "method" in message:
332
+ method = message["method"]
333
+ params = message.get("params", {})
334
+
335
+ if method == "textDocument/publishDiagnostics":
336
+ self._handle_diagnostics(params)
337
+
338
+ def _handle_diagnostics(self, params: dict) -> None:
339
+ """Handle diagnostics notification."""
340
+ uri = params.get("uri", "")
341
+
342
+ # Convert URI to path
343
+ if uri.startswith("file://"):
344
+ file_path = uri[7:]
345
+ else:
346
+ file_path = uri
347
+
348
+ # Parse diagnostics
349
+ diagnostics = [Diagnostic.from_dict(d) for d in params.get("diagnostics", [])]
350
+
351
+ self._diagnostics[file_path] = diagnostics
352
+
353
+ # Call callback if set
354
+ if self._on_diagnostics:
355
+ self._on_diagnostics(file_path, diagnostics)
356
+
357
+ async def _send_request(
358
+ self,
359
+ language: str,
360
+ method: str,
361
+ params: dict,
362
+ ) -> Any:
363
+ """Send a request to language server."""
364
+ process = self._processes.get(language)
365
+ if not process or process.poll() is not None:
366
+ raise Exception(f"Language server not running: {language}")
367
+
368
+ request_id = self._next_request_id()
369
+
370
+ message = {
371
+ "jsonrpc": "2.0",
372
+ "id": request_id,
373
+ "method": method,
374
+ "params": params,
375
+ }
376
+
377
+ content = json.dumps(message)
378
+ header = f"Content-Length: {len(content)}\r\n\r\n"
379
+
380
+ # Create future for response
381
+ future = asyncio.get_event_loop().create_future()
382
+ self._pending_requests[request_id] = future
383
+
384
+ # Send request
385
+ process.stdin.write(header.encode() + content.encode())
386
+ process.stdin.flush()
387
+
388
+ # Wait for response
389
+ try:
390
+ return await asyncio.wait_for(future, timeout=self.config.timeout)
391
+ except asyncio.TimeoutError:
392
+ self._pending_requests.pop(request_id, None)
393
+ raise Exception(f"LSP request timeout: {method}")
394
+
395
+ async def _send_notification(
396
+ self,
397
+ language: str,
398
+ method: str,
399
+ params: dict,
400
+ ) -> None:
401
+ """Send a notification to language server."""
402
+ process = self._processes.get(language)
403
+ if not process or process.poll() is not None:
404
+ return
405
+
406
+ message = {
407
+ "jsonrpc": "2.0",
408
+ "method": method,
409
+ "params": params,
410
+ }
411
+
412
+ content = json.dumps(message)
413
+ header = f"Content-Length: {len(content)}\r\n\r\n"
414
+
415
+ process.stdin.write(header.encode() + content.encode())
416
+ process.stdin.flush()
417
+
418
+ async def open_file(self, file_path: str) -> None:
419
+ """Notify server that a file is opened."""
420
+ abs_path = self.project_root / file_path
421
+ language = self._get_language(file_path)
422
+
423
+ if not language:
424
+ return
425
+
426
+ if language not in self._processes:
427
+ await self.start_server(language)
428
+
429
+ if not abs_path.exists():
430
+ return
431
+
432
+ content = abs_path.read_text(errors="replace")
433
+
434
+ await self._send_notification(
435
+ language,
436
+ "textDocument/didOpen",
437
+ {
438
+ "textDocument": {
439
+ "uri": f"file://{abs_path}",
440
+ "languageId": language,
441
+ "version": 1,
442
+ "text": content,
443
+ }
444
+ },
445
+ )
446
+
447
+ async def close_file(self, file_path: str) -> None:
448
+ """Notify server that a file is closed."""
449
+ abs_path = self.project_root / file_path
450
+ language = self._get_language(file_path)
451
+
452
+ if not language or language not in self._processes:
453
+ return
454
+
455
+ await self._send_notification(
456
+ language,
457
+ "textDocument/didClose",
458
+ {
459
+ "textDocument": {
460
+ "uri": f"file://{abs_path}",
461
+ }
462
+ },
463
+ )
464
+
465
+ async def update_file(self, file_path: str, content: str) -> None:
466
+ """Notify server of file changes."""
467
+ abs_path = self.project_root / file_path
468
+ language = self._get_language(file_path)
469
+
470
+ if not language or language not in self._processes:
471
+ return
472
+
473
+ await self._send_notification(
474
+ language,
475
+ "textDocument/didChange",
476
+ {
477
+ "textDocument": {
478
+ "uri": f"file://{abs_path}",
479
+ "version": 2, # Simplified versioning
480
+ },
481
+ "contentChanges": [{"text": content}],
482
+ },
483
+ )
484
+
485
+ async def get_diagnostics(self, file_path: str) -> List[Diagnostic]:
486
+ """Get cached diagnostics for a file."""
487
+ abs_path = str(self.project_root / file_path)
488
+ return self._diagnostics.get(abs_path, [])
489
+
490
+ async def get_all_diagnostics(self) -> Dict[str, List[Diagnostic]]:
491
+ """Get all cached diagnostics."""
492
+ return dict(self._diagnostics)
493
+
494
+ def on_diagnostics(
495
+ self,
496
+ callback: Callable[[str, List[Diagnostic]], None],
497
+ ) -> None:
498
+ """Set callback for diagnostic updates."""
499
+ self._on_diagnostics = callback
500
+
501
+ async def shutdown(self) -> None:
502
+ """Shutdown all language servers."""
503
+ self._running = False
504
+
505
+ for language, process in self._processes.items():
506
+ try:
507
+ # Send shutdown request
508
+ await self._send_request(language, "shutdown", {})
509
+ await self._send_notification(language, "exit", {})
510
+ except Exception:
511
+ pass
512
+
513
+ # Terminate process
514
+ try:
515
+ process.terminate()
516
+ process.wait(timeout=5.0)
517
+ except Exception:
518
+ process.kill()
519
+
520
+ self._processes.clear()
521
+ self._readers.clear()
522
+ self._diagnostics.clear()
523
+
524
+ def __enter__(self) -> "LSPClient":
525
+ return self
526
+
527
+ def __exit__(self, *args) -> None:
528
+ asyncio.run(self.shutdown())
529
+
530
+
531
+ async def get_file_diagnostics(
532
+ project_root: Path,
533
+ file_path: str,
534
+ ) -> List[Diagnostic]:
535
+ """Convenience function to get diagnostics for a single file."""
536
+ client = LSPClient(project_root)
537
+
538
+ try:
539
+ await client.open_file(file_path)
540
+ # Wait a bit for diagnostics
541
+ await asyncio.sleep(1.0)
542
+ return await client.get_diagnostics(file_path)
543
+ finally:
544
+ await client.shutdown()