neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

help.lua (7193B)


      1 local M = {}
      2 
      3 local tag_exceptions = {
      4  -- Interpret asterisk (star, '*') literal but name it 'star'
      5  ['*'] = 'star',
      6  ['g*'] = 'gstar',
      7  ['[*'] = '[star',
      8  [']*'] = ']star',
      9  [':*'] = ':star',
     10  ['/*'] = '/star',
     11  ['/\\*'] = '/\\\\star',
     12  ['\\\\star'] = '/\\\\star',
     13  ['"*'] = 'quotestar',
     14  ['**'] = 'starstar',
     15  ['cpo-*'] = 'cpo-star',
     16 
     17  -- Literal question mark '?'
     18  ['?'] = '?',
     19  ['??'] = '??',
     20  [':?'] = ':?',
     21  ['?<CR>'] = '?<CR>',
     22  ['g?'] = 'g?',
     23  ['g?g?'] = 'g?g?',
     24  ['g??'] = 'g??',
     25  ['-?'] = '-?',
     26  ['q?'] = 'q?',
     27  ['v_g?'] = 'v_g?',
     28  ['/\\?'] = '/\\\\?',
     29 
     30  -- Backslash-escaping hell
     31  ['/\\%(\\)'] = '/\\\\%(\\\\)',
     32  ['/\\z(\\)'] = '/\\\\z(\\\\)',
     33  ['\\='] = '\\\\=',
     34  ['\\%$'] = '/\\\\%\\$',
     35 
     36  -- Some expressions are literal but without the 'expr-' prefix. Note: not all 'expr-' subjects!
     37  ['expr-!=?'] = '!=?',
     38  ['expr-!~?'] = '!\\~?',
     39  ['expr-<=?'] = '<=?',
     40  ['expr-<?'] = '<?',
     41  ['expr-==?'] = '==?',
     42  ['expr-=~?'] = '=~?',
     43  ['expr->=?'] = '>=?',
     44  ['expr->?'] = '>?',
     45  ['expr-is?'] = 'is?',
     46  ['expr-isnot?'] = 'isnot?',
     47 }
     48 
     49 ---Transform a help tag query into a search pattern for find_tags().
     50 ---
     51 ---This function converts user input from `:help {subject}` into a regex pattern that balances
     52 ---literal matching with wildcard support. Vim help tags can contain characters that have special
     53 ---meaning in regex (like *, ?, |), but we also want to support wildcard searches.
     54 ---
     55 ---Examples:
     56 ---  '*' --> 'star' (literal match for the * command help tag)
     57 ---  'buffer*' --> 'buffer.*' (wildcard: find all buffer-related tags)
     58 ---  'CTRL-W' --> stays as 'CTRL-W' (already in tag format)
     59 ---  '^A' --> 'CTRL-A' (caret notation converted to tag format)
     60 ---
     61 ---@param word string The help subject as entered by the user
     62 ---@return string pattern The escaped regex pattern to search for in tag files
     63 function M.escape_subject(word)
     64  local replacement = tag_exceptions[word]
     65  if replacement then
     66    return replacement
     67  end
     68 
     69  -- Add prefix '/\\' to patterns starting with a backslash
     70  -- Examples: \S, \%^, \%(, \zs, \z1, \@<, \@=, \@<=, \_$, \_^
     71  if word:match([[^\.$]]) or word:match('^\\[%%_z@]') then
     72    word = [[/\]] .. word
     73    word = word:gsub('[$.~]', [[\%0]])
     74    word = word:gsub('|', 'bar')
     75  else
     76    -- Fix for bracket expressions and curly braces:
     77    -- '\' --> '\\' (needs to come first)
     78    -- '[' --> '\[' (escape the opening bracket)
     79    -- ':[' --> ':\[' (escape the opening bracket)
     80    -- '\{' --> '\\{' (for '\{' pattern matching)
     81    -- '(' --> '' (parentheses around option tags should be ignored)
     82    word = word:gsub([[\+]], [[\\]])
     83    word = word:gsub([[^%[]], [[\[]])
     84    word = word:gsub([[^:%[]], [[:\[]])
     85    word = word:gsub([[^\{]], [[\\{]])
     86    word = word:gsub([[^%(']], [[']])
     87 
     88    word = word:gsub('|', 'bar')
     89    word = word:gsub([["]], 'quote')
     90    word = word:gsub('[$.~]', [[\%0]])
     91    word = word:gsub('%*', '.*')
     92    word = word:gsub('?', '.')
     93 
     94    -- Handle control characters.
     95    -- First convert raw control chars to the caret notation
     96    -- E.g. 0x01 --> '^A' etc.
     97    ---@type string
     98    word = word:gsub('([\1-\31])', function(ctrl_char)
     99      -- '^\' needs an extra backslash
    100      local repr = string.char(ctrl_char:byte() + 64):gsub([[\]], [[\\]])
    101      return '^' .. repr
    102    end)
    103 
    104    -- Change caret notation to 'CTRL-', except '^_'
    105    -- E.g. 'i^G^J' --> 'iCTRL-GCTRL-J'
    106    word = word:gsub('%^([^_])', 'CTRL-%1')
    107    -- Add underscores around 'CTRL-X' characters
    108    -- E.g. 'iCTRL-GCTRL-J' --> 'i_CTRL-G_CTRL-J'
    109    -- Only exception: 'CTRL-{character}'
    110    word = word:gsub('([^_])CTRL%-', '%1_CTRL-')
    111    word = word:gsub('(CTRL%-[^{])([^_\\])', '%1_%2')
    112 
    113    -- Skip function arguments
    114    -- E.g. 'abs({expr})' --> 'abs'
    115    -- E.g. 'abs([arg])' --> 'abs'
    116    word = word:gsub('%({.*', '')
    117    word = word:gsub('%(%[.*', '')
    118 
    119    -- Skip punctuation after second apostrophe/curly brace
    120    -- E.g. ''option',' --> ''option''
    121    -- E.g. '{address},' --> '{address}'
    122    -- E.g. '`command`,' --> 'command' (backticks are removed too, but '``' stays '``')
    123    word = word:gsub([[^'([^']*)'.*]], [['%1']])
    124    word = word:gsub([[^{([^}]*)}.*]], '{%1}')
    125    word = word:gsub([[.*`([^`]+)`.*]], '%1')
    126  end
    127 
    128  return word
    129 end
    130 
    131 ---Populates the |local-additions| section of a help buffer with references to locally-installed
    132 ---help files. These are help files outside of $VIMRUNTIME (typically from plugins) whose first
    133 ---line contains a tag (e.g. *plugin-name.txt*) and a short description.
    134 ---
    135 ---For each help file found in 'runtimepath', the first line is extracted and added to the buffer
    136 ---as a reference (converting '*tag*' to '|tag|'). If a translated version of a help file exists
    137 ---in the same language as the current buffer (e.g. 'plugin.nlx' alongside 'plugin.txt'), the
    138 ---translated version is preferred over the '.txt' file.
    139 function M.local_additions()
    140  local buf = vim.api.nvim_get_current_buf()
    141  local bufname = vim.fs.basename(vim.api.nvim_buf_get_name(buf))
    142 
    143  -- "help.txt" or "help.??x" where ?? is a language code, see |help-translated|.
    144  local lang = bufname:match('^help%.(%a%a)x$')
    145  if bufname ~= 'help.txt' and not lang then
    146    return
    147  end
    148 
    149  -- Find local help files
    150  ---@type table<string, string>
    151  local plugins = {}
    152  local pattern = lang and ('doc/*.{txt,%sx}'):format(lang) or 'doc/*.txt'
    153  for _, docpath in ipairs(vim.api.nvim_get_runtime_file(pattern, true)) do
    154    if not vim.fs.relpath(vim.env.VIMRUNTIME, docpath) then
    155      -- '/path/to/doc/plugin.txt' --> 'plugin'
    156      local plugname = vim.fs.basename(docpath):sub(1, -5)
    157      -- prefer language-specific files over .txt
    158      if not plugins[plugname] or vim.endswith(plugins[plugname], '.txt') then
    159        plugins[plugname] = docpath
    160      end
    161    end
    162  end
    163 
    164  -- Format plugin list lines
    165  -- Default to 78 if 'textwidth' is not set (e.g. in sandbox)
    166  local textwidth = math.max(vim.bo[buf].textwidth, 78)
    167  local lines = {}
    168  for _, path in vim.spairs(plugins) do
    169    local fp = io.open(path, 'r')
    170    if fp then
    171      local tagline = fp:read('*l') or ''
    172      fp:close()
    173      ---@type string, string
    174      local plugname, desc = tagline:match('^%*([^*]+)%*%s*(.*)$')
    175      if plugname and desc then
    176        -- left-align taglink and right-align description by inserting spaces in between
    177        local plug_width = vim.fn.strdisplaywidth(plugname)
    178        local _, concealed_chars = desc:gsub('|', '')
    179        local desc_width = vim.fn.strdisplaywidth(desc) - concealed_chars
    180        -- max(l, 1) forces at least one space for if the description is too long
    181        local spaces = string.rep(' ', math.max(textwidth - desc_width - plug_width - 2, 1))
    182        local fmt = string.format('|%s|%s%s', plugname, spaces, desc)
    183        table.insert(lines, fmt)
    184      end
    185    end
    186  end
    187 
    188  -- Add plugin list to local-additions section
    189  for linenr, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
    190    if line:find('*local-additions*', 1, true) then
    191      vim._with({ buf = buf, bo = { modifiable = true, readonly = false } }, function()
    192        vim.api.nvim_buf_set_lines(buf, linenr, linenr, true, lines)
    193      end)
    194      break
    195    end
    196  end
    197 end
    198 
    199 return M