codexa 0.4.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.
Files changed (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,609 @@
1
+ """LSP server — Language Server Protocol implementation for CodexA.
2
+
3
+ Provides a JSON-RPC 2.0 LSP server over stdio so any LSP-compatible editor
4
+ (VS Code, Neovim, Sublime Text, JetBrains, Emacs) gets:
5
+
6
+ - **textDocument/hover** — explain symbol under cursor
7
+ - **textDocument/completion** — semantic search suggestions
8
+ - **textDocument/publishDiagnostics** — quality issues (complexity, dead code, security)
9
+ - **workspace/symbol** — global symbol search
10
+ - **textDocument/definition** — jump to symbol definition
11
+ - **textDocument/references** — find all references
12
+ - **codexa/search** — custom semantic search request
13
+
14
+ The server reuses the same service layer (SearchService, IndexingService,
15
+ ContextProvider) as the MCP server, bridge, and CLI.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import re
22
+ import sys
23
+ import threading
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from semantic_code_intelligence.config.settings import AppConfig, load_config
28
+ from semantic_code_intelligence.utils.logging import get_logger
29
+
30
+ logger = get_logger("lsp")
31
+
32
+ # ── LSP message framing (Content-Length headers) ──────────────────────
33
+
34
+ def _read_lsp_message() -> dict[str, Any] | None:
35
+ """Read a single LSP message from stdin using Content-Length framing."""
36
+ headers: dict[str, str] = {}
37
+ while True:
38
+ line = sys.stdin.buffer.readline()
39
+ if not line:
40
+ return None
41
+ line_str = line.decode("utf-8").rstrip("\r\n")
42
+ if line_str == "":
43
+ break # blank line separates headers from body
44
+ if ":" in line_str:
45
+ key, value = line_str.split(":", 1)
46
+ headers[key.strip()] = value.strip()
47
+
48
+ content_length = int(headers.get("Content-Length", "0"))
49
+ if content_length == 0:
50
+ return None
51
+
52
+ body = sys.stdin.buffer.read(content_length)
53
+ try:
54
+ return json.loads(body.decode("utf-8"))
55
+ except json.JSONDecodeError:
56
+ return None
57
+
58
+
59
+ def _write_lsp_message(msg: dict[str, Any]) -> None:
60
+ """Write a single LSP message to stdout with Content-Length framing."""
61
+ body = json.dumps(msg, ensure_ascii=False)
62
+ encoded = body.encode("utf-8")
63
+ header = f"Content-Length: {len(encoded)}\r\n\r\n"
64
+ sys.stdout.buffer.write(header.encode("utf-8"))
65
+ sys.stdout.buffer.write(encoded)
66
+ sys.stdout.buffer.flush()
67
+
68
+
69
+ # ── LSP capabilities & constants ─────────────────────────────────────
70
+
71
+ _SERVER_INFO = {
72
+ "name": "codexa-lsp",
73
+ "version": "0.29.0",
74
+ }
75
+
76
+ _SERVER_CAPABILITIES = {
77
+ "textDocumentSync": {
78
+ "openClose": True,
79
+ "change": 1, # Full document sync
80
+ "save": {"includeText": True},
81
+ },
82
+ "hoverProvider": True,
83
+ "completionProvider": {
84
+ "triggerCharacters": [".", "(", ":", " "],
85
+ "resolveProvider": False,
86
+ },
87
+ "definitionProvider": True,
88
+ "referencesProvider": True,
89
+ "workspaceSymbolProvider": True,
90
+ "diagnosticProvider": {
91
+ "interFileDependencies": False,
92
+ "workspaceDiagnostics": False,
93
+ },
94
+ }
95
+
96
+
97
+ # ── Document store (open documents in memory) ────────────────────────
98
+
99
+ class _DocumentStore:
100
+ """Track open text documents for hover/completion context."""
101
+
102
+ def __init__(self) -> None:
103
+ self._docs: dict[str, str] = {} # uri → content
104
+
105
+ def open(self, uri: str, text: str) -> None:
106
+ self._docs[uri] = text
107
+
108
+ def update(self, uri: str, text: str) -> None:
109
+ self._docs[uri] = text
110
+
111
+ def close(self, uri: str) -> None:
112
+ self._docs.pop(uri, None)
113
+
114
+ def get(self, uri: str) -> str | None:
115
+ return self._docs.get(uri)
116
+
117
+ def get_word_at(self, uri: str, line: int, character: int) -> str:
118
+ """Extract the word (symbol name) at a given position."""
119
+ text = self._docs.get(uri)
120
+ if not text:
121
+ return ""
122
+ lines = text.splitlines()
123
+ if line >= len(lines):
124
+ return ""
125
+ row = lines[line]
126
+ if character >= len(row):
127
+ return ""
128
+ # Walk left and right to find word boundaries
129
+ start = character
130
+ while start > 0 and (row[start - 1].isalnum() or row[start - 1] == "_"):
131
+ start -= 1
132
+ end = character
133
+ while end < len(row) and (row[end].isalnum() or row[end] == "_"):
134
+ end += 1
135
+ return row[start:end]
136
+
137
+ def uri_to_path(self, uri: str) -> str:
138
+ """Convert a file:// URI to a filesystem path."""
139
+ if uri.startswith("file:///"):
140
+ # Windows: file:///C:/... → C:/...
141
+ path = uri[8:] if len(uri) > 9 and uri[9] == ":" else uri[7:]
142
+ elif uri.startswith("file://"):
143
+ path = uri[7:]
144
+ else:
145
+ path = uri
146
+ # Decode percent-encoding
147
+ import urllib.parse
148
+ return urllib.parse.unquote(path)
149
+
150
+
151
+ # ── LSP Server ───────────────────────────────────────────────────────
152
+
153
+ class LSPServer:
154
+ """CodexA Language Server Protocol server.
155
+
156
+ Reads JSON-RPC 2.0 messages from stdin and writes responses to stdout,
157
+ using the standard LSP Content-Length framing.
158
+ """
159
+
160
+ def __init__(self, project_root: Path) -> None:
161
+ self._root = project_root.resolve()
162
+ self._docs = _DocumentStore()
163
+ self._initialized = False
164
+ self._shutdown = False
165
+
166
+ # ── main loop ─────────────────────────────────────────────────
167
+
168
+ def run(self) -> None:
169
+ """Run the LSP server — blocks until stdin closes or shutdown."""
170
+ logger.info("LSP server starting for %s", self._root)
171
+
172
+ while not self._shutdown:
173
+ msg = _read_lsp_message()
174
+ if msg is None:
175
+ break
176
+ response = self._handle(msg)
177
+ if response:
178
+ _write_lsp_message(response)
179
+
180
+ logger.info("LSP server stopped.")
181
+
182
+ # ── dispatch ──────────────────────────────────────────────────
183
+
184
+ def _handle(self, msg: dict[str, Any]) -> dict[str, Any] | None:
185
+ """Route a JSON-RPC message to the correct handler."""
186
+ method = msg.get("method", "")
187
+ req_id = msg.get("id")
188
+ params = msg.get("params", {})
189
+
190
+ # Lifecycle
191
+ if method == "initialize":
192
+ return self._on_initialize(req_id, params)
193
+ if method == "initialized":
194
+ self._initialized = True
195
+ return None # notification
196
+ if method == "shutdown":
197
+ self._shutdown = True
198
+ return _ok(req_id, None)
199
+ if method == "exit":
200
+ return None
201
+
202
+ # Document sync
203
+ if method == "textDocument/didOpen":
204
+ self._on_did_open(params)
205
+ return None
206
+ if method == "textDocument/didChange":
207
+ self._on_did_change(params)
208
+ return None
209
+ if method == "textDocument/didClose":
210
+ self._on_did_close(params)
211
+ return None
212
+ if method == "textDocument/didSave":
213
+ self._on_did_save(params)
214
+ return None
215
+
216
+ # Language features
217
+ if method == "textDocument/hover":
218
+ return self._on_hover(req_id, params)
219
+ if method == "textDocument/completion":
220
+ return self._on_completion(req_id, params)
221
+ if method == "textDocument/definition":
222
+ return self._on_definition(req_id, params)
223
+ if method == "textDocument/references":
224
+ return self._on_references(req_id, params)
225
+ if method == "workspace/symbol":
226
+ return self._on_workspace_symbol(req_id, params)
227
+
228
+ # Custom CodexA methods
229
+ if method == "codexa/search":
230
+ return self._on_codex_search(req_id, params)
231
+ if method == "codexa/quality":
232
+ return self._on_codex_quality(req_id, params)
233
+
234
+ # Unknown method
235
+ if req_id is not None:
236
+ return _error(req_id, -32601, f"Method not found: {method}")
237
+ return None # unknown notification — ignore
238
+
239
+ # ── lifecycle ─────────────────────────────────────────────────
240
+
241
+ def _on_initialize(
242
+ self, req_id: Any, params: dict[str, Any]
243
+ ) -> dict[str, Any]:
244
+ return _ok(req_id, {
245
+ "capabilities": _SERVER_CAPABILITIES,
246
+ "serverInfo": _SERVER_INFO,
247
+ })
248
+
249
+ # ── document sync ─────────────────────────────────────────────
250
+
251
+ def _on_did_open(self, params: dict[str, Any]) -> None:
252
+ td = params.get("textDocument", {})
253
+ self._docs.open(td.get("uri", ""), td.get("text", ""))
254
+
255
+ def _on_did_change(self, params: dict[str, Any]) -> None:
256
+ td = params.get("textDocument", {})
257
+ changes = params.get("contentChanges", [])
258
+ if changes:
259
+ self._docs.update(td.get("uri", ""), changes[-1].get("text", ""))
260
+
261
+ def _on_did_close(self, params: dict[str, Any]) -> None:
262
+ td = params.get("textDocument", {})
263
+ self._docs.close(td.get("uri", ""))
264
+
265
+ def _on_did_save(self, params: dict[str, Any]) -> None:
266
+ # Trigger diagnostics on save
267
+ td = params.get("textDocument", {})
268
+ uri = td.get("uri", "")
269
+ file_path = self._docs.uri_to_path(uri)
270
+ self._publish_diagnostics(uri, file_path)
271
+
272
+ # ── hover (explain symbol) ────────────────────────────────────
273
+
274
+ def _on_hover(
275
+ self, req_id: Any, params: dict[str, Any]
276
+ ) -> dict[str, Any]:
277
+ td = params.get("textDocument", {})
278
+ pos = params.get("position", {})
279
+ uri = td.get("uri", "")
280
+ line = pos.get("line", 0)
281
+ char = pos.get("character", 0)
282
+
283
+ word = self._docs.get_word_at(uri, line, char)
284
+ if not word:
285
+ return _ok(req_id, None)
286
+
287
+ try:
288
+ from semantic_code_intelligence.storage.symbol_registry import SymbolRegistry
289
+
290
+ index_dir = AppConfig.index_dir(self._root)
291
+ registry = SymbolRegistry.load(index_dir)
292
+ entries = registry.find_by_name(word)
293
+
294
+ if not entries:
295
+ return _ok(req_id, None)
296
+
297
+ # Build hover markdown
298
+ parts = [f"**{word}**\n"]
299
+ for e in entries[:5]:
300
+ parts.append(f"- `{e.kind}` in `{e.file_path}` "
301
+ f"(L{e.start_line}–{e.end_line})")
302
+ if e.parameters:
303
+ parts.append(f" Parameters: `{e.parameters}`")
304
+ markdown = "\n".join(parts)
305
+
306
+ return _ok(req_id, {
307
+ "contents": {"kind": "markdown", "value": markdown},
308
+ })
309
+ except Exception as exc:
310
+ logger.debug("Hover error: %s", exc)
311
+ return _ok(req_id, None)
312
+
313
+ # ── completion (semantic search) ──────────────────────────────
314
+
315
+ def _on_completion(
316
+ self, req_id: Any, params: dict[str, Any]
317
+ ) -> dict[str, Any]:
318
+ td = params.get("textDocument", {})
319
+ pos = params.get("position", {})
320
+ uri = td.get("uri", "")
321
+ line = pos.get("line", 0)
322
+ char = pos.get("character", 0)
323
+
324
+ word = self._docs.get_word_at(uri, line, char)
325
+ if not word or len(word) < 2:
326
+ return _ok(req_id, {"isIncomplete": False, "items": []})
327
+
328
+ try:
329
+ from semantic_code_intelligence.storage.symbol_registry import SymbolRegistry
330
+
331
+ index_dir = AppConfig.index_dir(self._root)
332
+ registry = SymbolRegistry.load(index_dir)
333
+ entries = registry.find_by_name(word)
334
+
335
+ items = []
336
+ for e in entries[:20]:
337
+ kind = _symbol_kind_to_completion(e.kind)
338
+ detail = f"{e.file_path}:{e.start_line}"
339
+ items.append({
340
+ "label": e.name,
341
+ "kind": kind,
342
+ "detail": detail,
343
+ "documentation": {
344
+ "kind": "markdown",
345
+ "value": f"`{e.kind}` — {e.language}",
346
+ },
347
+ })
348
+
349
+ return _ok(req_id, {
350
+ "isIncomplete": len(entries) > 20,
351
+ "items": items,
352
+ })
353
+ except Exception as exc:
354
+ logger.debug("Completion error: %s", exc)
355
+ return _ok(req_id, {"isIncomplete": False, "items": []})
356
+
357
+ # ── definition ────────────────────────────────────────────────
358
+
359
+ def _on_definition(
360
+ self, req_id: Any, params: dict[str, Any]
361
+ ) -> dict[str, Any]:
362
+ td = params.get("textDocument", {})
363
+ pos = params.get("position", {})
364
+ uri = td.get("uri", "")
365
+ line = pos.get("line", 0)
366
+ char = pos.get("character", 0)
367
+
368
+ word = self._docs.get_word_at(uri, line, char)
369
+ if not word:
370
+ return _ok(req_id, [])
371
+
372
+ try:
373
+ from semantic_code_intelligence.storage.symbol_registry import SymbolRegistry
374
+
375
+ index_dir = AppConfig.index_dir(self._root)
376
+ registry = SymbolRegistry.load(index_dir)
377
+ entries = registry.find_by_name(word)
378
+
379
+ locations = []
380
+ for e in entries[:10]:
381
+ loc_uri = _path_to_uri(e.file_path, self._root)
382
+ locations.append({
383
+ "uri": loc_uri,
384
+ "range": {
385
+ "start": {"line": max(0, e.start_line - 1), "character": 0},
386
+ "end": {"line": max(0, e.end_line - 1), "character": 0},
387
+ },
388
+ })
389
+ return _ok(req_id, locations)
390
+ except Exception as exc:
391
+ logger.debug("Definition error: %s", exc)
392
+ return _ok(req_id, [])
393
+
394
+ # ── references ────────────────────────────────────────────────
395
+
396
+ def _on_references(
397
+ self, req_id: Any, params: dict[str, Any]
398
+ ) -> dict[str, Any]:
399
+ td = params.get("textDocument", {})
400
+ pos = params.get("position", {})
401
+ uri = td.get("uri", "")
402
+ line = pos.get("line", 0)
403
+ char = pos.get("character", 0)
404
+
405
+ word = self._docs.get_word_at(uri, line, char)
406
+ if not word:
407
+ return _ok(req_id, [])
408
+
409
+ try:
410
+ from semantic_code_intelligence.services.search_service import search_codebase
411
+
412
+ results = search_codebase(
413
+ query=word, project_root=self._root,
414
+ top_k=20, mode="keyword",
415
+ )
416
+ locations = []
417
+ for r in results:
418
+ loc_uri = _path_to_uri(r.file_path, self._root)
419
+ locations.append({
420
+ "uri": loc_uri,
421
+ "range": {
422
+ "start": {"line": max(0, r.start_line - 1), "character": 0},
423
+ "end": {"line": max(0, r.end_line - 1), "character": 0},
424
+ },
425
+ })
426
+ return _ok(req_id, locations)
427
+ except Exception as exc:
428
+ logger.debug("References error: %s", exc)
429
+ return _ok(req_id, [])
430
+
431
+ # ── workspace/symbol ──────────────────────────────────────────
432
+
433
+ def _on_workspace_symbol(
434
+ self, req_id: Any, params: dict[str, Any]
435
+ ) -> dict[str, Any]:
436
+ query = params.get("query", "")
437
+ if not query or len(query) < 2:
438
+ return _ok(req_id, [])
439
+
440
+ try:
441
+ from semantic_code_intelligence.storage.symbol_registry import SymbolRegistry
442
+
443
+ index_dir = AppConfig.index_dir(self._root)
444
+ registry = SymbolRegistry.load(index_dir)
445
+ entries = registry.find_by_name(query)
446
+
447
+ symbols = []
448
+ for e in entries[:50]:
449
+ loc_uri = _path_to_uri(e.file_path, self._root)
450
+ symbols.append({
451
+ "name": e.name,
452
+ "kind": _symbol_kind_to_lsp(e.kind),
453
+ "location": {
454
+ "uri": loc_uri,
455
+ "range": {
456
+ "start": {"line": max(0, e.start_line - 1), "character": 0},
457
+ "end": {"line": max(0, e.end_line - 1), "character": 0},
458
+ },
459
+ },
460
+ "containerName": e.parent or "",
461
+ })
462
+ return _ok(req_id, symbols)
463
+ except Exception as exc:
464
+ logger.debug("Workspace symbol error: %s", exc)
465
+ return _ok(req_id, [])
466
+
467
+ # ── custom codexa/search ───────────────────────────────────────
468
+
469
+ def _on_codex_search(
470
+ self, req_id: Any, params: dict[str, Any]
471
+ ) -> dict[str, Any]:
472
+ query = params.get("query", "")
473
+ top_k = params.get("top_k", 10)
474
+ mode = params.get("mode", "semantic")
475
+
476
+ if not query:
477
+ return _error(req_id, -32602, "Missing 'query' parameter")
478
+
479
+ try:
480
+ from semantic_code_intelligence.services.search_service import search_codebase
481
+
482
+ results = search_codebase(
483
+ query=query, project_root=self._root,
484
+ top_k=top_k, mode=mode,
485
+ )
486
+ return _ok(req_id, [r.to_dict() for r in results])
487
+ except Exception as exc:
488
+ return _error(req_id, -32603, str(exc))
489
+
490
+ # ── custom codexa/quality ──────────────────────────────────────
491
+
492
+ def _on_codex_quality(
493
+ self, req_id: Any, params: dict[str, Any]
494
+ ) -> dict[str, Any]:
495
+ file_path = params.get("file_path", "")
496
+ if not file_path:
497
+ return _error(req_id, -32602, "Missing 'file_path' parameter")
498
+
499
+ try:
500
+ from semantic_code_intelligence.ci.quality import (
501
+ analyze_complexity,
502
+ detect_dead_code,
503
+ )
504
+
505
+ issues = analyze_complexity(file_path) + detect_dead_code(file_path)
506
+ return _ok(req_id, [i.to_dict() for i in issues])
507
+ except Exception as exc:
508
+ return _error(req_id, -32603, str(exc))
509
+
510
+ # ── diagnostics ───────────────────────────────────────────────
511
+
512
+ def _publish_diagnostics(self, uri: str, file_path: str) -> None:
513
+ """Compute and publish diagnostics for a file."""
514
+ if not file_path or not Path(file_path).is_file():
515
+ return
516
+
517
+ diagnostics: list[dict[str, Any]] = []
518
+ try:
519
+ from semantic_code_intelligence.ci.quality import analyze_complexity
520
+
521
+ results = analyze_complexity(file_path)
522
+ for r in results:
523
+ if r.rating in ("high", "very_high"):
524
+ diagnostics.append({
525
+ "range": {
526
+ "start": {"line": max(0, r.start_line - 1), "character": 0},
527
+ "end": {"line": max(0, r.end_line - 1), "character": 0},
528
+ },
529
+ "severity": 2 if r.rating == "high" else 1, # Warning/Error
530
+ "source": "codexa",
531
+ "message": f"High cyclomatic complexity ({r.complexity}) "
532
+ f"in {r.symbol_name}",
533
+ })
534
+ except Exception as exc:
535
+ logger.debug("Diagnostics error: %s", exc)
536
+
537
+ if diagnostics or True: # always publish (clears old diagnostics)
538
+ _write_lsp_message({
539
+ "jsonrpc": "2.0",
540
+ "method": "textDocument/publishDiagnostics",
541
+ "params": {"uri": uri, "diagnostics": diagnostics},
542
+ })
543
+
544
+
545
+ # ── JSON-RPC helpers ─────────────────────────────────────────────────
546
+
547
+ def _ok(req_id: Any, result: Any) -> dict[str, Any]:
548
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
549
+
550
+
551
+ def _error(req_id: Any, code: int, message: str) -> dict[str, Any]:
552
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
553
+
554
+
555
+ # ── URI / kind helpers ───────────────────────────────────────────────
556
+
557
+ def _path_to_uri(file_path: str, project_root: Path) -> str:
558
+ """Convert a file path to a file:// URI."""
559
+ import urllib.parse
560
+ p = Path(file_path)
561
+ if not p.is_absolute():
562
+ p = project_root / p
563
+ return "file:///" + urllib.parse.quote(str(p).replace("\\", "/"), safe="/:")
564
+
565
+
566
+ def _symbol_kind_to_lsp(kind: str) -> int:
567
+ """Map CodexA symbol kinds to LSP SymbolKind numbers."""
568
+ mapping = {
569
+ "function": 12, # Function
570
+ "method": 6, # Method
571
+ "class": 5, # Class
572
+ "module": 2, # Module
573
+ "variable": 13, # Variable
574
+ "constant": 14, # Constant
575
+ "interface": 11, # Interface
576
+ "enum": 10, # Enum
577
+ "property": 7, # Property
578
+ "constructor": 9, # Constructor
579
+ }
580
+ return mapping.get(kind.lower(), 12)
581
+
582
+
583
+ def _symbol_kind_to_completion(kind: str) -> int:
584
+ """Map CodexA symbol kinds to LSP CompletionItemKind numbers."""
585
+ mapping = {
586
+ "function": 3, # Function
587
+ "method": 2, # Method
588
+ "class": 7, # Class
589
+ "module": 9, # Module
590
+ "variable": 6, # Variable
591
+ "constant": 21, # Constant
592
+ "interface": 8, # Interface
593
+ "enum": 13, # Enum
594
+ "property": 10, # Property
595
+ "constructor": 4, # Constructor
596
+ }
597
+ return mapping.get(kind.lower(), 3)
598
+
599
+
600
+ # ── Public entry point ───────────────────────────────────────────────
601
+
602
+ def run_lsp_server(project_root: Path) -> None:
603
+ """Run the CodexA LSP server.
604
+
605
+ Reads JSON-RPC requests from stdin and writes responses to stdout
606
+ using standard LSP Content-Length framing.
607
+ """
608
+ server = LSPServer(project_root)
609
+ server.run()