mirror of
https://github.com/neovim/neovim.git
synced 2026-02-21 01:40:19 +10:00
Problem: - ~200 line function of hard-to-maintain C code. - Local Addition section looks messy because of the varying description formats. Solution: - Move code to Lua. - Have a best-effort approach where short descriptions are right aligned, giving a cleaner look. Long descriptions are untouched.
199 lines
7.0 KiB
Lua
199 lines
7.0 KiB
Lua
local M = {}
|
|
|
|
local tag_exceptions = {
|
|
-- Interpret asterisk (star, '*') literal but name it 'star'
|
|
['*'] = 'star',
|
|
['g*'] = 'gstar',
|
|
['[*'] = '[star',
|
|
[']*'] = ']star',
|
|
[':*'] = ':star',
|
|
['/*'] = '/star',
|
|
['/\\*'] = '/\\\\star',
|
|
['\\\\star'] = '/\\\\star',
|
|
['"*'] = 'quotestar',
|
|
['**'] = 'starstar',
|
|
['cpo-*'] = 'cpo-star',
|
|
|
|
-- Literal question mark '?'
|
|
['?'] = '?',
|
|
['??'] = '??',
|
|
[':?'] = ':?',
|
|
['?<CR>'] = '?<CR>',
|
|
['g?'] = 'g?',
|
|
['g?g?'] = 'g?g?',
|
|
['g??'] = 'g??',
|
|
['-?'] = '-?',
|
|
['q?'] = 'q?',
|
|
['v_g?'] = 'v_g?',
|
|
['/\\?'] = '/\\\\?',
|
|
|
|
-- Backslash-escaping hell
|
|
['/\\%(\\)'] = '/\\\\%(\\\\)',
|
|
['/\\z(\\)'] = '/\\\\z(\\\\)',
|
|
['\\='] = '\\\\=',
|
|
['\\%$'] = '/\\\\%\\$',
|
|
|
|
-- Some expressions are literal but without the 'expr-' prefix. Note: not all 'expr-' subjects!
|
|
['expr-!=?'] = '!=?',
|
|
['expr-!~?'] = '!\\~?',
|
|
['expr-<=?'] = '<=?',
|
|
['expr-<?'] = '<?',
|
|
['expr-==?'] = '==?',
|
|
['expr-=~?'] = '=~?',
|
|
['expr->=?'] = '>=?',
|
|
['expr->?'] = '>?',
|
|
['expr-is?'] = 'is?',
|
|
['expr-isnot?'] = 'isnot?',
|
|
}
|
|
|
|
---Transform a help tag query into a search pattern for find_tags().
|
|
---
|
|
---This function converts user input from `:help {subject}` into a regex pattern that balances
|
|
---literal matching with wildcard support. Vim help tags can contain characters that have special
|
|
---meaning in regex (like *, ?, |), but we also want to support wildcard searches.
|
|
---
|
|
---Examples:
|
|
--- '*' --> 'star' (literal match for the * command help tag)
|
|
--- 'buffer*' --> 'buffer.*' (wildcard: find all buffer-related tags)
|
|
--- 'CTRL-W' --> stays as 'CTRL-W' (already in tag format)
|
|
--- '^A' --> 'CTRL-A' (caret notation converted to tag format)
|
|
---
|
|
---@param word string The help subject as entered by the user
|
|
---@return string pattern The escaped regex pattern to search for in tag files
|
|
function M.escape_subject(word)
|
|
local replacement = tag_exceptions[word]
|
|
if replacement then
|
|
return replacement
|
|
end
|
|
|
|
-- Add prefix '/\\' to patterns starting with a backslash
|
|
-- Examples: \S, \%^, \%(, \zs, \z1, \@<, \@=, \@<=, \_$, \_^
|
|
if word:match([[^\.$]]) or word:match('^\\[%%_z@]') then
|
|
word = [[/\]] .. word
|
|
word = word:gsub('[$.~]', [[\%0]])
|
|
word = word:gsub('|', 'bar')
|
|
else
|
|
-- Fix for bracket expressions and curly braces:
|
|
-- '\' --> '\\' (needs to come first)
|
|
-- '[' --> '\[' (escape the opening bracket)
|
|
-- ':[' --> ':\[' (escape the opening bracket)
|
|
-- '\{' --> '\\{' (for '\{' pattern matching)
|
|
-- '(' --> '' (parentheses around option tags should be ignored)
|
|
word = word:gsub([[\+]], [[\\]])
|
|
word = word:gsub([[^%[]], [[\[]])
|
|
word = word:gsub([[^:%[]], [[:\[]])
|
|
word = word:gsub([[^\{]], [[\\{]])
|
|
word = word:gsub([[^%(']], [[']])
|
|
|
|
word = word:gsub('|', 'bar')
|
|
word = word:gsub([["]], 'quote')
|
|
word = word:gsub('[$.~]', [[\%0]])
|
|
word = word:gsub('%*', '.*')
|
|
word = word:gsub('?', '.')
|
|
|
|
-- Handle control characters.
|
|
-- First convert raw control chars to the caret notation
|
|
-- E.g. 0x01 --> '^A' etc.
|
|
---@type string
|
|
word = word:gsub('([\1-\31])', function(ctrl_char)
|
|
-- '^\' needs an extra backslash
|
|
local repr = string.char(ctrl_char:byte() + 64):gsub([[\]], [[\\]])
|
|
return '^' .. repr
|
|
end)
|
|
|
|
-- Change caret notation to 'CTRL-', except '^_'
|
|
-- E.g. 'i^G^J' --> 'iCTRL-GCTRL-J'
|
|
word = word:gsub('%^([^_])', 'CTRL-%1')
|
|
-- Add underscores around 'CTRL-X' characters
|
|
-- E.g. 'iCTRL-GCTRL-J' --> 'i_CTRL-G_CTRL-J'
|
|
-- Only exception: 'CTRL-{character}'
|
|
word = word:gsub('([^_])CTRL%-', '%1_CTRL-')
|
|
word = word:gsub('(CTRL%-[^{])([^_\\])', '%1_%2')
|
|
|
|
-- Skip function arguments
|
|
-- E.g. 'abs({expr})' --> 'abs'
|
|
-- E.g. 'abs([arg])' --> 'abs'
|
|
word = word:gsub('%({.*', '')
|
|
word = word:gsub('%(%[.*', '')
|
|
|
|
-- Skip punctuation after second apostrophe/curly brace
|
|
-- E.g. ''option',' --> ''option''
|
|
-- E.g. '{address},' --> '{address}'
|
|
-- E.g. '`command`,' --> 'command' (backticks are removed too, but '``' stays '``')
|
|
word = word:gsub([[^'([^']*)'.*]], [['%1']])
|
|
word = word:gsub([[^{([^}]*)}.*]], '{%1}')
|
|
word = word:gsub([[.*`([^`]+)`.*]], '%1')
|
|
end
|
|
|
|
return word
|
|
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.
|
|
---
|
|
---For each help file found in 'runtimepath', the first line is extracted and added to the buffer
|
|
---as a reference (converting '*tag*' to '|tag|'). If a translated version of a help file exists
|
|
---in the same language as the current buffer (e.g. 'plugin.nlx' alongside 'plugin.txt'), the
|
|
---translated version is preferred over the '.txt' file.
|
|
function M.local_additions()
|
|
local buf = vim.api.nvim_get_current_buf()
|
|
local bufname = vim.fs.basename(vim.api.nvim_buf_get_name(buf))
|
|
|
|
-- "help.txt" or "help.??x" where ?? is a language code, see |help-translated|.
|
|
local lang = bufname:match('^help%.(%a%a)x$')
|
|
if bufname ~= 'help.txt' and not lang then
|
|
return
|
|
end
|
|
|
|
-- Find local help files
|
|
---@type table<string, string>
|
|
local plugins = {}
|
|
local pattern = lang and ('doc/*.{txt,%sx}'):format(lang) or 'doc/*.txt'
|
|
for _, docpath in ipairs(vim.api.nvim_get_runtime_file(pattern, true)) do
|
|
if not vim.fs.relpath(vim.env.VIMRUNTIME, docpath) then
|
|
-- '/path/to/doc/plugin.txt' --> 'plugin'
|
|
local plugname = vim.fs.basename(docpath):sub(1, -5)
|
|
-- prefer language-specific files over .txt
|
|
if not plugins[plugname] or vim.endswith(plugins[plugname], '.txt') then
|
|
plugins[plugname] = docpath
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Format plugin list lines
|
|
-- Default to 78 if 'textwidth' is not set (e.g. in sandbox)
|
|
local textwidth = math.max(vim.bo[buf].textwidth, 78)
|
|
local lines = {}
|
|
for _, path in vim.spairs(plugins) do
|
|
local fp = io.open(path, 'r')
|
|
if fp then
|
|
local tagline = fp:read('*l') or ''
|
|
fp:close()
|
|
---@type string, string
|
|
local plugname, desc = tagline:match('^%*([^*]+)%*%s*(.*)$')
|
|
if plugname and desc then
|
|
-- left-align taglink and right-align description by inserting spaces in between
|
|
local plug_width = vim.fn.strdisplaywidth(plugname)
|
|
local desc_width = vim.fn.strdisplaywidth(desc)
|
|
-- max(l, 1) forces at least one space for if the description is too long
|
|
local spaces = string.rep(' ', math.max(textwidth - desc_width - plug_width - 2, 1))
|
|
local fmt = string.format('|%s|%s%s', plugname, spaces, desc)
|
|
table.insert(lines, fmt)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add plugin list to local-additions section
|
|
for linenr, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
|
|
if line:find('*local-additions*', 1, true) then
|
|
vim._with({ buf = buf, bo = { modifiable = true, readonly = false } }, function()
|
|
vim.api.nvim_buf_set_lines(buf, linenr, linenr, true, lines)
|
|
end)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return M
|