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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- 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()
|