mirror of
https://github.com/neovim/neovim.git
synced 2026-03-27 19:02:02 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
21
runtime/lua/vim/_meta/options.lua
generated
21
runtime/lua/vim/_meta/options.lua
generated
@@ -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.
|
||||
---
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user