Work on clangd-query

This commit is contained in:
2026-03-09 06:47:43 -04:00
parent 642e3aca0a
commit 8f9f87aa8a
5 changed files with 158 additions and 28 deletions

View File

@@ -47,11 +47,11 @@ import time
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
WORKSPACE_FILE = os.path.join(PROJECT_DIR, "Integration.code-workspace")
SOCKET_PATH = os.path.join(PROJECT_DIR, ".clangd-query.sock")
PID_FILE = os.path.join(PROJECT_DIR, ".clangd-query.pid")
DAEMON_DIR = os.path.join(PROJECT_DIR, ".clangd-query")
SOCKET_PATH = os.path.join(DAEMON_DIR, "sock")
PID_FILE = os.path.join(DAEMON_DIR, "pid")
LOG_FILE = os.path.join(DAEMON_DIR, "log")
# A small file to open on startup to trigger background index loading
TRIGGER_FILE = os.path.join(PROJECT_DIR, "luprex", "cpp", "core", "util.hpp")
def read_workspace_config():
@@ -144,6 +144,23 @@ def format_uri(uri):
# Daemon process
# ============================================================
def truncate_text(obj):
"""Deep-copy obj, truncating any 'text' string values to 80 chars."""
if isinstance(obj, dict):
return {k: (v[:80] + "..." if k == "text" and isinstance(v, str) and len(v) > 80
else truncate_text(v))
for k, v in obj.items()}
if isinstance(obj, list):
return [truncate_text(v) for v in obj]
return obj
def lsp_log(direction, msg):
"""Log an LSP message to the log file."""
with open(LOG_FILE, "a") as f:
f.write(f"{direction} {json.dumps(truncate_text(msg))}\n")
class ClangdDaemon:
"""Manages a clangd subprocess and serves queries over a Unix socket."""
@@ -180,8 +197,9 @@ class ClangdDaemon:
self.events = {}
self.index_loaded = threading.Event()
self.opened_files = set()
self.diagnostics = {} # uri -> list of diagnostics
self.diag_events = {} # uri -> threading.Event
self.file_versions = {}
self.cached_diagnostics = {} # uri -> list of diagnostics
self.file_status_events = {} # uri -> threading.Event (set when idle)
threading.Thread(target=self._stdout_reader, daemon=True).start()
threading.Thread(target=self._stderr_reader, daemon=True).start()
@@ -191,13 +209,18 @@ class ClangdDaemon:
msg = read_lsp_message(self.proc.stdout)
if msg is None:
break
# Handle server-initiated diagnostics notifications
lsp_log("<<<", msg)
if msg.get("method") == "textDocument/publishDiagnostics":
uri = msg["params"]["uri"]
with self.lock:
self.diagnostics[uri] = msg["params"]["diagnostics"]
if uri in self.diag_events:
self.diag_events[uri].set()
self.cached_diagnostics[uri] = msg["params"]["diagnostics"]
continue
if msg.get("method") == "textDocument/clangd.fileStatus":
uri = msg["params"]["uri"]
state = msg["params"]["state"]
with self.lock:
if state == "idle" and uri in self.file_status_events:
self.file_status_events[uri].set()
continue
msg_id = msg.get("id")
if msg_id is not None and msg_id in self.pending:
@@ -219,6 +242,7 @@ class ClangdDaemon:
self.pending[rid] = None
self.events[rid] = event
msg = {"jsonrpc": "2.0", "id": rid, "method": method, "params": params}
lsp_log(">>>", msg)
self.proc.stdin.write(make_lsp_message(msg))
self.proc.stdin.flush()
event.wait(timeout=30)
@@ -229,6 +253,7 @@ class ClangdDaemon:
def send_notification(self, method, params):
msg = {"jsonrpc": "2.0", "method": method, "params": params}
lsp_log(">>>", msg)
self.proc.stdin.write(make_lsp_message(msg))
self.proc.stdin.flush()
@@ -237,17 +262,36 @@ class ClangdDaemon:
"processId": os.getpid(),
"rootUri": f"file://{PROJECT_DIR}",
"capabilities": {},
"initializationOptions": {
"clangdFileStatus": True,
},
})
self.send_notification("initialized", {})
self.open_file(TRIGGER_FILE)
self.send_notification("textDocument/didOpen", {
"textDocument": {
"uri": f"file://{PROJECT_DIR}/fake.cpp",
"languageId": "cpp",
"version": 1,
"text": "",
}
})
def open_file(self, filepath):
abs_path = os.path.abspath(filepath)
if abs_path in self.opened_files:
return
self.opened_files.add(abs_path)
with open(abs_path) as f:
text = f.read()
if abs_path in self.opened_files:
self.file_versions[abs_path] = self.file_versions.get(abs_path, 1) + 1
self.send_notification("textDocument/didChange", {
"textDocument": {
"uri": f"file://{abs_path}",
"version": self.file_versions[abs_path],
},
"contentChanges": [{"text": text}],
})
return
self.opened_files.add(abs_path)
self.file_versions[abs_path] = 1
self.send_notification("textDocument/didOpen", {
"textDocument": {
"uri": f"file://{abs_path}",
@@ -261,6 +305,8 @@ class ClangdDaemon:
"""Handle a query dict and return a result dict."""
cmd = query["command"]
args = query.get("args", [])
with open(LOG_FILE, "a") as f:
f.write(f"\n\n\n------------------------ QUERY: {cmd} {' '.join(args)} ----------------\n\n\n")
if cmd == "symbol":
self.index_loaded.wait(timeout=15)
@@ -271,14 +317,25 @@ class ClangdDaemon:
filepath = args[0]
abs_path = os.path.abspath(filepath)
uri = f"file://{abs_path}"
# Set up event before opening so we don't miss the notification
with open(abs_path) as f:
text = f.read()
event = threading.Event()
with self.lock:
self.diag_events[uri] = threading.Event()
self.open_file(abs_path)
self.diag_events[uri].wait(timeout=30)
self.cached_diagnostics.pop(uri, None)
self.file_status_events[uri] = event
self.send_notification("textDocument/didOpen", {
"textDocument": {"uri": uri, "languageId": "cpp",
"version": 1, "text": text}
})
event.wait(timeout=30)
with self.lock:
diags = self.diagnostics.get(uri, [])
self.diag_events.pop(uri, None)
self.file_status_events.pop(uri, None)
diags = self.cached_diagnostics.pop(uri, None)
self.send_notification("textDocument/didClose", {
"textDocument": {"uri": uri}
})
if diags is None:
return {"error": "Timed out waiting for diagnostics"}
return {"result": diags}
if cmd == "definition":
@@ -343,6 +400,7 @@ def kill_existing_daemon():
def run_daemon():
"""Run the daemon process: start clangd, listen on Unix socket."""
os.makedirs(DAEMON_DIR, exist_ok=True)
kill_existing_daemon()
# Write our PID