feat(help): super K (":help!") guesses tag at cursor #36205

Problem:
`K` in help files may fail in some noisy text. Example:

      (`fun(config: vim.lsp.ClientConfig): boolean`)
                            ^cursor

Solution:
- `:help!` (bang, no args) activates DWIM behavior: tries `<cWORD>`,
  then trims punctuation until a valid tag is found.
- Set `keywordprg=:help!` by default.
- Does not affect `CTRL-]`, that is still fully "tags" based.
This commit is contained in:
Justin M. Keyes
2026-03-15 19:02:49 -04:00
committed by GitHub
parent 747da13f44
commit 16f7440cc7
14 changed files with 495 additions and 74 deletions

View File

@@ -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('<cword>')`.
*{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 so<C-V><CR>only
<
: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.

View File

@@ -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

View File

@@ -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*

View File

@@ -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

View File

@@ -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 `<cWORD>` at cursor, tries it first, then trims punctuation and dots until a valid help
--- tag is found. Falls back to `<cword>` (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('<cWORD>')
if not tag or tag == '' then
return nil
end
-- Compute cursor offset within <cWORD>.
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 <cWORD>
-- 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 <cword> 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 <cWORD> 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.

View File

@@ -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.
---

View File

@@ -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;

View File

@@ -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

View File

@@ -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 <identifier").
if (cmdchar == 'K' && kp_helpbang && !visual_sel) {
// Special case: ":help!": Don't get the identifier, ex_help will get cWORD at cursor.
// nv_K_getcmd already set `buf="help!"` so we don't need to do anything here.
STRCPY(buf, "help!");
buflen = STRLEN_LITERAL("help!");
} else if (cmdchar == 'K' && !kp_help) {
ptr = xstrnsave(ptr, n);
if (kp_ex) {
// Escape the argument properly for an Ex command
@@ -3519,8 +3532,8 @@ static void nv_ident(cmdarg_T *cap)
}
p = buf + buflen;
// Escape various chars with a backslash "\".
while (n-- > 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.

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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('<cWORD>')
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 <cword> 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.<type>.<ft>` 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<cr>')
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.<lt>type><cr>')
eq({ '`@lsp.type.<type>.<ft>`', 'lsp.txt' }, buf_word())
-- ^ cursor on "type"
n.command(':help!')
eq({ '*type()*', 'vimfn.txt' }, buf_word())
n.feed('<c-o>f<lt>')
eq({ '`@lsp.type.<type>.<ft>`', 'lsp.txt' }, buf_word())
-- ^ cursor on "<"
n.command(':help!')
n.command(':help lsp')
n.feed('gg/codelens.run()|<cr>')
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')

View File

@@ -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