From a857b251d123112eda78945e163fe7fd0438ff59 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 2 Feb 2025 14:24:38 +0800 Subject: vim-patch: port some userfunc.c refactorings from Vim (#32292) Port one_function_arg() and get_function_body() from Vim. vim-patch:8.2.2865: skipping over function body fails Problem: Skipping over function body fails. Solution: Do not define the function when skipping. https://github.com/vim/vim/commit/d87c21a918d8d611750f22d68fc638bf7a79b1d5 Co-authored-by: Bram Moolenaar --- src/nvim/eval/userfunc.c | 764 +++++++++++++++++++++++++---------------------- 1 file changed, 401 insertions(+), 363 deletions(-) diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index a24a3d5622..2e549fcf37 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -104,6 +104,48 @@ hashtab_T *func_tbl_get(void) return &func_hashtab; } +/// Get one function argument. +/// Return a pointer to after the type. +/// When something is wrong return "arg". +static char *one_function_arg(char *arg, garray_T *newargs, bool skip) +{ + char *p = arg; + + while (ASCII_ISALNUM(*p) || *p == '_') { + p++; + } + if (arg == p || isdigit((uint8_t)(*arg)) + || (p - arg == 9 && strncmp(arg, "firstline", 9) == 0) + || (p - arg == 8 && strncmp(arg, "lastline", 8) == 0)) { + if (!skip) { + semsg(_("E125: Illegal argument: %s"), arg); + } + return arg; + } + + if (newargs != NULL) { + ga_grow(newargs, 1); + uint8_t c = (uint8_t)(*p); + *p = NUL; + char *arg_copy = xstrdup(arg); + + // Check for duplicate argument name. + for (int i = 0; i < newargs->ga_len; i++) { + if (strcmp(((char **)(newargs->ga_data))[i], arg_copy) == 0) { + semsg(_("E853: Duplicate argument name: %s"), arg_copy); + xfree(arg_copy); + return arg; + } + } + ((char **)(newargs->ga_data))[newargs->ga_len] = arg_copy; + newargs->ga_len++; + + *p = (char)c; + } + + return p; +} + /// Get function arguments. static int get_function_args(char **argp, char endchar, garray_T *newargs, int *varargs, garray_T *default_args, bool skip) @@ -134,36 +176,11 @@ static int get_function_args(char **argp, char endchar, garray_T *newargs, int * mustend = true; } else { arg = p; - while (ASCII_ISALNUM(*p) || *p == '_') { - p++; - } - if (arg == p || isdigit((uint8_t)(*arg)) - || (p - arg == 9 && strncmp(arg, "firstline", 9) == 0) - || (p - arg == 8 && strncmp(arg, "lastline", 8) == 0)) { - if (!skip) { - semsg(_("E125: Illegal argument: %s"), arg); - } + p = one_function_arg(p, newargs, skip); + if (p == arg) { break; } - if (newargs != NULL) { - ga_grow(newargs, 1); - uint8_t c = (uint8_t)(*p); - *p = NUL; - arg = xstrdup(arg); - - // Check for duplicate argument name. - for (int i = 0; i < newargs->ga_len; i++) { - if (strcmp(((char **)(newargs->ga_data))[i], arg) == 0) { - semsg(_("E853: Duplicate argument name: %s"), arg); - xfree(arg); - goto err_ret; - } - } - ((char **)(newargs->ga_data))[newargs->ga_len] = arg; - newargs->ga_len++; - *p = (char)c; - } if (*skipwhite(p) == '=' && default_args != NULL) { typval_T rettv; @@ -2186,8 +2203,6 @@ char *save_function_name(char **name, bool skip, int flags, funcdict_T *fudi) return saved; } -#define MAX_FUNC_NESTING 50 - /// List functions. /// /// @param regmatch When NULL, all of them. @@ -2218,349 +2233,100 @@ static void list_functions(regmatch_T *regmatch) } } -/// ":function" -void ex_function(exarg_T *eap) +#define MAX_FUNC_NESTING 50 + +/// Read the body of a function, put every line in "newlines". +/// This stops at "endfunction". +/// "newlines" must already have been initialized. +static int get_function_body(exarg_T *eap, garray_T *newlines, char *line_arg_in, + char **line_to_free, bool show_block) { - char *theline; - char *line_to_free = NULL; bool saved_wait_return = need_wait_return; - char *arg; - char *line_arg = NULL; - garray_T newargs; - garray_T default_args; - garray_T newlines; - int varargs = false; - int flags = 0; - ufunc_T *fp; - bool overwrite = false; - funcdict_T fudi; - static int func_nr = 0; // number for nameless function - hashtab_T *ht; - bool is_heredoc = false; + char *line_arg = line_arg_in; + int indent = 2; + int nesting = 0; char *skip_until = NULL; + int ret = FAIL; + bool is_heredoc = false; char *heredoc_trimmed = NULL; - bool show_block = false; bool do_concat = true; - // ":function" without argument: list functions. - if (ends_excmd(*eap->arg)) { - if (!eap->skip) { - list_functions(NULL); + while (true) { + if (KeyTyped) { + msg_scroll = true; + saved_wait_return = false; } - eap->nextcmd = check_nextcmd(eap->arg); - return; - } + need_wait_return = false; - // ":function /pat": list functions matching pattern. - if (*eap->arg == '/') { - char *p = skip_regexp(eap->arg + 1, '/', true); - if (!eap->skip) { - regmatch_T regmatch; + char *theline; + char *p; + char *arg; - char c = *p; - *p = NUL; - regmatch.regprog = vim_regcomp(eap->arg + 1, RE_MAGIC); - *p = c; - if (regmatch.regprog != NULL) { - regmatch.rm_ic = p_ic; - list_functions(®match); - vim_regfree(regmatch.regprog); + if (line_arg != NULL) { + // Use eap->arg, split up in parts by line breaks. + theline = line_arg; + p = vim_strchr(theline, '\n'); + if (p == NULL) { + line_arg += strlen(line_arg); + } else { + *p = NUL; + line_arg = p + 1; } - } - if (*p == '/') { - p++; - } - eap->nextcmd = check_nextcmd(p); - return; - } - - // Get the function name. There are these situations: - // func function name - // "name" == func, "fudi.fd_dict" == NULL - // dict.func new dictionary entry - // "name" == NULL, "fudi.fd_dict" set, - // "fudi.fd_di" == NULL, "fudi.fd_newkey" == func - // dict.func existing dict entry with a Funcref - // "name" == func, "fudi.fd_dict" set, - // "fudi.fd_di" set, "fudi.fd_newkey" == NULL - // dict.func existing dict entry that's not a Funcref - // "name" == NULL, "fudi.fd_dict" set, - // "fudi.fd_di" set, "fudi.fd_newkey" == NULL - // s:func script-local function name - // g:func global function name, same as "func" - char *p = eap->arg; - char *name = save_function_name(&p, eap->skip, TFN_NO_AUTOLOAD, &fudi); - int paren = (vim_strchr(p, '(') != NULL); - if (name == NULL && (fudi.fd_dict == NULL || !paren) && !eap->skip) { - // Return on an invalid expression in braces, unless the expression - // evaluation has been cancelled due to an aborting error, an - // interrupt, or an exception. - if (!aborting()) { - if (fudi.fd_newkey != NULL) { - semsg(_(e_dictkey), fudi.fd_newkey); + } else { + xfree(*line_to_free); + if (eap->ea_getline == NULL) { + theline = getcmdline(':', 0, indent, do_concat); + } else { + theline = eap->ea_getline(':', eap->cookie, indent, do_concat); } - xfree(fudi.fd_newkey); - return; - } - eap->skip = true; - } - - // An error in a function call during evaluation of an expression in magic - // braces should not cause the function not to be defined. - const int saved_did_emsg = did_emsg; - did_emsg = false; - - // - // ":function func" with only function name: list function. - // If bang is given: - // - include "!" in function head - // - exclude line numbers from function body - // - if (!paren) { - if (!ends_excmd(*skipwhite(p))) { - semsg(_(e_trailing_arg), p); - goto ret_free; + *line_to_free = theline; } - eap->nextcmd = check_nextcmd(p); - if (eap->nextcmd != NULL) { - *p = NUL; + if (KeyTyped) { + lines_left = Rows - 1; } - if (!eap->skip && !got_int) { - fp = find_func(name); - if (fp != NULL) { - // Check no function was added or removed from a callback, e.g. at - // the more prompt. "fp" may then be invalid. - const int prev_ht_changed = func_hashtab.ht_changed; - - if (list_func_head(fp, !eap->forceit, eap->forceit) == OK) { - for (int j = 0; j < fp->uf_lines.ga_len && !got_int; j++) { - if (FUNCLINE(fp, j) == NULL) { - continue; - } - msg_putchar('\n'); - if (!eap->forceit) { - msg_outnum(j + 1); - if (j < 9) { - msg_putchar(' '); - } - if (j < 99) { - msg_putchar(' '); - } - if (function_list_modified(prev_ht_changed)) { - break; - } - } - msg_prt_line(FUNCLINE(fp, j), false); - line_breakcheck(); // show multiple lines at a time! - } - if (!got_int) { - msg_putchar('\n'); - if (!function_list_modified(prev_ht_changed)) { - msg_puts(eap->forceit ? "endfunction" : " endfunction"); - } - } - } + if (theline == NULL) { + if (skip_until != NULL) { + semsg(_(e_missing_heredoc_end_marker_str), skip_until); } else { - emsg_funcname(N_("E123: Undefined function: %s"), name); + emsg(_("E126: Missing :endfunction")); } + goto theend; } - goto ret_free; - } - - // ":function name(arg1, arg2)" Define function. - p = skipwhite(p); - if (*p != '(') { - if (!eap->skip) { - semsg(_("E124: Missing '(': %s"), eap->arg); - goto ret_free; - } - // attempt to continue by skipping some text - if (vim_strchr(p, '(') != NULL) { - p = vim_strchr(p, '('); + if (show_block) { + assert(indent >= 0); + ui_ext_cmdline_block_append((size_t)indent, theline); } - } - p = skipwhite(p + 1); - - ga_init(&newargs, (int)sizeof(char *), 3); - ga_init(&newlines, (int)sizeof(char *), 3); - if (!eap->skip) { - // Check the name of the function. Unless it's a dictionary function - // (that we are overwriting). - if (name != NULL) { - arg = name; + // Detect line continuation: SOURCING_LNUM increased more than one. + linenr_T sourcing_lnum_off = get_sourced_lnum(eap->ea_getline, eap->cookie); + if (SOURCING_LNUM < sourcing_lnum_off) { + sourcing_lnum_off -= SOURCING_LNUM; } else { - arg = fudi.fd_newkey; + sourcing_lnum_off = 0; } - if (arg != NULL && (fudi.fd_di == NULL || !tv_is_func(fudi.fd_di->di_tv))) { - char *name_base = arg; - if ((uint8_t)(*arg) == K_SPECIAL) { - name_base = vim_strchr(arg, '_'); - if (name_base == NULL) { - name_base = arg + 3; + + if (skip_until != NULL) { + // Don't check for ":endfunc" between + // * ":append" and "." + // * ":python <dv_scope == VAR_DEF_SCOPE) { - emsg(_("E862: Cannot use g: here")); - goto ret_free; - } - } - - if (get_function_args(&p, ')', &newargs, &varargs, - &default_args, eap->skip) == FAIL) { - goto errret_2; - } - - if (KeyTyped && ui_has(kUICmdline)) { - show_block = true; - ui_ext_cmdline_block_append(0, eap->cmd); - } - - // find extra arguments "range", "dict", "abort" and "closure" - while (true) { - p = skipwhite(p); - if (strncmp(p, "range", 5) == 0) { - flags |= FC_RANGE; - p += 5; - } else if (strncmp(p, "dict", 4) == 0) { - flags |= FC_DICT; - p += 4; - } else if (strncmp(p, "abort", 5) == 0) { - flags |= FC_ABORT; - p += 5; - } else if (strncmp(p, "closure", 7) == 0) { - flags |= FC_CLOSURE; - p += 7; - if (current_funccal == NULL) { - emsg_funcname(N_("E932: Closure function should not be at top level: %s"), - name == NULL ? "" : name); - goto erret; - } - } else { - break; - } - } - - // When there is a line break use what follows for the function body. - // Makes 'exe "func Test()\n...\nendfunc"' work. - if (*p == '\n') { - line_arg = p + 1; - } else if (*p != NUL && *p != '"' && !eap->skip && !did_emsg) { - semsg(_(e_trailing_arg), p); - } - - // Read the body of the function, until ":endfunction" is found. - if (KeyTyped) { - // Check if the function already exists, don't let the user type the - // whole function before telling them it doesn't work! For a script we - // need to skip the body to be able to find what follows. - if (!eap->skip && !eap->forceit) { - if (fudi.fd_dict != NULL && fudi.fd_newkey == NULL) { - emsg(_(e_funcdict)); - } else if (name != NULL && find_func(name) != NULL) { - emsg_funcname(e_funcexts, name); - } - } - - if (!eap->skip && did_emsg) { - goto erret; - } - - if (!ui_has(kUICmdline)) { - msg_putchar('\n'); // don't overwrite the function name - } - cmdline_row = msg_row; - } - - // Save the starting line number. - linenr_T sourcing_lnum_top = SOURCING_LNUM; - - int indent = 2; - int nesting = 0; - while (true) { - if (KeyTyped) { - msg_scroll = true; - saved_wait_return = false; - } - need_wait_return = false; - - if (line_arg != NULL) { - // Use eap->arg, split up in parts by line breaks. - theline = line_arg; - p = vim_strchr(theline, '\n'); - if (p == NULL) { - line_arg += strlen(line_arg); - } else { - *p = NUL; - line_arg = p + 1; - } - } else { - xfree(line_to_free); - if (eap->ea_getline == NULL) { - theline = getcmdline(':', 0, indent, do_concat); - } else { - theline = eap->ea_getline(':', eap->cookie, indent, do_concat); - } - line_to_free = theline; - } - if (KeyTyped) { - lines_left = Rows - 1; - } - if (theline == NULL) { - if (skip_until != NULL) { - semsg(_(e_missing_heredoc_end_marker_str), skip_until); - } else { - emsg(_("E126: Missing :endfunction")); - } - goto erret; - } - if (show_block) { - assert(indent >= 0); - ui_ext_cmdline_block_append((size_t)indent, theline); - } - - // Detect line continuation: SOURCING_LNUM increased more than one. - linenr_T sourcing_lnum_off = get_sourced_lnum(eap->ea_getline, eap->cookie); - if (SOURCING_LNUM < sourcing_lnum_off) { - sourcing_lnum_off -= SOURCING_LNUM; - } else { - sourcing_lnum_off = 0; - } - - if (skip_until != NULL) { - // Don't check for ":endfunc" between - // * ":append" and "." - // * ":python <cmdlinep". eap->nextcmd = nextcmd; - if (line_to_free != NULL) { + if (*line_to_free != NULL) { xfree(*eap->cmdlinep); - *eap->cmdlinep = line_to_free; - line_to_free = NULL; + *eap->cmdlinep = *line_to_free; + *line_to_free = NULL; } } break; @@ -2705,18 +2471,18 @@ void ex_function(exarg_T *eap) } // Add the line to the function. - ga_grow(&newlines, 1 + (int)sourcing_lnum_off); + ga_grow(newlines, 1 + (int)sourcing_lnum_off); // Copy the line to newly allocated memory. get_one_sourceline() // allocates 250 bytes per line, this saves 80% on average. The cost // is an extra alloc/free. p = xstrdup(theline); - ((char **)(newlines.ga_data))[newlines.ga_len++] = p; + ((char **)(newlines->ga_data))[newlines->ga_len++] = p; // Add NULL lines for continuation lines, so that the line count is // equal to the index in the growarray. while (sourcing_lnum_off-- > 0) { - ((char **)(newlines.ga_data))[newlines.ga_len++] = NULL; + ((char **)(newlines->ga_data))[newlines->ga_len++] = NULL; } // Check for end of eap->arg. @@ -2725,9 +2491,284 @@ void ex_function(exarg_T *eap) } } - // Don't define the function when skipping commands or when an error was - // detected. - if (eap->skip || did_emsg) { + // Return OK when no error was detected. + if (!did_emsg) { + ret = OK; + } + +theend: + xfree(skip_until); + xfree(heredoc_trimmed); + need_wait_return |= saved_wait_return; + return ret; +} + +/// ":function" +void ex_function(exarg_T *eap) +{ + char *line_to_free = NULL; + char *arg; + char *line_arg = NULL; + garray_T newargs; + garray_T default_args; + garray_T newlines; + int varargs = false; + int flags = 0; + ufunc_T *fp; + bool overwrite = false; + funcdict_T fudi; + static int func_nr = 0; // number for nameless function + hashtab_T *ht; + bool show_block = false; + + // ":function" without argument: list functions. + if (ends_excmd(*eap->arg)) { + if (!eap->skip) { + list_functions(NULL); + } + eap->nextcmd = check_nextcmd(eap->arg); + return; + } + + // ":function /pat": list functions matching pattern. + if (*eap->arg == '/') { + char *p = skip_regexp(eap->arg + 1, '/', true); + if (!eap->skip) { + regmatch_T regmatch; + + char c = *p; + *p = NUL; + regmatch.regprog = vim_regcomp(eap->arg + 1, RE_MAGIC); + *p = c; + if (regmatch.regprog != NULL) { + regmatch.rm_ic = p_ic; + list_functions(®match); + vim_regfree(regmatch.regprog); + } + } + if (*p == '/') { + p++; + } + eap->nextcmd = check_nextcmd(p); + return; + } + + // Get the function name. There are these situations: + // func function name + // "name" == func, "fudi.fd_dict" == NULL + // dict.func new dictionary entry + // "name" == NULL, "fudi.fd_dict" set, + // "fudi.fd_di" == NULL, "fudi.fd_newkey" == func + // dict.func existing dict entry with a Funcref + // "name" == func, "fudi.fd_dict" set, + // "fudi.fd_di" set, "fudi.fd_newkey" == NULL + // dict.func existing dict entry that's not a Funcref + // "name" == NULL, "fudi.fd_dict" set, + // "fudi.fd_di" set, "fudi.fd_newkey" == NULL + // s:func script-local function name + // g:func global function name, same as "func" + char *p = eap->arg; + char *name = save_function_name(&p, eap->skip, TFN_NO_AUTOLOAD, &fudi); + int paren = (vim_strchr(p, '(') != NULL); + if (name == NULL && (fudi.fd_dict == NULL || !paren) && !eap->skip) { + // Return on an invalid expression in braces, unless the expression + // evaluation has been cancelled due to an aborting error, an + // interrupt, or an exception. + if (!aborting()) { + if (fudi.fd_newkey != NULL) { + semsg(_(e_dictkey), fudi.fd_newkey); + } + xfree(fudi.fd_newkey); + return; + } + eap->skip = true; + } + + // An error in a function call during evaluation of an expression in magic + // braces should not cause the function not to be defined. + const int saved_did_emsg = did_emsg; + did_emsg = false; + + // + // ":function func" with only function name: list function. + // If bang is given: + // - include "!" in function head + // - exclude line numbers from function body + // + if (!paren) { + if (!ends_excmd(*skipwhite(p))) { + semsg(_(e_trailing_arg), p); + goto ret_free; + } + eap->nextcmd = check_nextcmd(p); + if (eap->nextcmd != NULL) { + *p = NUL; + } + if (!eap->skip && !got_int) { + fp = find_func(name); + if (fp != NULL) { + // Check no function was added or removed from a callback, e.g. at + // the more prompt. "fp" may then be invalid. + const int prev_ht_changed = func_hashtab.ht_changed; + + if (list_func_head(fp, !eap->forceit, eap->forceit) == OK) { + for (int j = 0; j < fp->uf_lines.ga_len && !got_int; j++) { + if (FUNCLINE(fp, j) == NULL) { + continue; + } + msg_putchar('\n'); + if (!eap->forceit) { + msg_outnum(j + 1); + if (j < 9) { + msg_putchar(' '); + } + if (j < 99) { + msg_putchar(' '); + } + if (function_list_modified(prev_ht_changed)) { + break; + } + } + msg_prt_line(FUNCLINE(fp, j), false); + line_breakcheck(); // show multiple lines at a time! + } + if (!got_int) { + msg_putchar('\n'); + if (!function_list_modified(prev_ht_changed)) { + msg_puts(eap->forceit ? "endfunction" : " endfunction"); + } + } + } + } else { + emsg_funcname(N_("E123: Undefined function: %s"), name); + } + } + goto ret_free; + } + + // ":function name(arg1, arg2)" Define function. + p = skipwhite(p); + if (*p != '(') { + if (!eap->skip) { + semsg(_("E124: Missing '(': %s"), eap->arg); + goto ret_free; + } + // attempt to continue by skipping some text + if (vim_strchr(p, '(') != NULL) { + p = vim_strchr(p, '('); + } + } + p = skipwhite(p + 1); + + ga_init(&newargs, (int)sizeof(char *), 3); + ga_init(&newlines, (int)sizeof(char *), 3); + + if (!eap->skip) { + // Check the name of the function. Unless it's a dictionary function + // (that we are overwriting). + if (name != NULL) { + arg = name; + } else { + arg = fudi.fd_newkey; + } + if (arg != NULL && (fudi.fd_di == NULL || !tv_is_func(fudi.fd_di->di_tv))) { + char *name_base = arg; + if ((uint8_t)(*arg) == K_SPECIAL) { + name_base = vim_strchr(arg, '_'); + if (name_base == NULL) { + name_base = arg + 3; + } else { + name_base++; + } + } + int i; + for (i = 0; name_base[i] != NUL && (i == 0 + ? eval_isnamec1(name_base[i]) + : eval_isnamec(name_base[i])); i++) {} + if (name_base[i] != NUL) { + emsg_funcname(e_invarg2, arg); + goto ret_free; + } + } + // Disallow using the g: dict. + if (fudi.fd_dict != NULL && fudi.fd_dict->dv_scope == VAR_DEF_SCOPE) { + emsg(_("E862: Cannot use g: here")); + goto ret_free; + } + } + + if (get_function_args(&p, ')', &newargs, &varargs, + &default_args, eap->skip) == FAIL) { + goto errret_2; + } + + if (KeyTyped && ui_has(kUICmdline)) { + show_block = true; + ui_ext_cmdline_block_append(0, eap->cmd); + } + + // find extra arguments "range", "dict", "abort" and "closure" + while (true) { + p = skipwhite(p); + if (strncmp(p, "range", 5) == 0) { + flags |= FC_RANGE; + p += 5; + } else if (strncmp(p, "dict", 4) == 0) { + flags |= FC_DICT; + p += 4; + } else if (strncmp(p, "abort", 5) == 0) { + flags |= FC_ABORT; + p += 5; + } else if (strncmp(p, "closure", 7) == 0) { + flags |= FC_CLOSURE; + p += 7; + if (current_funccal == NULL) { + emsg_funcname(N_("E932: Closure function should not be at top level: %s"), + name == NULL ? "" : name); + goto erret; + } + } else { + break; + } + } + + // When there is a line break use what follows for the function body. + // Makes 'exe "func Test()\n...\nendfunc"' work. + if (*p == '\n') { + line_arg = p + 1; + } else if (*p != NUL && *p != '"' && !eap->skip && !did_emsg) { + semsg(_(e_trailing_arg), p); + } + + // Read the body of the function, until ":endfunction" is found. + if (KeyTyped) { + // Check if the function already exists, don't let the user type the + // whole function before telling them it doesn't work! For a script we + // need to skip the body to be able to find what follows. + if (!eap->skip && !eap->forceit) { + if (fudi.fd_dict != NULL && fudi.fd_newkey == NULL) { + emsg(_(e_funcdict)); + } else if (name != NULL && find_func(name) != NULL) { + emsg_funcname(e_funcexts, name); + } + } + + if (!eap->skip && did_emsg) { + goto erret; + } + + if (!ui_has(kUICmdline)) { + msg_putchar('\n'); // don't overwrite the function name + } + cmdline_row = msg_row; + } + + // Save the starting line number. + linenr_T sourcing_lnum_top = SOURCING_LNUM; + + // Do not define the function when getting the body fails and when skipping. + if (get_function_body(eap, &newlines, line_arg, &line_to_free, show_block) == FAIL + || eap->skip) { goto erret; } @@ -2879,13 +2920,10 @@ erret: errret_2: ga_clear_strings(&newlines); ret_free: - xfree(skip_until); - xfree(heredoc_trimmed); xfree(line_to_free); xfree(fudi.fd_newkey); xfree(name); did_emsg |= saved_did_emsg; - need_wait_return |= saved_wait_return; if (show_block) { ui_ext_cmdline_block_leave(); } -- cgit