diff --git a/runtime/doc/helphelp.txt b/runtime/doc/helphelp.txt index bb39e94abf..a8ca6db4ea 100644 --- a/runtime/doc/helphelp.txt +++ b/runtime/doc/helphelp.txt @@ -161,6 +161,18 @@ Help commands *online-help* Type |gO| to see the table of contents. + *:help!* +:h[elp]! Guesses a help tag from the |WORD| at cursor, in DWIM ("Do + What I Mean") fashion: trims punctuation using various + heuristics until a valid help tag is found. + + For example, move the cursor anywhere in this code, + then run `:help!`: > + + local v = vim.version.parse(vim.system({'foo'}):wait().stdout) +< + Then compare to `:exe 'help' expand('')`. + *{subject}* *E149* *E661* :h[elp] {subject} Like ":help", additionally jump to the tag {subject}. For example: > @@ -234,9 +246,10 @@ Help commands *online-help* :help soonly < -:h[elp]! [subject] Like ":help", but in non-English help files prefer to - find a tag in a file with the same language as the - current file. See |help-translated|. +:h[elp]! {subject} Like ":help", but prefers tags with the same + |help-translated| language as the current file. + + Unrelated to |:help!| (no {subject}). *:helpc* *:helpclose* :helpc[lose] Close one help window, if there is one. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 7e5f7565b1..e35f26680a 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -220,6 +220,10 @@ EDITOR whitespace in indented lines. • |:uniq| deduplicates text in the current buffer. • |omnicompletion| in `help` buffer. |ft-help-omni| +• |:help!| has DWIM ("Do What I Mean") behavior: it tries to guess the help + tag at cursor. In help buffers, 'keywordprg' defaults to ":help!". For + example, try "K" anywhere in this code: > + local v = vim.version.parse(vim.system({'foo'}):wait().stdout) • Setting "'0" in 'shada' prevents storing the jumplist in the shada file. • 'shada' now correctly respects "/0" and "f0". • |prompt-buffer| supports multiline input/paste, undo/redo, and o/O normal diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index f40a4ee622..b6d61575cc 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -3904,18 +3904,25 @@ A jump table for the options with a short description can be found at |Q_op|. 'keywordprg' 'kp' string (default ":Man", Windows: ":help") global or local to buffer |global-local| Program to use for the |K| command. Environment variables are - expanded |:set_env|. ":help" may be used to access the Vim internal - help. (Note that previously setting the global option to the empty - value did this, which is now deprecated.) - When the first character is ":", the command is invoked as a Vim - Ex command prefixed with [count]. - When "man" or "man -s" is used, Vim will automatically translate - a [count] for the "K" command to a section number. + expanded |:set_env|. + + Special cases: + - ":help" opens the |word| at cursor using |:help|. (Note that + previously setting the global option to the empty value did this, + which is now deprecated.) + - ":help!" performs |:help!| (DWIM) on the |WORD| at cursor. + - If the value starts with ":", it is invoked as an Ex command + prefixed with [count]. + - If "man" or "man -s", [count] is the manpage section number. + See |option-backslash| about including spaces and backslashes. + Example: >vim + set keywordprg=:help! set keywordprg=man\ -s set keywordprg=:Man -< This option cannot be set from a |modeline| or in the |sandbox|, for +< + This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. *'langmap'* *'lmap'* *E357* *E358* diff --git a/runtime/ftplugin/help.vim b/runtime/ftplugin/help.vim index e8651b5d59..7f60953738 100644 --- a/runtime/ftplugin/help.vim +++ b/runtime/ftplugin/help.vim @@ -17,7 +17,7 @@ let b:undo_ftplugin = "setl isk< fo< tw< cole< cocu< keywordprg< omnifunc< comme setl comments= cms= -setlocal formatoptions+=tcroql textwidth=78 keywordprg=:help omnifunc=s:HelpComplete +setlocal formatoptions+=tcroql textwidth=78 keywordprg=:help! omnifunc=s:HelpComplete let &l:iskeyword='!-~,^*,^|,^",192-255' if has("conceal") setlocal cole=2 cocu=nc diff --git a/runtime/lua/vim/_core/help.lua b/runtime/lua/vim/_core/help.lua index 8eae4e993b..87f9d462de 100644 --- a/runtime/lua/vim/_core/help.lua +++ b/runtime/lua/vim/_core/help.lua @@ -128,6 +128,201 @@ function M.escape_subject(word) return word end +--- Characters that are considered punctuation for trimming help tags. +--- Dots (.) are NOT included here — they're trimmed separately as a last resort. +local trimmable_punct = { + ['('] = true, + [')'] = true, + ['<'] = true, + ['>'] = true, + ['['] = true, + [']'] = true, + ['{'] = true, + ['}'] = true, + ['`'] = true, + ['|'] = true, + ['"'] = true, + [','] = true, + ["'"] = true, + [' '] = true, + ['\t'] = true, +} + +--- Trim one layer of punctuation from a help tag string. +--- Uses cursor offset to intelligently trim: if cursor is on trimmable punctuation, +--- removes everything before cursor and skips past punctuation after cursor. +--- +---@param tag string The tag to trim +---@param offset integer Cursor position within the tag (-1 if not applicable) +---@return string? trimmed Trimmed string, or nil if unchanged +local function trim_tag(tag, offset) + if not tag or tag == '' then + return nil + end + + -- Special cases: single character tags + if tag == '|' then + return 'bar' + end + if tag == '"' then + return 'quote' + end + + local len = #tag + -- start/end are 1-indexed inclusive positions into tag + local s = 1 + local e = len + + if offset >= 0 and offset < len and trimmable_punct[tag:sub(offset + 1, offset + 1)] then + -- Heuristic: cursor is on trimmable punctuation, skip past it to the right + s = offset + 1 + while s <= e and trimmable_punct[tag:sub(s, s)] do + s = s + 1 + end + elseif offset >= 0 and offset < len then + -- Cursor is on non-trimmable char: find start of identifier at cursor + local cursor_pos = offset + 1 -- 1-indexed + while cursor_pos > s and not trimmable_punct[tag:sub(cursor_pos - 1, cursor_pos - 1)] do + cursor_pos = cursor_pos - 1 + end + s = cursor_pos + else + -- No cursor info: trim leading punctuation + while s <= e and trimmable_punct[tag:sub(s, s)] do + s = s + 1 + end + end + + -- Trim trailing punctuation + while e >= s and trimmable_punct[tag:sub(e, e)] do + e = e - 1 + end + + -- Truncate at "(" with args, e.g. "foo('bar')" => "foo". + -- But keep "()" since it's part of valid tags like "vim.fn.expand()". + for i = s, e do + if tag:sub(i, i) == '(' and not (i + 1 <= e and tag:sub(i + 1, i + 1) == ')') then + e = i - 1 + break + end + end + + -- If nothing changed, return nil + if s == 1 and e == len then + return nil + end + + -- If everything was trimmed, return nil + if s > e then + return nil + end + + return tag:sub(s, e) +end + +--- Trim namespace prefix (dots) from a help tag. +--- Only call this if regular trimming didn't find a match. +--- Returns the tag with the leftmost dot-separated segment removed. +--- +---@param tag string The tag to trim +---@return string? trimmed Trimmed string, or nil if no dots found +local function trim_tag_dots(tag) + if not tag or tag == '' then + return nil + end + local after_dot = tag:match('^[^.]+%.(.+)$') + return after_dot +end + +--- For ":help!" (bang, no args): DWIM resolve a help tag from the cursor context. +--- Gets `` at cursor, tries it first, then trims punctuation and dots until a valid help +--- tag is found. Falls back to `` (keyword at cursor) before dot-trimming. +--- +---@return string? resolved The resolved help tag, or nil if no match found +function M.resolve_tag() + local tag = vim.fn.expand('') + if not tag or tag == '' then + return nil + end + + -- Compute cursor offset within . + local line = vim.api.nvim_get_current_line() + local col = vim.fn.col('.') -- 1-indexed + local s = col + -- Scan backward from col('.') to find the whitespace boundary. + while s > 1 and not line:sub(s - 1, s - 1):match('%s') do + s = s - 1 + end + local offset = col - s -- 0-indexed offset within + + -- Try the original tag first. + if #vim.fn.getcompletion(tag, 'help') > 0 then + return tag + end + + -- Extract |tag| reference if the cursor is inside one (help's link syntax). + local pipe_tag = tag:match('|(.+)|') + if pipe_tag and #vim.fn.getcompletion(pipe_tag, 'help') > 0 then + return pipe_tag + end + + -- Iteratively trim punctuation and try again, up to 10 times. + local candidate = tag + for _ = 1, 10 do + local trimmed = trim_tag(candidate, offset) + if not trimmed then + break + end + candidate = trimmed + -- After first trim, offset is no longer valid. + offset = -1 + + if #vim.fn.getcompletion(candidate, 'help') > 0 then + return candidate + end + end + + -- Try the word (alphanumeric/underscore run) at the cursor before dot-trimming, since + -- dot-trimming strips from the left and may move away from the cursor position. + -- E.g. for '@lsp.type.function' with cursor on "lsp", the word is "lsp". + -- Note: we don't use because it depends on 'iskeyword'. + local word_s, word_e = col, col + -- If cursor is not on a word char, find the nearest word char to the right. + if not line:sub(col, col):match('[%w_]') then + while word_s <= #line and not line:sub(word_s, word_s):match('[%w_]') do + word_s = word_s + 1 + end + word_e = word_s + end + while word_s > 1 and line:sub(word_s - 1, word_s - 1):match('[%w_]') do + word_s = word_s - 1 + end + while word_e <= #line and line:sub(word_e, word_e):match('[%w_]') do + word_e = word_e + 1 + end + word_e = word_e - 1 + local cword = line:sub(word_s, word_e) + if #cword > 1 and cword ~= tag and #vim.fn.getcompletion(cword, 'help') > 0 then + return cword + end + + -- Try trimming namespace dots (left-to-right). + for _ = 1, 10 do + local trimmed = trim_tag_dots(candidate) + if not trimmed then + break + end + candidate = trimmed + + if #vim.fn.getcompletion(candidate, 'help') > 0 then + return candidate + end + end + + -- No match found: return raw so the caller can show it in an error message. + return tag +end + ---Populates the |local-additions| section of a help buffer with references to locally-installed ---help files. These are help files outside of $VIMRUNTIME (typically from plugins) whose first ---line contains a tag (e.g. *plugin-name.txt*) and a short description. diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 6652045c87..ab24f03595 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -3834,20 +3834,27 @@ vim.go.keymodel = vim.o.keymodel vim.go.km = vim.go.keymodel --- Program to use for the `K` command. Environment variables are ---- expanded `:set_env`. ":help" may be used to access the Vim internal ---- help. (Note that previously setting the global option to the empty ---- value did this, which is now deprecated.) ---- When the first character is ":", the command is invoked as a Vim ---- Ex command prefixed with [count]. ---- When "man" or "man -s" is used, Vim will automatically translate ---- a [count] for the "K" command to a section number. +--- expanded `:set_env`. +--- +--- Special cases: +--- - ":help" opens the `word` at cursor using `:help`. (Note that +--- previously setting the global option to the empty value did this, +--- which is now deprecated.) +--- - ":help!" performs `:help!` (DWIM) on the `WORD` at cursor. +--- - If the value starts with ":", it is invoked as an Ex command +--- prefixed with [count]. +--- - If "man" or "man -s", [count] is the manpage section number. +--- --- See `option-backslash` about including spaces and backslashes. +--- --- Example: --- --- ```vim +--- set keywordprg=:help! --- set keywordprg=man\ -s --- set keywordprg=:Man --- ``` +--- --- This option cannot be set from a `modeline` or in the `sandbox`, for --- security reasons. --- diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index b49730da78..8200dc3fde 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -7585,7 +7585,7 @@ char *eval_vars(char *src, const char *srcstart, size_t *usedlen, linenr_T *lnum ? (FIND_IDENT | FIND_STRING) : (spec_idx == SPEC_CEXPR ? (FIND_IDENT | FIND_STRING | FIND_EVAL) - : FIND_STRING)); + : FIND_STRING), NULL); if (resultlen == 0) { *errormsg = ""; return NULL; diff --git a/src/nvim/help.c b/src/nvim/help.c index da7aeee1ad..5b7cfcecbe 100644 --- a/src/nvim/help.c +++ b/src/nvim/help.c @@ -32,6 +32,7 @@ #include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/message.h" +#include "nvim/normal.h" #include "nvim/option.h" #include "nvim/option_defs.h" #include "nvim/option_vars.h" @@ -52,6 +53,7 @@ #include "help.c.generated.h" /// ":help": open a read-only window on a help file +/// ":help!": DWIM parse the best match at cursor void ex_help(exarg_T *eap) { char *arg; @@ -76,11 +78,6 @@ void ex_help(exarg_T *eap) } arg = eap->arg; - if (eap->forceit && *arg == NUL && !curbuf->b_help) { - emsg(_("E478: Don't panic!")); - return; - } - if (eap->skip) { // not executing commands return; } @@ -97,11 +94,28 @@ void ex_help(exarg_T *eap) // Check for a specified language char *lang = check_help_lang(arg); + // ":help!" (bang, no args). + bool helpbang = (eap != NULL && eap->forceit && *arg == NUL); + // When no argument given go to the index. - if (*arg == NUL) { + if (*arg == NUL && !helpbang) { arg = "help.txt"; } + // ":help!" (bang, no args): DWIM help, resolve best tag at cursor via Lua. + char *allocated_arg = NULL; + if (helpbang) { + Error err = ERROR_INIT; + Object res = NLUA_EXEC_STATIC("return require'vim._core.help'.resolve_tag()", + (Array)ARRAY_DICT_INIT, kRetObject, NULL, &err); + if (!ERROR_SET(&err) && res.type == kObjectTypeString && res.data.string.size > 0) { + allocated_arg = xstrdup(res.data.string.data); + arg = allocated_arg; + } + api_free_object(res); + api_clear_error(&err); + } + // Check if there is a match for the argument. int n = find_help_tags(arg, &num_matches, &matches, eap != NULL && eap->forceit); @@ -125,6 +139,7 @@ void ex_help(exarg_T *eap) if (n != FAIL) { FreeWild(num_matches, matches); } + xfree(allocated_arg); return; } @@ -214,6 +229,7 @@ void ex_help(exarg_T *eap) erret: xfree(tag); + xfree(allocated_arg); } /// ":helpclose": Close one help window diff --git a/src/nvim/normal.c b/src/nvim/normal.c index d45b4a4a28..3f464fb6ce 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1617,12 +1617,7 @@ static bool find_is_eval_item(const char *const ptr, int *const colp, int *const return false; } -/// Find the identifier under or to the right of the cursor. -/// "find_type" can have one of three values: -/// FIND_IDENT: find an identifier (keyword) -/// FIND_STRING: find any non-white text -/// FIND_IDENT + FIND_STRING: find any non-white text, identifier preferred. -/// FIND_EVAL: find text useful for C program debugging +/// Finds the identifier under or to the right of the cursor, and stores it in `text`. /// /// There are three steps: /// 1. Search forward for the start of an identifier/text. Doesn't move if @@ -1633,15 +1628,25 @@ static bool find_is_eval_item(const char *const ptr, int *const colp, int *const /// 3. Search forward to the end of this identifier/text. /// When FIND_IDENT isn't defined, we backup until a blank. /// -/// @return the length of the text, or zero if no text is found. -/// -/// If text is found, a pointer to the text is put in "*text". This -/// points into the current buffer line and is not always NUL terminated. -size_t find_ident_under_cursor(char **text, int find_type) +/// @param text If text is found, a pointer to the text is put in `*text`. This points into the +/// current buffer line and is not always NUL terminated. +/// @param find_type One of three values: +/// - FIND_IDENT: find an identifier (keyword) +/// - FIND_STRING: find any non-white text +/// - FIND_IDENT + FIND_STRING: find any non-white text, identifier preferred. +/// - FIND_EVAL: find text useful for C program debugging +/// @param offset If not NULL, gets cursor position relative to start of `text`. +/// @return Text length, or zero if no text is found. +size_t find_ident_under_cursor(char **text, int find_type, int *offset) FUNC_ATTR_NONNULL_ARG(1) { - return find_ident_at_pos(curwin, curwin->w_cursor.lnum, - curwin->w_cursor.col, text, NULL, find_type); + int textcol = 0; + size_t len = find_ident_at_pos(curwin, curwin->w_cursor.lnum, + curwin->w_cursor.col, text, offset ? &textcol : NULL, find_type); + if (offset) { + *offset = curwin->w_cursor.col - textcol; + } + return len; } /// Like find_ident_under_cursor(), but for any window and any position. @@ -2311,7 +2316,7 @@ static void nv_gd(oparg_T *oap, int nchar, int thisblock) { size_t len; char *ptr; - if ((len = find_ident_under_cursor(&ptr, FIND_IDENT)) == 0 + if ((len = find_ident_under_cursor(&ptr, FIND_IDENT, NULL)) == 0 || !find_decl(ptr, len, nchar == 'd', thisblock, SEARCH_START)) { clearopbeep(oap); return; @@ -2745,7 +2750,7 @@ static int nv_zg_zw(cmdarg_T *cap, int nchar) curwin->w_cursor = pos; } - if (ptr == NULL && (len = find_ident_under_cursor(&ptr, FIND_IDENT)) == 0) { + if (ptr == NULL && (len = find_ident_under_cursor(&ptr, FIND_IDENT, NULL)) == 0) { return FAIL; } assert(len <= INT_MAX); @@ -3313,15 +3318,14 @@ void do_nv_ident(int c1, int c2) nv_ident(&ca); } -/// 'K' normal-mode command. Get the command to lookup the keyword under the -/// cursor. +/// Sets `buf` to the Ex command which will perform the "K" normal-mode command. static size_t nv_K_getcmd(cmdarg_T *cap, char *kp, bool kp_help, bool kp_ex, char **ptr_arg, size_t n, char *buf, size_t bufsize, size_t *buflen) { if (kp_help) { - // in the help buffer - STRCPY(buf, "he! "); - *buflen = STRLEN_LITERAL("he! "); + // :help or :help! + STRCPY(buf, "help! "); + *buflen = STRLEN_LITERAL("help! "); return n; } @@ -3403,20 +3407,23 @@ static void nv_ident(cmdarg_T *cap) } // The "]", "CTRL-]" and "K" commands accept an argument in Visual mode. + bool visual_sel = false; if (cmdchar == ']' || cmdchar == Ctrl_RSB || cmdchar == 'K') { if (VIsual_active && get_visual_text(cap, &ptr, &n) == false) { return; } + visual_sel = (ptr != NULL); if (checkclearopq(cap->oap)) { return; } } + int ident_offset = 0; if (ptr == NULL && (n = find_ident_under_cursor(&ptr, ((cmdchar == '*' || cmdchar == '#') ? FIND_IDENT|FIND_STRING - : FIND_IDENT))) == 0) { + : FIND_IDENT), &ident_offset)) == 0) { clearop(cap->oap); return; } @@ -3425,8 +3432,9 @@ static void nv_ident(cmdarg_T *cap) // double the length of the word. p_kp / curbuf->b_p_kp could be added // and some numbers. char *kp = *curbuf->b_p_kp == NUL ? p_kp : curbuf->b_p_kp; // 'keywordprg' - bool kp_help = (*kp == NUL || strcmp(kp, ":he") == 0 || strcmp(kp, ":help") == 0); - if (kp_help && *skipwhite(ptr) == NUL) { + bool kp_helpbang = strequal(kp, ":help!"); + bool kp_help = kp_helpbang || *kp == NUL || strequal(kp, ":he") || strequal(kp, ":help"); + if (kp_help && !kp_helpbang && *skipwhite(ptr) == NUL) { emsg(_(e_noident)); // found white space only return; } @@ -3462,30 +3470,35 @@ static void nv_ident(cmdarg_T *cap) case ']': tag_cmd = true; - STRCPY(buf, "ts "); - buflen = STRLEN_LITERAL("ts "); + STRCPY(buf, "tselect "); + buflen = STRLEN_LITERAL("tselect "); break; default: tag_cmd = true; if (curbuf->b_help) { - STRCPY(buf, "he! "); - buflen = STRLEN_LITERAL("he! "); + STRCPY(buf, "help! "); + buflen = STRLEN_LITERAL("help! "); } else { if (g_cmd) { - STRCPY(buf, "tj "); - buflen = STRLEN_LITERAL("tj "); + STRCPY(buf, "tjump "); + buflen = STRLEN_LITERAL("tjump "); } else if (cap->count0 == 0) { - STRCPY(buf, "ta "); - buflen = STRLEN_LITERAL("ta "); + STRCPY(buf, "tag "); + buflen = STRLEN_LITERAL("tag "); } else { - buflen = (size_t)snprintf(buf, bufsize, ":%" PRId64 "ta ", (int64_t)cap->count0); + buflen = (size_t)snprintf(buf, bufsize, ":%" PRId64 "tag ", (int64_t)cap->count0); } } } - // Now grab the chars in the identifier - if (cmdchar == 'K' && !kp_help) { + // Get the identifier at cursor/selection and append to `buf` (to get ":foo 0) { - // put a backslash before \ and some others if (vim_strchr(aux_ptr, (uint8_t)(*ptr)) != NULL) { *p++ = '\\'; } @@ -4228,7 +4241,7 @@ static void nv_brackets(cmdarg_T *cap) char *ptr; size_t len; - if ((len = find_ident_under_cursor(&ptr, FIND_IDENT)) == 0) { + if ((len = find_ident_under_cursor(&ptr, FIND_IDENT, NULL)) == 0) { clearop(cap->oap); } else { // Make a copy, if the line was changed it will be freed. diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 463ca0e086..bbc66cde5e 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -5058,24 +5058,31 @@ local options = { abbreviation = 'kp', defaults = { condition = 'MSWIN', - if_true = ':help', + if_true = ':help!', if_false = ':Man', doc = '":Man", Windows: ":help"', }, desc = [=[ Program to use for the |K| command. Environment variables are - expanded |:set_env|. ":help" may be used to access the Vim internal - help. (Note that previously setting the global option to the empty - value did this, which is now deprecated.) - When the first character is ":", the command is invoked as a Vim - Ex command prefixed with [count]. - When "man" or "man -s" is used, Vim will automatically translate - a [count] for the "K" command to a section number. + expanded |:set_env|. + + Special cases: + - ":help" opens the |word| at cursor using |:help|. (Note that + previously setting the global option to the empty value did this, + which is now deprecated.) + - ":help!" performs |:help!| (DWIM) on the |WORD| at cursor. + - If the value starts with ":", it is invoked as an Ex command + prefixed with [count]. + - If "man" or "man -s", [count] is the manpage section number. + See |option-backslash| about including spaces and backslashes. + Example: >vim + set keywordprg=:help! set keywordprg=man\ -s set keywordprg=:Man - < This option cannot be set from a |modeline| or in the |sandbox|, for + < + This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. ]=], expand = true, diff --git a/src/nvim/register.c b/src/nvim/register.c index 1d139cd933..e6e8401f26 100644 --- a/src/nvim/register.c +++ b/src/nvim/register.c @@ -872,7 +872,7 @@ bool get_spec_reg(int regname, char **argp, bool *allocated, bool errmsg) } size_t cnt = find_ident_under_cursor(argp, (regname == Ctrl_W ? (FIND_IDENT|FIND_STRING) - : FIND_STRING)); + : FIND_STRING), NULL); *argp = cnt ? xmemdupz(*argp, cnt) : NULL; *allocated = true; return true; diff --git a/src/nvim/window.c b/src/nvim/window.c index da4f74fc76..e07ba21fea 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -693,7 +693,7 @@ wingotofile: CHECK_CMDWIN; size_t len; char *ptr; - if ((len = find_ident_under_cursor(&ptr, FIND_IDENT)) == 0) { + if ((len = find_ident_under_cursor(&ptr, FIND_IDENT, NULL)) == 0) { break; } diff --git a/test/functional/ex_cmds/help_spec.lua b/test/functional/ex_cmds/help_spec.lua index 540ee42134..98caa8a8d7 100644 --- a/test/functional/ex_cmds/help_spec.lua +++ b/test/functional/ex_cmds/help_spec.lua @@ -10,6 +10,34 @@ local mkdir = t.mkdir local rmdir = n.rmdir local write_file = t.write_file +local cursor = n.api.nvim_win_set_cursor + +local function buf_word() + local word = n.fn.expand('') + local bufname = n.fn.fnamemodify(n.fn.bufname('%'), ':t') + return { word, bufname } +end + +local function open_helptag() + -- n.exec [[:normal! K]] + n.exec [[:help!]] + local rv = buf_word() + if n.fn.winnr('$') > 1 then + n.command('close') + end + return rv +end + +local function set_lines(text) + n.exec_lua( + [[ + vim.cmd'%delete _' + vim.api.nvim_paste(vim.text.indent(-1, ...), false, -1) + ]], + text + ) +end + describe(':help', function() before_each(clear) @@ -121,6 +149,136 @@ describe(':help', function() check_tag([[help \|]], [[*/\bar*]]) end) + it('":help!" (bang + no args) guesses the best tag near cursor', function() + n.command('helptags ++t $VIMRUNTIME/doc') + -- n.command('enew') + -- n.command('set filetype=help') + -- n.command [[set keywordprg=:help]] + + -- Failure modes: + set_lines 'xxxxxxxxx' + cursor(0, { 1, 4 }) + t.matches('E149: Sorry, no help for xxxxxxxxx', t.pcall_err(n.exec, [[:help!]])) + + -- Success: + + set_lines 'some plain text' + cursor(0, { 1, 5 }) -- on 'plain' + eq({ '*ft-plaintex-syntax*', 'syntax.txt' }, open_helptag()) + + set_lines ':help command' + cursor(0, { 1, 4 }) + eq({ '*:help*', 'helphelp.txt' }, open_helptag()) + + set_lines ' :help command' + cursor(0, { 1, 5 }) + eq({ '*:command*', 'map.txt' }, open_helptag()) + + set_lines 'v:version name' + cursor(0, { 1, 5 }) + eq({ '*v:version*', 'vvars.txt' }, open_helptag()) + cursor(0, { 1, 2 }) + eq({ '*v:version*', 'vvars.txt' }, open_helptag()) + + set_lines "See 'option' for more." + cursor(0, { 1, 6 }) -- on 'option' + eq({ "*'option'*", 'helphelp.txt' }, open_helptag()) + + set_lines ':command-nargs' + cursor(0, { 1, 7 }) -- on 'nargs' + eq({ '*:command-nargs*', 'map.txt' }, open_helptag()) + + set_lines '|("vim.lsp.foldtext()")|' + cursor(0, { 1, 10 }) + eq({ '*vim.lsp.foldtext()*', 'lsp.txt' }, open_helptag()) + + set_lines 'nvim_buf_detach_event[{buf}]' + cursor(0, { 1, 10 }) + eq({ '*nvim_buf_detach_event*', 'api.txt' }, open_helptag()) + + set_lines '{buf}' + cursor(0, { 1, 1 }) + eq({ '*:buf*', 'windows.txt' }, open_helptag()) + + set_lines '(`vim.lsp.ClientConfig`)' + cursor(0, { 1, 1 }) + eq({ '*vim.lsp.ClientConfig*', 'lsp.txt' }, open_helptag()) + + set_lines "vim.lsp.enable('clangd')" + cursor(0, { 1, 3 }) + eq({ '*vim.lsp.enable()*', 'lsp.txt' }, open_helptag()) + + set_lines "vim.lsp.enable('clangd')" + cursor(0, { 1, 6 }) + eq({ '*vim.lsp.enable()*', 'lsp.txt' }, open_helptag()) + + set_lines "vim.lsp.enable('clangd')" + cursor(0, { 1, 9 }) + eq({ '*vim.lsp.enable()*', 'lsp.txt' }, open_helptag()) + + set_lines 'assert(vim.lsp.get_client_by_id(client_id))' + cursor(0, { 1, 12 }) + eq({ '*vim.lsp.get_client_by_id()*', 'lsp.txt' }, open_helptag()) + + set_lines "vim.api.nvim_create_autocmd('LspAttach', {" + cursor(0, { 1, 7 }) + eq({ '*nvim_create_autocmd()*', 'api.txt' }, open_helptag()) + + -- Falls back to when all trimming fails. + set_lines "'@lsp.type.function'" + cursor(0, { 1, 2 }) -- on 'lsp' + eq({ '*lsp*', 'lsp.txt' }, open_helptag()) + set_lines "'@lsp.type.function'" + cursor(0, { 1, 14 }) -- on 'function' + eq({ '*:function*', 'userfunc.txt' }, open_helptag()) + + set_lines ' • `@lsp.type..` for the type' + cursor(0, { 1, 6 }) -- on backtick '`' (byte 6, after 2 spaces + 3-byte '•' + space) + eq({ '*lsp*', 'lsp.txt' }, open_helptag()) + + set_lines [[ + - `root_dir` usages akin to >lua + root_dir = require'lspconfig.util'.root_pattern(...) + < + require'lspconfig.util'.root_pattern(...) + ]] + cursor(0, { 2, 17 }) -- on "require" + eq({ '*require()*', 'luaref.txt' }, open_helptag()) + + set_lines '`:lsp restart`. You' + cursor(0, { 1, 6 }) -- on "restart" + eq({ '*:restart*', 'gui.txt' }, open_helptag()) + + -- + -- Test with actual helpfiles. This affects getcompletion(…,'help') ... + -- + + n.command(':help lua') + n.feed('gg/package.searchpath') + eq({ "vim.cmd.edit(package.searchpath('jit.p',", 'lua.txt' }, buf_word()) + -- ^ cursor on "package" + n.command(':help!') + eq({ '*packages*', 'pack.txt' }, buf_word()) + + n.command(':help lsp') + n.feed('gg/type.type>') + eq({ '`@lsp.type..`', 'lsp.txt' }, buf_word()) + -- ^ cursor on "type" + n.command(':help!') + eq({ '*type()*', 'vimfn.txt' }, buf_word()) + n.feed('f') + eq({ '`@lsp.type..`', 'lsp.txt' }, buf_word()) + -- ^ cursor on "<" + n.command(':help!') + + n.command(':help lsp') + n.feed('gg/codelens.run()|') + eq({ '|vim.lsp.codelens.run()|.', 'lsp.txt' }, buf_word()) + -- ^ cursor on "codelens" + n.command(':help!') + eq({ '*vim.lsp.codelens.run()*', 'lsp.txt' }, buf_word()) + end) + it('window closed makes cursor return to a valid win/buf #9773', function() n.add_builddir_to_rtp() command('help help') diff --git a/test/old/testdir/test_help.vim b/test/old/testdir/test_help.vim index 09814fffe6..b69d10f381 100644 --- a/test/old/testdir/test_help.vim +++ b/test/old/testdir/test_help.vim @@ -53,7 +53,8 @@ endfunc func Test_help_errors() call assert_fails('help doesnotexist', 'E149:') - call assert_fails('help!', 'E478:') + " Nvim: `:help!` is DWIM, not an error. + " call assert_fails('help!', 'E478:') if has('multi_lang') call assert_fails('help help@xy', 'E661:') endif