diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 93b993c741..1053c84d10 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -372,6 +372,10 @@ PERFORMANCE • |i_CTRL-R| inserts named/clipboard registers literally, 10x speedup. • LSP `textDocument/semanticTokens/range` is supported, which requests tokens for the viewport (visible screen) only. +• |:packadd| doesn't invalidate the cached Lua package path. Instead the cache + gets updated in place. This might make a big startuptime difference for + certain |init.lua| patterns where multiple |:packadd| or |vim.pack.add()| + calls are interspersed with other code. PLUGINS diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index 96b4461e65..7641120ae1 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -94,7 +94,9 @@ typedef struct { typedef struct { char *path; bool after; + bool pack_inserted; TriState has_lua; + size_t pos_in_rtp; } SearchPathItem; typedef kvec_t(SearchPathItem) RuntimeSearchPath; @@ -292,6 +294,7 @@ void f_getstacktrace(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) } static bool runtime_search_path_valid = false; +static bool runtime_search_path_valid_thread = false; static int *runtime_search_path_ref = NULL; static RuntimeSearchPath runtime_search_path; static RuntimeSearchPath runtime_search_path_thread; @@ -531,8 +534,9 @@ static RuntimeSearchPath copy_runtime_search_path(const RuntimeSearchPath src) { RuntimeSearchPath dst = KV_INITIAL_VALUE; for (size_t j = 0; j < kv_size(src); j++) { - SearchPathItem src_item = kv_A(src, j); - kv_push(dst, ((SearchPathItem){ xstrdup(src_item.path), src_item.after, src_item.has_lua })); + SearchPathItem item = kv_A(src, j); + kv_push(dst, ((SearchPathItem){ xstrdup(item.path), item.after, item.pack_inserted, + item.has_lua, item.pos_in_rtp })); } return dst; @@ -642,13 +646,20 @@ Array runtime_inspect(Arena *arena) for (size_t i = 0; i < kv_size(path); i++) { SearchPathItem *item = &kv_A(path, i); - Array entry = arena_array(arena, 3); - ADD_C(entry, CSTR_AS_OBJ(item->path)); - ADD_C(entry, BOOLEAN_OBJ(item->after)); - if (item->has_lua != kNone) { - ADD_C(entry, BOOLEAN_OBJ(item->has_lua == kTrue)); + Dict entry = arena_dict(arena, 5); + PUT_C(entry, "path", CSTR_AS_OBJ(item->path)); + if (item->after) { + PUT_C(entry, "after", BOOLEAN_OBJ(true)); } - ADD_C(rv, ARRAY_OBJ(entry)); + if (item->pack_inserted) { + PUT_C(entry, "pack_inserted", BOOLEAN_OBJ(true)); + } + if (item->has_lua != kNone) { + PUT_C(entry, "has_lua", BOOLEAN_OBJ(item->has_lua == kTrue)); + } + PUT_C(entry, "pos_in_rtp", INTEGER_OBJ((Integer)item->pos_in_rtp)); + + ADD_C(rv, DICT_OBJ(entry)); } return rv; } @@ -752,25 +763,27 @@ int do_in_path_and_pp(char *path, char *name, int flags, DoInRuntimepathCB callb return done; } -static void push_path(RuntimeSearchPath *search_path, Set(String) *rtp_used, char *entry, - bool after) +static bool push_path(RuntimeSearchPath *search_path, Set(String) *rtp_used, char *entry, + bool after, size_t pos_in_rtp) { String *key_alloc; if (set_put_ref(String, rtp_used, cstr_as_string(entry), &key_alloc)) { *key_alloc = cstr_to_string(entry); - kv_push(*search_path, ((SearchPathItem){ key_alloc->data, after, kNone })); + kv_push(*search_path, ((SearchPathItem){ key_alloc->data, after, false, kNone, pos_in_rtp })); + return true; } + return false; } static void expand_rtp_entry(RuntimeSearchPath *search_path, Set(String) *rtp_used, char *entry, - bool after) + bool after, size_t pos_in_rtp) { if (set_has(String, rtp_used, cstr_as_string(entry))) { return; } if (!*entry) { - push_path(search_path, rtp_used, entry, after); + push_path(search_path, rtp_used, entry, after, pos_in_rtp); } int num_files; @@ -778,14 +791,16 @@ static void expand_rtp_entry(RuntimeSearchPath *search_path, Set(String) *rtp_us char *(pat[]) = { entry }; if (gen_expand_wildcards(1, pat, &num_files, &files, EW_DIR | EW_NOBREAK) == OK) { for (int i = 0; i < num_files; i++) { - push_path(search_path, rtp_used, files[i], after); + // reuses position but it is ok, we need to be monotonic but not strictly + push_path(search_path, rtp_used, files[i], after, pos_in_rtp); } FreeWild(num_files, files); } } static void expand_pack_entry(RuntimeSearchPath *search_path, Set(String) *rtp_used, - CharVec *after_path, char *pack_entry, size_t pack_entry_len) + CharVec *after_path, char *pack_entry, size_t pack_entry_len, + size_t pos_in_rtp) { static char buf[MAXPATHL]; char *(start_pat[]) = { "/pack/*/start/*", "/start/*" }; // NOLINT @@ -795,7 +810,7 @@ static void expand_pack_entry(RuntimeSearchPath *search_path, Set(String) *rtp_u } xstrlcpy(buf, pack_entry, sizeof buf); xstrlcpy(buf + pack_entry_len, start_pat[i], sizeof buf - pack_entry_len); - expand_rtp_entry(search_path, rtp_used, buf, false); + expand_rtp_entry(search_path, rtp_used, buf, false, pos_in_rtp); size_t after_size = strlen(buf) + 7; char *after = xmallocz(after_size); xstrlcpy(after, buf, after_size); @@ -844,34 +859,45 @@ static RuntimeSearchPath runtime_search_path_build(void) break; } + size_t pos_in_rtp = (size_t)(cur_entry - p_rtp); + // fact: &rtp entries can contain wild chars - expand_rtp_entry(&search_path, &rtp_used, buf, false); + expand_rtp_entry(&search_path, &rtp_used, buf, false, pos_in_rtp); handle_T *h = map_ref(String, int)(&pack_used, cstr_as_string(buf), NULL); if (h) { (*h)++; - expand_pack_entry(&search_path, &rtp_used, &after_path, buf, buflen); + expand_pack_entry(&search_path, &rtp_used, &after_path, buf, buflen, pos_in_rtp); } } + // The following entries were not explicit in rtp. + // this is fine, but keep pos_in_rtp monotonic: + // use the comma between two entries as a sentinel + size_t sentinel_pos_in_rtp = (size_t)(rtp_entry - p_rtp); + sentinel_pos_in_rtp -= (sentinel_pos_in_rtp > 0) ? 1 : 0; + for (size_t i = 0; i < kv_size(pack_entries); i++) { String item = kv_A(pack_entries, i); handle_T h = map_get(String, int)(&pack_used, item); if (h == 0) { - expand_pack_entry(&search_path, &rtp_used, &after_path, item.data, item.size); + expand_pack_entry(&search_path, &rtp_used, &after_path, item.data, item.size, + sentinel_pos_in_rtp); } } // "after" packages for (size_t i = 0; i < kv_size(after_path); i++) { - expand_rtp_entry(&search_path, &rtp_used, kv_A(after_path, i), true); + expand_rtp_entry(&search_path, &rtp_used, kv_A(after_path, i), true, sentinel_pos_in_rtp); xfree(kv_A(after_path, i)); } // "after" dirs in rtp for (; *rtp_entry != NUL;) { + char *cur_entry = rtp_entry; copy_option_part(&rtp_entry, buf, MAXPATHL, ","); - expand_rtp_entry(&search_path, &rtp_used, buf, path_is_after(buf, strlen(buf))); + size_t pos_in_rtp = (size_t)(cur_entry - p_rtp); + expand_rtp_entry(&search_path, &rtp_used, buf, path_is_after(buf, strlen(buf)), pos_in_rtp); } // strings are not owned @@ -914,13 +940,23 @@ void runtime_search_path_validate(void) runtime_search_path = runtime_search_path_build(); runtime_search_path_valid = true; runtime_search_path_ref = NULL; // initially unowned - uv_mutex_lock(&runtime_search_path_mutex); - runtime_search_path_free(runtime_search_path_thread); - runtime_search_path_thread = copy_runtime_search_path(runtime_search_path); - uv_mutex_unlock(&runtime_search_path_mutex); + update_runtime_search_path_thread(true); } } +void update_runtime_search_path_thread(bool force) +{ + if (!force && !(runtime_search_path_valid && !runtime_search_path_valid_thread)) { + return; + } + + uv_mutex_lock(&runtime_search_path_mutex); + runtime_search_path_free(runtime_search_path_thread); + runtime_search_path_thread = copy_runtime_search_path(runtime_search_path); + uv_mutex_unlock(&runtime_search_path_mutex); + runtime_search_path_valid_thread = true; +} + /// Just like do_in_path_and_pp(), using 'runtimepath' for "path". int do_in_runtimepath(char *name, int flags, DoInRuntimepathCB callback, void *cookie) { @@ -1012,8 +1048,8 @@ static int add_pack_dir_to_rtp(char *fname, bool is_pack) } // now we have: - // rtp/pack/name/start/name - // p4 p3 p2 p1 + // rtp/pack/name/(start|opt)/name + // p4 p3 p2 p1 // // find the part up to "pack" in 'runtimepath' p4++; // append pathsep in order to expand symlink @@ -1091,10 +1127,12 @@ static int add_pack_dir_to_rtp(char *fname, bool is_pack) // We now have 'rtp' parts: {keep}{keep_after}{rest}. // Create new_rtp, first: {keep},{fname} size_t keep = (size_t)(insp - p_rtp); + size_t first_pos = keep; memmove(new_rtp, p_rtp, keep); size_t new_rtp_len = keep; if (*insp == NUL) { new_rtp[new_rtp_len++] = ','; // add comma before + first_pos++; } memmove(new_rtp + new_rtp_len, fname, addlen - 1); new_rtp_len += addlen - 1; @@ -1102,6 +1140,8 @@ static int add_pack_dir_to_rtp(char *fname, bool is_pack) new_rtp[new_rtp_len++] = ','; // add comma after } + size_t after_pos = 0; + if (afterlen > 0 && after_insp != NULL) { size_t keep_after = (size_t)(after_insp - p_rtp); @@ -1112,6 +1152,7 @@ static int add_pack_dir_to_rtp(char *fname, bool is_pack) new_rtp_len += afterlen - 1; new_rtp[new_rtp_len++] = ','; keep = keep_after; + after_pos = keep_after; } if (p_rtp[keep] != NUL) { @@ -1124,11 +1165,51 @@ static int add_pack_dir_to_rtp(char *fname, bool is_pack) if (afterlen > 0 && after_insp == NULL) { // Append afterdir when "after" was not found: // {keep},{fname}{rest},{afterdir} - xstrlcat(new_rtp, ",", new_rtp_capacity); + after_pos = xstrlcat(new_rtp, ",", new_rtp_capacity); xstrlcat(new_rtp, afterdir, new_rtp_capacity); } + bool was_valid = runtime_search_path_valid; set_option_value_give_err(kOptRuntimepath, CSTR_AS_OPTVAL(new_rtp), 0); + + assert(!runtime_search_path_valid); + // If this is the result of "packadd opt_pack", rebuilding runtime_search_pat + // from scratch is needlessly slow. splice in the package and its afterdir instead. + // But don't do this for "pack/*/start/*" (is_pack=true): + // we want properly expand wildcards in a "start" bundle. + if (was_valid && !is_pack) { + runtime_search_path_valid = true; + runtime_search_path_valid_thread = false; + kv_pushp(runtime_search_path); + ssize_t i = (ssize_t)(kv_size(runtime_search_path)) - 1; + + if (afterlen > 0) { + kv_pushp(runtime_search_path); + i += 1; + for (; i >= 1; i--) { + if (i > 1 && kv_A(runtime_search_path, i - 2).pos_in_rtp >= after_pos) { + kv_A(runtime_search_path, i) = kv_A(runtime_search_path, i - 2); + kv_A(runtime_search_path, i).pos_in_rtp += addlen + afterlen; + } else { + kv_A(runtime_search_path, i) = (SearchPathItem){ xstrdup(afterdir), true, true, kNone, + after_pos + addlen }; + i--; + break; + } + } + } + + for (; i >= 0; i--) { + if (i > 0 && kv_A(runtime_search_path, i - 1).pos_in_rtp >= first_pos) { + kv_A(runtime_search_path, i) = kv_A(runtime_search_path, i - 1); + kv_A(runtime_search_path, i).pos_in_rtp += addlen; + } else { + kv_A(runtime_search_path, i) = (SearchPathItem){ xstrdup(fname), false, true, kNone, + first_pos }; + break; + } + } + } xfree(new_rtp); retval = OK; @@ -1280,6 +1361,8 @@ void load_start_packages(void) add_start_pack_plugins, &APP_LOAD); do_in_path(p_pp, "", "start/*", DIP_ALL + DIP_DIR, // NOLINT add_start_pack_plugins, &APP_LOAD); + + update_runtime_search_path_thread(false); } // ":packloadall" @@ -1347,6 +1430,8 @@ void ex_packadd(exarg_T *eap) do_in_path(p_pp, "", pat, DIP_ALL + DIP_DIR + (res == FAIL ? DIP_ERR : 0), add_opt_pack_plugins, cookie); + update_runtime_search_path_thread(false); + xfree(pat); } diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 34995af7fe..86e8fe2bca 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -1128,6 +1128,44 @@ describe('startup', function() }, exec_lua [[ return _G.test_loadorder ]]) end) + it('does an incremental update for packadd', function() + pack_clear [[ lua _G.test_loadorder = {} ]] + command [[ + " need to use the runtime to make the initial cache: + runtime! non_exist_ent + " this should now incrementally update it: + packadd! superspecial + ]] + + local check = api.nvim__runtime_inspect() + local check_copy = vim.deepcopy(check) + local any_incremental = false + for _, item in ipairs(check_copy) do + any_incremental = any_incremental or item.pack_inserted + item.pack_inserted = nil + end + eq(true, any_incremental, 'no pack_inserted in ' .. vim.inspect(check)) + + command [[ + let &rtp = &rtp + runtime! phantom_ghost + ]] + + local new_check = api.nvim__runtime_inspect() + eq(check_copy, new_check) + + command [[ runtime! filen.lua ]] + eq({ + 'ordinary', + 'SuperSpecial', + 'FANCY', + 'mittel', + 'FANCY after', + 'SuperSpecial after', + 'ordinary after', + }, exec_lua [[ return _G.test_loadorder ]]) + end) + it('handles the correct order with opt packages and globpath(&rtp, ...)', function() pack_clear [[ set loadplugins | lua _G.test_loadorder = {} ]] command [[