diff --git a/CHANGELOG.md b/CHANGELOG.md index c54b617..8df6db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/eigenlsp.c b/src/eigenlsp.c index 4333eaa..a473427 100644 --- a/src/eigenlsp.c +++ b/src/eigenlsp.c @@ -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 */ } } diff --git a/tests/test_lsp.py b/tests/test_lsp.py index 218bbad..628fd3c 100755 --- a/tests/test_lsp.py +++ b/tests/test_lsp.py @@ -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}}} diff --git a/tools/gen_lsp_stdlib_index.sh b/tools/gen_lsp_stdlib_index.sh index de8876f..cd32b87 100755 --- a/tools/gen_lsp_stdlib_index.sh +++ b/tools/gen_lsp_stdlib_index.sh @@ -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 " > OUT print "static const char *stdlib_docs[][4] = {" > OUT } @@ -64,6 +70,7 @@ FNR == 1 { sub(/\.eigs$/, "", module) ncb = 0 modules++ + modname[modules] = module } /^[ \t]*#/ { cb[++ncb] = $0 @@ -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" }