mirror of
https://github.com/neovim/neovim.git
synced 2026-02-18 10:21:26 +10:00
Problem:
Escaping logic for {subject} in ex cmd `:help {subject}` is done in a
messy 200+ lines C function which is hard to maintain and improve.
Solution:
Rewrite in Lua. Use `string.gsub()` instead of looping over characters
to improve clarity and add many more tests to be able to confidently
improve current code later on.
132 lines
4.2 KiB
Lua
132 lines
4.2 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
|
|
|
|
return M
|