add full El LSP — completions, hover, go-to-def, diagnostics, VSCode extension

This commit is contained in:
Will Anderson
2026-05-03 15:59:42 -05:00
parent cefff5b891
commit 9aa0c49d0c
11 changed files with 1381 additions and 930 deletions
+86 -85
View File
@@ -1,124 +1,125 @@
#!/usr/bin/env bash
# tools/lsp/build.sh — Build the El LSP server binary.
# tools/lsp/build.sh — Build the El LSP server binary end-to-end.
#
# Pipeline:
# 1. Compile el-lsp.el → C source (using elc)
# 2. Compile C source → binary (using cc with el_runtime.c)
#
# Output: tools/lsp/dist/el-lsp
#
# Usage:
# cd tools/lsp && ./build.sh
# EL_HOME=/path/to/el ./build.sh # if el root is elsewhere
# ELC=/path/to/custom/elc ./build.sh # use a specific compiler binary
#
# Prerequisites:
# - elc binary (either on PATH or ELC env var)
# - el-compiler/runtime/el_runtime.c
# - dist/platform/elc (the El self-hosted compiler)
# - el-compiler/runtime/el_runtime.c + el_runtime.h
# - cc (clang or gcc), libcurl, pthreads
#
# NOTE — seed primitive __read_n:
# el-lsp.el calls __read_n(n: Int) -> String, which reads exactly n bytes
# from stdin. This primitive must be present in el_runtime.c and declared
# in el_runtime.h for the compiled binary to work correctly.
# Usage:
# cd <el-root>
# ./tools/lsp/build.sh
#
# Until __read_n is added to the runtime, the LSP will compile and link
# successfully but lsp_read_message() will silently use the __read_n symbol
# — causing a linker error. The seed agent must:
# 1. Add el_val_t __read_n(el_val_t n) to el-compiler/runtime/el_runtime.c
# 2. Declare el_val_t __read_n(el_val_t n) in el-compiler/runtime/el_runtime.h
# 3. Add "__read_n" with arity 1 to the builtin_arity table in
# el-compiler/src/codegen.el
# 4. Rebuild the elc binary
#
# Implementation sketch for __read_n in C:
#
# el_val_t __read_n(el_val_t n) {
# int64_t count = (int64_t)n;
# if (count <= 0) return EL_STR("");
# char* buf = malloc(count + 1);
# if (!buf) return EL_STR("");
# size_t total = 0;
# while (total < (size_t)count) {
# size_t got = fread(buf + total, 1, (size_t)count - total, stdin);
# if (got == 0) break;
# total += got;
# }
# buf[total] = '\0';
# return EL_STR(buf);
# }
# Or from anywhere:
# EL_HOME=/path/to/el-root ./tools/lsp/build.sh
set -euo pipefail
cd "$(dirname "$0")"
# ── Locate el root ─────────────────────────────────────────────────────────────
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
# ── Locate el root ─────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
EL_HOME="${EL_HOME:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
ELC="${ELC:-${EL_HOME}/dist/platform/elc}"
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
LSP_DIR="${SCRIPT_DIR}"
OUT_DIR="${LSP_DIR}/dist"
# ── Validate prerequisites ─────────────────────────────────────────────────
echo "==> Checking prerequisites..."
echo " EL_HOME = ${EL_HOME}"
echo " elc = ${ELC}"
echo " runtime = ${RUNTIME_DIR}"
# ── Validate prerequisites ─────────────────────────────────────────────────────
if [ ! -x "${ELC}" ]; then
echo "error: elc not found at ${ELC}" >&2
echo " Set ELC=/path/to/elc or EL_HOME=/path/to/el-root" >&2
echo " Build it first: cd <el-root> && make (or compile elc-bootstrap.c)" >&2
exit 1
fi
if [ ! -f "${RUNTIME_DIR}/el_runtime.c" ]; then
echo "error: el_runtime.c not found at ${RUNTIME_DIR}/el_runtime.c" >&2
echo "error: el_runtime.c not found at ${RUNTIME_DIR}" >&2
exit 1
fi
mkdir -p dist
# ── Concatenate runtime modules + LSP source ───────────────────────────────────
# Load order follows runtime/manifest.el: string → math → state → env →
# fs → exec → time → json → http → then our LSP source.
RUNTIME_SRCS=(
"${EL_HOME}/runtime/string.el"
"${EL_HOME}/runtime/math.el"
"${EL_HOME}/runtime/state.el"
"${EL_HOME}/runtime/env.el"
"${EL_HOME}/runtime/fs.el"
"${EL_HOME}/runtime/exec.el"
"${EL_HOME}/runtime/time.el"
"${EL_HOME}/runtime/json.el"
)
# Check that runtime modules exist
MISSING=0
for f in "${RUNTIME_SRCS[@]}"; do
if [ ! -f "${f}" ]; then
echo "warning: runtime module not found: ${f}" >&2
MISSING=$((MISSING + 1))
fi
done
if [ "${MISSING}" -gt 0 ]; then
echo "error: ${MISSING} runtime module(s) missing — cannot build" >&2
if [ ! -f "${LSP_DIR}/el-lsp.el" ]; then
echo "error: el-lsp.el not found at ${LSP_DIR}" >&2
exit 1
fi
COMBINED="dist/el-lsp-combined.el"
echo "==> Combining sources..."
cat "${RUNTIME_SRCS[@]}" el-lsp.el > "${COMBINED}"
echo " ${COMBINED}"
mkdir -p "${OUT_DIR}"
# ── Compile El → C ─────────────────────────────────────────────────────────────
C_OUT="dist/el-lsp.c"
# ── Compile El → C ─────────────────────────────────────────────────────────
C_OUT="${OUT_DIR}/el-lsp.c"
echo "==> Compiling El → C..."
"${ELC}" "${COMBINED}" > "${C_OUT}"
echo " ${C_OUT}"
echo " ${LSP_DIR}/el-lsp.el → ${C_OUT}"
# ── Compile C → binary ─────────────────────────────────────────────────────────
BIN="dist/el-lsp"
# el-lsp.el uses only standard El builtins (no import statements needed).
# The elc compiler emits #include "el_runtime.h" at the top of the output.
"${ELC}" "${LSP_DIR}/el-lsp.el" > "${C_OUT}"
echo " Done ($(wc -l < "${C_OUT}") lines of C)."
# ── Compile C → binary ─────────────────────────────────────────────────────
BIN="${OUT_DIR}/el-lsp"
echo "==> Compiling C → binary..."
echo " ${C_OUT} + el_runtime.c → ${BIN}"
cc -std=c11 -O2 \
-I "${RUNTIME_DIR}" \
-o "${BIN}" \
"${C_OUT}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lpthread
echo " ${BIN}"
echo " Done."
# ── Summary ────────────────────────────────────────────────────────────────
echo
echo "==> Build complete: ${BIN}"
echo "==> Build complete."
echo
echo " Run as LSP server (editors connect via stdin/stdout):"
echo " ${BIN}"
echo " Binary : ${BIN}"
echo " Size : $(du -sh "${BIN}" | cut -f1)"
echo
echo " Test with a synthetic LSP initialize request:"
echo " echo -e 'Content-Length: 97\\r\\n\\r\\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"capabilities\":{}}}' | ${BIN}"
echo " Install system-wide:"
echo " sudo cp ${BIN} /usr/local/bin/el-lsp"
echo
echo " Quick smoke test (initialize + shutdown):"
cat << 'SMOKETEST'
python3 - << 'PY'
import subprocess, json
def frame(body):
b = body.encode()
return f"Content-Length: {len(b)}\r\n\r\n".encode() + b
def send(proc, obj):
proc.stdin.write(frame(json.dumps(obj)))
proc.stdin.flush()
def recv(proc):
hdr = b""
while not hdr.endswith(b"\r\n\r\n"):
hdr += proc.stdout.read(1)
cl = int([l for l in hdr.decode().split("\r\n") if l.startswith("Content-Length")][0].split(": ")[1])
return json.loads(proc.stdout.read(cl))
import sys, os
bin_path = sys.argv[1] if len(sys.argv) > 1 else "./dist/el-lsp"
proc = subprocess.Popen([bin_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
send(proc, {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}})
r = recv(proc)
print("initialize:", r.get("result", {}).get("serverInfo", {}).get("name"), "OK" if "result" in r else "FAIL")
send(proc, {"jsonrpc":"2.0","id":2,"method":"shutdown","params":{}})
r = recv(proc)
print("shutdown:", "OK" if r.get("result") is None else "FAIL")
send(proc, {"jsonrpc":"2.0","method":"exit","params":{}})
proc.wait()
print("exit: OK")
PY
SMOKETEST