Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ All notable changes to EigenScript are documented here.
by pointing the instrument at itself.

### Fixed
- **LSP `import ` completion offered 34 of 75 stdlib modules (#692).** The
completion list was a hand-maintained array in `src/eigenlsp.c` that had
fallen 41 modules behind `lib/` — every `ui_w_*` widget module, the whole
science set (`physics`, `chemistry`, `biology`, `calculus`, `linalg`,
`numerics`, `optimize`, `probability`, `geometry`, `earth_science`,
`engineering`), `audio`, `utf8`, `sync`, `pkg`, and more were simply
invisible in the editor. `tools/gen_lsp_stdlib_index.sh` (added by #590 for
symbol completion/hover) now also emits `stdlib_modules[]`, and completion
iterates that. The list is generated from the `lib/` **file set**, not
derived from the `stdlib_docs` rows: a module whose defines are all private
(`ui_layout`) or that declares none at top level (`text_builder`) has no
docs rows but is still importable, so deriving it would have silently
dropped 5 real modules — including one the old hardcoded list did offer.
`tests/test_lsp.py` now asserts completion against the actual `lib/*.eigs`
listing rather than a count, so a new module can never fall out again.
- **MT use-after-free: module-env observer-slot growth raced unlocked worker
reads (#694).** `observer_slot_update_e` grew `env->obs` with a bare
`realloc` + non-atomic pointer store while holding no lock, and the
Expand Down
20 changes: 8 additions & 12 deletions src/eigenlsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -987,21 +987,17 @@ static void handle_completion(int id, const char *params) {
/* Check if line starts with "import " */
while (*lp == ' ' || *lp == '\t') lp++;
if (strncmp(lp, "import ", 7) == 0) {
/* List available modules */
static const char *modules[] = {
"args", "auth", "concurrent", "config", "data", "datetime",
"eigen", "format", "functional", "http", "io", "json",
"list", "log", "map", "math", "observer", "queue",
"sanitize", "set", "sort", "state", "stats", "store",
"string", "template", "tensor", "test", "ui",
"ui_anim", "ui_draw", "ui_layout", "ui_theme", "validate",
NULL
};
for (int i = 0; modules[i]; i++) {
/* #692: offer EVERY lib/ module, from the generated
* stdlib_modules[] (one entry per .eigs file in lib/).
* This was a hand-maintained 34-name array that had
* silently fallen 41 modules behind lib/ — the drift a
* build-time-generated list makes structurally
* impossible. */
for (int i = 0; stdlib_modules[i]; i++) {
if (!first) strbuf_append_char(&sb, ',');
first = 0;
strbuf_append(&sb, "{\"label\":\"");
strbuf_append(&sb, modules[i]);
strbuf_append(&sb, stdlib_modules[i]);
strbuf_append(&sb, "\",\"kind\":9}"); /* 9 = Module */
}
}
Expand Down
25 changes: 25 additions & 0 deletions tests/test_lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,31 @@ def main():
check("stdlib completion omits unimported modules (map.map_get absent)",
not any(it.get("label") == "map_get" for it in items))

# --- #692: `import ` completion offers EVERY lib/ module ---
# Asserted against the actual lib/*.eigs file set rather than a number,
# so adding a module can never silently fall out of completion again —
# which is exactly how the old hand-maintained array drifted 41 behind.
lib_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib")
want = sorted(f[:-5] for f in os.listdir(lib_dir) if f.endswith(".eigs"))
comp_imp = {"jsonrpc": "2.0", "id": 29, "method": "textDocument/completion",
"params": {"textDocument": {"uri": URI},
"position": {"line": 0, "character": 7}}}
r = converse([INIT, did_open("import \n"), comp_imp, SHUTDOWN, EXIT])
res = (by_id(r, 29) or {}).get("result", {})
items = res.get("items") if isinstance(res, dict) and isinstance(res.get("items"), list) else []
got = sorted(it.get("label") for it in items if it.get("kind") == 9)
missing = [m for m in want if m not in got]
check("import completion offers every lib/ module (%d, none missing)" % len(want),
not missing and len(want) > 0)
if missing:
print(" missing from completion: " + ", ".join(missing))
# The five that a stdlib_docs-derived list would have dropped: their
# defines are all private (ui_layout) or absent at top level
# (text_builder), so they have no docs rows but ARE importable.
check("import completion includes modules with no public defines",
all(m in got for m in
("text_builder", "ui_dnd", "ui_focus", "ui_layout", "ui_registry")))

# No imports in the document → no stdlib items at all.
comp_plain = {"jsonrpc": "2.0", "id": 27, "method": "textDocument/completion",
"params": {"textDocument": {"uri": URI}, "position": {"line": 0, "character": 0}}}
Expand Down
17 changes: 16 additions & 1 deletion tools/gen_lsp_stdlib_index.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ BEGIN {
print " * signature comments (docs/STDLIB.md \"Writing Library Functions\"), served" > OUT
print " * by src/eigenlsp.c as import-aware completion + hover (#590). Fallback" > OUT
print " * detail is the define shape. Regenerated by the Makefile lsp target /" > OUT
print " * build.sh lsp; not committed. Columns: module, name, params, detail. */" > OUT
print " * build.sh lsp; not committed. Columns: module, name, params, detail." > OUT
print " * Also emits stdlib_modules[]: EVERY lib/*.eigs basename, which is what" > OUT
print " * `import ` completion offers (#692). That list is deliberately NOT" > OUT
print " * derived from stdlib_docs -- a module whose defines are all private" > OUT
print " * (ui_layout) or which declares none at top level (text_builder) has no" > OUT
print " * row there but is still perfectly importable, so deriving it would" > OUT
print " * silently drop 5 real modules. */" > OUT
print "#include <stddef.h>" > OUT
print "static const char *stdlib_docs[][4] = {" > OUT
}
Expand All @@ -64,6 +70,7 @@ FNR == 1 {
sub(/\.eigs$/, "", module)
ncb = 0
modules++
modname[modules] = module
}
/^[ \t]*#/ {
cb[++ncb] = $0
Expand Down Expand Up @@ -100,6 +107,14 @@ FNR == 1 {
END {
print " {NULL, NULL, NULL, NULL}" > OUT
print "};" > OUT
print "" > OUT
print "/* Every importable lib/ module (#692). Sourced from the FILE set, not" > OUT
print " * from stdlib_docs above -- see the header comment. */" > OUT
print "static const char *stdlib_modules[] = {" > OUT
for (i = 1; i <= modules; i++)
printf " \"%s\",\n", c_escape(modname[i]) > OUT
print " NULL" > OUT
print "};" > OUT
printf "lsp_stdlib_index: %d public functions across %d lib modules -> %s\n", \
funcs, modules, OUT > "/dev/stderr"
}
Expand Down
Loading