runtime(helptoc): the helptoc package can be improved

Adds the following changes:
- New Maintainer: Pete Kenny
- New filetypes supported (asciidoc, html, tex, vim, xhtml)
- improved Markdown support
- Sanitised ToCs and popup presentation
- Configuration improvements and options
- Add helptoc.txt help file

closes: #17255

Signed-off-by: Peter Kenny <github.com@k1w1.cyou>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Peter Kenny
2025-05-05 20:15:39 +02:00
committed by Christian Brabandt
parent adfeb4ad95
commit ba0062b0c7
6 changed files with 803 additions and 109 deletions

View File

@ -1,24 +1,71 @@
vim9script noclear
# Config {{{1
# g:helptoc {{{2
# Create the g:helptoc dict (used to specify the shell_prompt and other
# options) when it does not exist
g:helptoc = exists('g:helptoc') ? g:helptoc : {}
var SHELL_PROMPT: string = ''
# Set the initial shell_prompt pattern matching a default bash prompt
g:helptoc.shell_prompt = get(g:helptoc, 'shell_prompt', '^\w\+@\w\+:\f\+\$\s')
# Track the prior prompt (used to reset b:toc if 'shell_prompt' changes)
g:helptoc.prior_shell_prompt = g:helptoc.shell_prompt
def UpdateUserSettings() #{{{2
var new_prompt: string = g:
->get('helptoc', {})
->get('shell_prompt', '^\w\+@\w\+:\f\+\$\s')
if new_prompt != SHELL_PROMPT
SHELL_PROMPT = new_prompt
if g:helptoc.shell_prompt != g:helptoc.prior_shell_prompt
# invalidate cache: user config has changed
unlet! b:toc
# reset the prior prompt to the new prompt
g:helptoc.prior_shell_prompt = g:helptoc.shell_prompt
endif
# helptoc popup presentation options{{{
# Enable users to choose whether, in toc and help text popups, to have:
# - border (default [], which is a border, so is usually wanted)
# - borderchars (default single box drawing; use [] for Vim's defaults)
# - borderhighlight (default [], but a user may prefer something else)
# - close (default 'none'; mouse users may prefer 'button')
# - drag (default true, which is a popup_menu's default)
# - scrollbar (default false; for long tocs/HELP_TEXT true may be better)
# For example, in a Vim9 script .vimrc, these settings will produce tocs
# with borders that have the same highlight group as the inactive
# statusline, a scrollbar, and an 'X' close button:
# g:helptoc.popup_borderchars = get(g:helptoc, 'popup_borderchars', [' '])
# g:helptoc.popup_borderhighlight = get(g:helptoc,
# 'popup_borderhighlight', ['StatusLineNC'])
# g:helptoc.popup_close = get(g:helptoc, 'popup_close', 'button')
# g:helptoc.popup_scrollbar = get(g:helptoc, 'popup_scrollbar', true)
# }}}
g:helptoc.popup_border = get(g:helptoc, 'popup_border', [])
g:helptoc.popup_borderchars = get(g:helptoc, 'popup_borderchars',
['─', '│', '─', '│', '┌', '┐', '┘', '└'])
g:helptoc.popup_borderhighlight = get(g:helptoc, 'popup_borderhighlight',
[])
g:helptoc.popup_drag = get(g:helptoc, 'popup_drag', true)
g:helptoc.popup_close = get(g:helptoc, 'popup_close', 'none')
g:helptoc.popup_scrollbar = get(g:helptoc, 'popup_scrollbar', false)
# For sanitized tocs, allow the user to specify the level indicator
g:helptoc.level_indicator = get(g:helptoc, 'level_indicator', '| ')
enddef
UpdateUserSettings()
# Init {{{1
# Syntax {{{1
# Used by sanitized tocs (asciidoc, html, markdown, tex, vim, and xhtml)
def SanitizedTocSyntax(): void
silent execute "syntax match helptocLevel _^\\(" ..
g:helptoc.level_indicator .. "\\)*_ contained"
silent execute "syntax region helptocText start=_^\\(" ..
g:helptoc.level_indicator .. "\\)*_ end=_$_ contains=helptocLevel"
highlight link helptocText Normal
highlight link helptocLevel NonText
enddef
# Init {{{1
# Constants {{{2
# HELP_TEXT {{{3
const HELP_TEXT: list<string> =<< trim END
normal commands in help window
──────────────────────────────
@ -73,38 +120,108 @@ const HELP_TEXT: list<string> =<< trim END
more context for each remaining entry by pressing J or K
END
# UPTOINC_H {{{3
const UPTOINC_H: string = '\v\c^%(%([<][^h][^>]*[>])|\s)*[<]h'
# MATCH_ENTRY {{{3
const MATCH_ENTRY: dict<dict<func: bool>> = {
help: {},
man: {
1: (line: string, _): bool => line =~ '^\S',
2: (line: string, _): bool => line =~ '^\%( \{3\}\)\=\S',
3: (line: string, _): bool => line =~ '^\s\+\(\%(+\|-\)\S\+,\s\+\)*\%(+\|-\)\S\+',
# For asciidoc, these patterns should match:
# https://docs.asciidoctor.org/asciidoc/latest/sections/titles-and-levels/
asciidoc: {
1: (l: string, _): bool => l =~ '\v^%(\=|#)\s',
2: (l: string, _): bool => l =~ '\v^%(\={2}|#{2})\s',
3: (l: string, _): bool => l =~ '\v^%(\={3}|#{3})\s',
4: (l: string, _): bool => l =~ '\v^%(\={4}|#{4})\s',
5: (l: string, _): bool => l =~ '\v^%(\={5}|#{5})\s',
6: (l: string, _): bool => l =~ '\v^%(\={6}|#{6})\s',
},
html: {
1: (l: string, _): bool => l =~ $"{UPTOINC_H}1",
2: (l: string, _): bool => l =~ $"{UPTOINC_H}2",
3: (l: string, _): bool => l =~ $"{UPTOINC_H}3",
4: (l: string, _): bool => l =~ $"{UPTOINC_H}4",
5: (l: string, _): bool => l =~ $"{UPTOINC_H}5",
6: (l: string, _): bool => l =~ $"{UPTOINC_H}6",
},
man: {
1: (l: string, _): bool => l =~ '^\S',
2: (l: string, _): bool => l =~ '\v^%( {3})=\S',
3: (l: string, _): bool => l =~ '\v^\s+%(%(\+|-)\S+,\s+)*(\+|-)\S+'
},
# For markdown, these patterns should match:
# https://spec.commonmark.org/0.31.2/#atx-headings and
# https://spec.commonmark.org/0.31.2/#setext-headings
markdown: {
1: (line: string, nextline: string): bool =>
(line =~ '^#[^#]' || nextline =~ '^=\+$') && line =~ '\w',
2: (line: string, nextline: string): bool =>
(line =~ '^##[^#]' || nextline =~ '^-\+$') && line =~ '\w',
3: (line: string, _): bool => line =~ '^###[^#]',
4: (line: string, _): bool => line =~ '^####[^#]',
5: (line: string, _): bool => line =~ '^#####[^#]',
6: (line: string, _): bool => line =~ '^######[^#]',
1: (l: string, nextline: string): bool =>
(l =~ '\v^ {0,3}#%(\s|$)' || nextline =~ '\v^ {0,3}\=+$') &&
l =~ '\S',
2: (l: string, nextline: string): bool =>
(l =~ '\v^ {0,3}##%(\s|$)' || nextline =~ '\v^ {0,3}-+$') &&
l =~ '\S',
3: (l: string, _): bool => l =~ '\v {0,3}#{3}%(\s|$)',
4: (l: string, _): bool => l =~ '\v {0,3}#{4}%(\s|$)',
5: (l: string, _): bool => l =~ '\v {0,3}#{5}%(\s|$)',
6: (l: string, _): bool => l =~ '\v {0,3}#{6}%(\s|$)',
},
terminal: {
1: (line: string, _): bool => line =~ SHELL_PROMPT,
1: (l: string, _): bool => l =~ g:helptoc.shell_prompt
},
# For LaTeX, this should meet
# https://mirrors.rit.edu/CTAN/info/latex2e-help-texinfo/latex2e.pdf
# including:
# para 6.3:
# \section{Heading}
# \section[Alternative ToC Heading]{Heading}
# para 25.1.2:
# \section*{Not for the TOC heading}
# \addcontentsline{toc}{section}{Alternative ToC Heading}
tex: {
1: (l: string, _): bool => l =~ '^[\\]\(\%(part\|chapter\)' ..
'\%([\u005B{]\)\|addcontentsline{toc}{\%(part\|chapter\)\)',
2: (l: string, _): bool => l =~ '^[\\]\%(section' ..
'\%([\u005B{]\)\|addcontentsline{toc}{section}\)',
3: (l: string, _): bool => l =~ '^[\\]\%(subsection' ..
'\%([\u005B{]\)\|addcontentsline{toc}{subsection}\)',
4: (l: string, _): bool => l =~ '^[\\]\%(subsubsection' ..
'\%([\u005B{]\)\|addcontentsline{toc}{subsubsection}\)',
},
vim: {
1: (l: string, _): bool => l =~ '\v\{{3}1',
2: (l: string, _): bool => l =~ '\v\{{3}2',
3: (l: string, _): bool => l =~ '\v\{{3}3',
4: (l: string, _): bool => l =~ '\v\{{3}4',
5: (l: string, _): bool => l =~ '\v\{{3}5',
6: (l: string, _): bool => l =~ '\v\{{3}6',
},
xhtml: {
1: (l: string, _): bool => l =~ $"{UPTOINC_H}1",
2: (l: string, _): bool => l =~ $"{UPTOINC_H}2",
3: (l: string, _): bool => l =~ $"{UPTOINC_H}3",
4: (l: string, _): bool => l =~ $"{UPTOINC_H}4",
5: (l: string, _): bool => l =~ $"{UPTOINC_H}5",
6: (l: string, _): bool => l =~ $"{UPTOINC_H}6",
}
}
# HELP_RULERS {{{3
const HELP_RULERS: dict<string> = {
'=': '^=\{40,}$',
'-': '^-\{40,}',
}
const HELP_RULER: string = HELP_RULERS->values()->join('\|')
# the regex is copied from the help syntax plugin
# HELP_TAG {{{3
# The regex is copied from the help syntax plugin
const HELP_TAG: string = '\*[#-)!+-~]\+\*\%(\s\|$\)\@='
# Adapted from `$VIMRUNTIME/syntax/help.vim`.{{{
@ -113,13 +230,15 @@ const HELP_TAG: string = '\*[#-)!+-~]\+\*\%(\s\|$\)\@='
#
# ^[-A-Z .][-A-Z0-9 .()_]*\ze\(\s\+\*\|$\)
#
# Allowing a space or a hyphen at the start can give false positives, and is
# Allowing a space or a hyphen at the start can give false positives, and is
# useless, so we don't allow them.
#}}}
# HELP_HEADLINE {{{3
const HELP_HEADLINE: string = '^\C[A-Z.][-A-Z0-9 .()_]*\%(\s\+\*+\@!\|$\)'
# ^--^
# To prevent some false positives under `:help feature-list`.
# Others {{{2
var lvls: dict<number>
def InitHelpLvls()
lvls = {
@ -133,7 +252,6 @@ def InitHelpLvls()
}
enddef
const AUGROUP: string = 'HelpToc'
var fuzzy_entries: list<dict<any>>
var help_winid: number
var print_entry: bool
@ -141,11 +259,11 @@ var selected_entry_match: number
# Interface {{{1
export def Open() #{{{2
var type: string = GetType()
if !MATCH_ENTRY->has_key(type)
g:helptoc.type = GetType()
if !MATCH_ENTRY->has_key(g:helptoc.type)
return
endif
if type == 'terminal' && win_gettype() == 'popup'
if g:helptoc.type == 'terminal' && win_gettype() == 'popup'
# trying to deal with a popup menu on top of a popup terminal seems
# too tricky for now
echomsg 'does not work in a popup window; only in a regular window'
@ -158,7 +276,7 @@ export def Open() #{{{2
if exists('b:toc') && &filetype != 'man'
if b:toc.changedtick != b:changedtick
# in a terminal buffer, `b:changedtick` does not change
|| type == 'terminal' && line('$') > b:toc.linecount
|| g:helptoc.type == 'terminal' && line('$') > b:toc.linecount
unlet! b:toc
endif
endif
@ -187,66 +305,257 @@ export def Open() #{{{2
line: winpos[0],
col: winpos[1] + width - 1,
pos: 'topright',
scrollbar: false,
highlight: type == 'terminal' ? 'Terminal' : 'Normal',
border: [],
borderchars: ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
highlight: g:helptoc.type == 'terminal' ? 'Terminal' : 'Normal',
minheight: height,
maxheight: height,
minwidth: b:toc.width,
maxwidth: b:toc.width,
filter: Filter,
callback: Callback,
border: g:helptoc.popup_border,
borderchars: g:helptoc.popup_borderchars,
borderhighlight: g:helptoc.popup_borderhighlight,
close: g:helptoc.popup_close,
drag: g:helptoc.popup_drag,
scrollbar: g:helptoc.popup_scrollbar,
})
Win_execute(winid, [$'ownsyntax {&filetype}', '&l:conceallevel = 3'])
# Specify filetypes using sanitized toc syntax{{{
# Those filetypes have a normalized toc structure. The top level is
# unprefixed and levels 2 to 6 are prefixed, by default, with a vertical
# line and space for each level below 1:
# Level 1
# | Level 2
# ...
# | | | | | Level 6 }}}
final SanitizedTocSyntaxTypes: list<string> =
['asciidoc', 'html', 'markdown', 'tex', 'vim', 'xhtml']
if index(SanitizedTocSyntaxTypes, g:helptoc.type) != -1
# Specified types' toc popups use a common syntax
Win_execute(winid, 'SanitizedTocSyntax()')
else
# Other types' toc popups use the same syntax as the buffer itself
Win_execute(winid, [$'ownsyntax {&filetype}', '&l:conceallevel = 3'])
endif
# In a help file, we might reduce some noisy tags to a trailing asterisk.
# Hide those.
if type == 'help'
if g:helptoc.type == 'help'
matchadd('Conceal', '\*$', 0, -1, {window: winid})
endif
SelectNearestEntryFromCursor(winid)
# can't set the title before jumping to the relevant line, otherwise the
# Can't set the title before jumping to the relevant line, otherwise the
# indicator in the title might be wrong
SetTitle(winid)
enddef
#}}}1
# Core {{{1
def SetToc() #{{{2
var toc: dict<any> = {entries: []}
var type: string = GetType()
# Lambdas:
# CHARACTER_REFERENCES_TO_CHARACTERS {{{3
# These are used for AsciiDoc, Markdown, and [X]HTML, all of which allow
# for decimal, hexadecimal, and XML predefined entities.
# Decimal character references: e.g., &#167; to §
# Hexadecimal character references: e.g., &#xA7; to §
# XML predefined entities to chars: e.g., &lt; to <
# All HTML5 named character references could be handled, though is that
# warranted for the few that may appear in a toc entry, especially when
# they are often mnemonic? Future: A common Vim dict/enum could be useful?
const CHARACTER_REFERENCES_TO_CHARACTERS = (text: string): string =>
text->substitute('\v\&#0*([1-9]\d{0,6});',
'\=nr2char(str2nr(submatch(1), 10), 1)', 'g')
->substitute('\c\v\&#x0*([1-9a-f][[:xdigit:]]{1,5});',
'\=nr2char(str2nr(submatch(1), 16), 1)', 'g')
->substitute('\C&amp;', '\="\u0026"', 'g')
->substitute('\C&apos;', "\u0027", 'g')
->substitute('\C&gt;', "\u003E", 'g')
->substitute('\C&lt;', "\u003C", 'g')
->substitute('\C&quot;', "\u0022", 'g')
# SANITIZE_ASCIIDOC {{{3
# 1 - Substitute the = or # heading markup with the level indicator
# 2 - Substitute XML predefined, dec, and hex char refs in the entry
# AsciiDoc recommends only using named char refs defined in XML:
# https://docs.asciidoctor.org/asciidoc/latest/subs/replacements/
const SANITIZE_ASCIIDOC = (text: string): string =>
text->substitute('\v^(\={1,6}|#{1,6})\s+',
'\=repeat(g:helptoc.level_indicator, len(submatch(1)) - 1)', '')
->CHARACTER_REFERENCES_TO_CHARACTERS()
# SANITIZE_HTML {{{3
# 1 - Remove any leading spaces or tabs
# 2 - Remove any <!--HTML comments-->
# 3 - Remove any <?processing_instructions?>
# 4 - Remove any leading tags (and any blanks) other than <h1 to <h6
# 5 - Remove any persisting leading blanks
# 6 - Handle empty XHTML headings, e.g., <h6 />
# 7 - Remove trailing content following the </h[1-6]>
# 8 - Remove the <h1
# 9 - Substitute the h2 to h6 heading tags with level indicator/level
# 10 - Remove intra-heading tags like <em>, </em>, <strong>, etc.
# 11 - Substitute XML predefined, dec and hex character references
const SANITIZE_HTML = (text: string): string =>
text->substitute('^\s*', '', '')
->substitute('[<]!--.\{-}--[>]', '', 'g')
->substitute('[<]?[^?]\+?[>]', '', 'g')
->substitute('\v%([<][^Hh][^1-6]?[^>][>])*\s*', '', '')
->substitute('^\s\+', '', '')
->substitute('\v[<][Hh]([1-6])\s*[/][>].*',
'\=repeat(g:helptoc.level_indicator, ' ..
'str2nr(submatch(1)) - 1) ' ..
'.. "[Empty heading " .. submatch(1) .. "]"', '')
->substitute('[<][/][Hh][1-6][>].*$', '', '')
->substitute('[<][Hh]1[^>]*[>]', '', '')
->substitute('\v[<][Hh]([2-6])[^>]*[>]',
'\=repeat(g:helptoc.level_indicator, ' ..
'str2nr(submatch(1)) - 1)', '')
->substitute('[<][/]\?[[:alpha:]][^>]*[>]', '', 'g')
->CHARACTER_REFERENCES_TO_CHARACTERS()
# SANITIZE_MARKDOWN #{{{3
# 1 - Hyperlink incl image, e.g. [![Vim The editor](xxx)](\uri), to Vim...
# 2 - Hyperlink [text](/uri) to text
# 3 - Substitute the # ATX heading markup with the level indicator/level
# The omitted markup reflects CommonMark Spec:
# https://spec.commonmark.org/0.31.2/#atx-headings
# 4 - Substitute decimal, hexadecimal, and XML predefined char refs
const SANITIZE_MARKDOWN = (text: string): string =>
text->substitute('\v[\u005B]![\u005B]([^\u005D]+)[\u005D]'
.. '[(][^)]+[)][\u005D][(][^)]+[)]', '\1', '')
->substitute('\v[\u005B]([^\u005D]+)[\u005D][(][^)]+[)]',
'\1', '')
->substitute('\v^ {0,3}(#{1,6})\s*',
'\=repeat(g:helptoc.level_indicator, len(submatch(1)) - 1)',
'')
->CHARACTER_REFERENCES_TO_CHARACTERS()
# SANITIZE_TERMINAL {{{3
# Omit the prompt, which may be very long and otherwise just adds clutter
const SANITIZE_TERMINAL = (text: string): string =>
text->substitute('^' .. g:helptoc.shell_prompt, '', '')
# SANITIZE_TEX #{{{3
# 1 - Use any [toc-title] overrides to move its content into the
# {heading} instead of the (non-ToC) heading's text
# 2 - Replace \part{ or \addcontentsline{toc}{part} with '[PART] '
# 3 - Omit \chapter{ or \addcontentsline{toc}{chapter}
# 4 - Omit \section{ or \addcontentsline{toc}{section}
# 5 - Omit \subsection{ or \addcontentsline{toc}{subsection}
# 6 - Omit \subsubsection{ or \addcontentsline{toc}{subsubsection}
# 7 - Omit the trailing }
# 8 - Unescape common escaped characters &%$_#{}~^\
const SANITIZE_TEX = (text: string): string =>
text->substitute('\v^[\\](part|chapter|%(sub){0,2}section)' ..
'[\u005B]([^\u005D]+).*', '\\\1{\2}', '')
->substitute('^[\\]\(part\|addcontentsline{toc}{part}\){',
'[PART] ', '')
->substitute('^[\\]\(chapter\|addcontentsline{toc}{chapter}\){',
'', '')
->substitute('^[\\]\(section\|addcontentsline{toc}{section}\){',
'\=g:helptoc.level_indicator', '')
->substitute('^[\\]\(subsection\|' ..
'addcontentsline{toc}{subsection}\){',
'\=repeat(g:helptoc.level_indicator, 2)', '')
->substitute('^[\\]\(subsubsection\|' ..
'addcontentsline{toc}{subsubsection}\){',
'\=repeat(g:helptoc.level_indicator, 3)', '')
->substitute('}[^}]*$', '', '')
->substitute('\\\([&%$_#{}~\\^]\)', '\1', 'g')
# SANITIZE_VIM {{{3
# #1 - Omit leading Vim9 script # or vimscript " markers and blanks
# #2 - Omit numbered 3x { markers
const SANITIZE_VIM = (text: string): string =>
text->substitute('\v^[#[:blank:]"]*(.+)\ze[{]{3}([1-6])',
'\=submatch(2) == "1" ? submatch(1) : ' ..
'repeat(g:helptoc.level_indicator, str2nr(submatch(2)) - 1)' ..
' .. submatch(1)', 'g')
->substitute('[#[:blank:]"]*{\{3}[1-6]', '', '')
#}}}3
final toc: dict<any> = {entries: []}
toc.changedtick = b:changedtick
if !toc->has_key('width')
toc.width = 0
endif
# We cache the toc in `b:toc` to get better performance.{{{
#
# Without caching, when we press `H`, `L`, `H`, `L`, ... quickly for a few
# Without caching, when we press `H`, `L`, `H`, `L`, ... quickly for a few
# seconds, there is some lag if we then try to move with `j` and `k`.
# This can only be perceived in big man pages like with `:Man ffmpeg-all`.
#}}}
b:toc = toc
if type == 'help'
if g:helptoc.type == 'help'
SetTocHelp()
return
endif
if type == 'terminal'
if g:helptoc.type == 'terminal'
b:toc.linecount = line('$')
endif
var curline: string = getline(1)
var nextline: string
var lvl_and_test: list<list<any>> = MATCH_ENTRY
->get(type, {})
->get(g:helptoc.type, {})
->items()
->sort((l: list<any>, ll: list<any>): number => l[0]->str2nr() - ll[0]->str2nr())
->sort((l: list<any>, ll: list<any>): number =>
l[0]->str2nr() - ll[0]->str2nr())
var skip_next: bool = false
# Non-help headings processing
for lnum: number in range(1, line('$'))
if skip_next
skip_next = false
curline = nextline
continue
endif
nextline = getline(lnum + 1)
# Special handling for markdown filetype using setext headings
if g:helptoc.type == 'markdown'
# Check for setext formatted headings (= or - underlined)
if nextline =~ '^\s\{0,3}=\+$' && curline =~ '\S'
# Level 1 heading (one or more =, up to three spaces preceding)
b:toc.entries->add({
lnum: lnum,
lvl: 1,
text: SANITIZE_MARKDOWN('# ' .. trim(curline)),
})
skip_next = true
curline = nextline
continue
elseif nextline =~ '^\s\{0,3}-\+$' && curline =~ '\S'
# Level 2 heading (one or more -, up to three spaces preceding)
b:toc.entries->add({
lnum: lnum,
lvl: 2,
text: SANITIZE_MARKDOWN('## ' .. trim(curline)),
})
skip_next = true
curline = nextline
continue
endif
endif
# Regular processing for markdown ATX-style headings + other filetypes
for [lvl: string, IsEntry: func: bool] in lvl_and_test
if IsEntry(curline, nextline)
if g:helptoc.type == 'asciidoc'
curline = curline->SANITIZE_ASCIIDOC()
elseif g:helptoc.type == 'html' || g:helptoc.type == 'xhtml'
curline = curline->SANITIZE_HTML()
elseif g:helptoc.type == 'markdown'
curline = curline->SANITIZE_MARKDOWN()
elseif g:helptoc.type == 'terminal'
curline = curline->SANITIZE_TERMINAL()
elseif g:helptoc.type == 'tex'
curline = curline->SANITIZE_TEX()
elseif g:helptoc.type == 'vim'
curline = curline->SANITIZE_VIM()
endif
b:toc.entries->add({
lnum: lnum,
lvl: lvl->str2nr(),
@ -281,9 +590,9 @@ def SetTocHelp() #{{{2
if main_ruler != '' && curline =~ main_ruler
last_numbered_entry = 0
# The information gathered in `lvls` might not be applicable to all
# the main sections of a help file. Let's reset it whenever we find
# a ruler.
# The information gathered in `lvls` might not be applicable to
# all the main sections of a help file. Let's reset it whenever
# we find a ruler.
InitHelpLvls()
endif
@ -304,7 +613,7 @@ def SetTocHelp() #{{{2
# 1.
if prevline =~ '^\d\+\.\s'
# let's assume that the start of a main entry is always followed by an
# Let's assume that the start of a main entry is always followed by an
# empty line, or a line starting with a tag
&& (curline =~ '^>\=\s*$' || curline =~ $'^\s*{HELP_TAG}')
# ignore a numbered line in a list
@ -337,7 +646,8 @@ def SetTocHelp() #{{{2
if curline =~ HELP_HEADLINE
&& curline !~ '^CTRL-'
&& prevline->IsSpecialHelpLine()
&& (nextline->IsSpecialHelpLine() || nextline =~ '^\s*(\|^\t\|^N[oO][tT][eE]:')
&& (nextline ->IsSpecialHelpLine()
|| nextline =~ '^\s*(\|^\t\|^N[oO][tT][eE]:')
AddEntryInTocHelp('HEADLINE', lnum, curline)
endif
@ -411,7 +721,8 @@ def SetTocHelp() #{{{2
->min()
for entry: dict<any> in b:toc.entries
entry.text = entry.text
->substitute('^\s*', () => repeat(' ', (entry.lvl - min_lvl) * 3), '')
->substitute('^\s*', () =>
repeat(' ', (entry.lvl - min_lvl) * 3), '')
endfor
enddef
@ -455,29 +766,30 @@ def AddEntryInTocHelp(type: string, lnum: number, line: string) #{{{2
# Ignore noisy tags.{{{
#
# 14. Linking groups *:hi-link* *:highlight-link* *E412* *E413*
# ^----------------------------------------^
# ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\zs\*.*
# 14. Linking groups *:hi-link* *:highlight-link* *E412* *E413*
# ^----------------------------------------^
# ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\zs\*.*
# ---
#
# We don't use conceal because then, `matchfuzzypos()` could match concealed
# characters, which would be confusing.
# We don't use conceal because then, `matchfuzzypos()` could match
# concealed characters, which would be confusing.
#}}}
# MAKING YOUR OWN SYNTAX FILES *mysyntaxfile*
# ^------------^
# ^\s*[A-Z].\{-}\*\zs.*
# MAKING YOUR OWN SYNTAX FILES *mysyntaxfile*
# ^------------^
# ^\s*[A-Z].\{-}\*\zs.*
#
var after_HEADLINE: string = '^\s*[A-Z].\{-}\*\zs.*'
# 14. Linking groups *:hi-link* *:highlight-link* *E412* *E413*
# ^----------------------------------------^
# ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*
# 14. Linking groups *:hi-link* *:highlight-link* *E412* *E413*
# ^----------------------------------------^
# ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*
var after_numbered: string = '^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*'
# 01.3 Using the Vim tutor *tutor* *vimtutor*
# ^----------------^
# 01.3 Using the Vim tutor *tutor* *vimtutor*
# ^----------------^
var after_numbered_tutor: string = '^\*\d\+\.\%(\d\+\.\=\)*.\{-}\t\*\zs.*'
var noisy_tags: string = $'{after_HEADLINE}\|{after_numbered}\|{after_numbered_tutor}'
var noisy_tags: string =
$'{after_HEADLINE}\|{after_numbered}\|{after_numbered_tutor}'
text = text->substitute(noisy_tags, '', '')
# We don't remove the trailing asterisk, because the help syntax plugin
# We don't remove the trailing asterisk, because the help syntax plugin
# might need it to highlight some headlines.
b:toc.entries->add({
@ -498,7 +810,7 @@ enddef
def Popup_settext(winid: number, entries: list<dict<any>>) #{{{2
var text: list<any>
# When we fuzzy search the toc, the dictionaries in `entries` contain a
# When we fuzzy search the toc, the dictionaries in `entries` contain a
# `props` key, to highlight each matched character individually.
# We don't want to process those dictionaries further.
# The processing should already have been done by the caller.
@ -544,7 +856,8 @@ def SelectNearestEntryFromCursor(winid: number) #{{{2
var lnum: number = line('.')
var firstline: number = b:toc.entries
->copy()
->filter((_, line: dict<any>): bool => line.lvl <= b:toc.curlvl && line.lnum <= lnum)
->filter((_, line: dict<any>): bool =>
line.lvl <= b:toc.curlvl && line.lnum <= lnum)
->len()
if firstline == 0
return
@ -599,8 +912,8 @@ def Filter(winid: number, key: string): bool #{{{2
if key == 'J' || key == 'K'
var lnum: number = GetBufLnum(winid)
execute $'normal! 0{lnum}zt'
# install a match in the regular buffer to highlight the position of
# the entry in the latter
# Install a match in the regular buffer to highlight the position
# of the entry in the latter
MatchDelete()
selected_entry_match = matchaddpos('IncSearch', [lnum], 0, -1)
endif
@ -665,41 +978,44 @@ def Filter(winid: number, key: string): bool #{{{2
return true
elseif key == '/'
# This is probably what the user expect if they've started a first fuzzy
# search, press Escape, then start a new one.
# This is probably what the user expects if they've started a first
# fuzzy search, press Escape, then start a new one.
DisplayNonFuzzyToc(winid)
[{
group: AUGROUP,
group: 'HelpToc',
event: 'CmdlineChanged',
pattern: '@',
cmd: $'FuzzySearch({winid})',
replace: true,
}, {
group: AUGROUP,
group: 'HelpToc',
event: 'CmdlineLeave',
pattern: '@',
cmd: 'TearDown()',
replace: true,
}]->autocmd_add()
# Need to evaluate `winid` right now with an `eval`'ed and `execute()`'ed heredoc because:{{{
# Need to evaluate `winid` right now{{{
# with an `eval`'ed and `execute()`'ed heredoc because:
#
# - the mappings can only access the script-local namespace
# - `winid` is in the function namespace; not in the script-local one
# - the mappings can only access the script-local namespace
# - `winid` is in the function namespace; not in the script-local one
#}}}
var input_mappings: list<string> =<< trim eval END
cnoremap <buffer><nowait> <Down> <ScriptCmd>Filter({winid}, 'j')<CR>
cnoremap <buffer><nowait> <Up> <ScriptCmd>Filter({winid}, 'k')<CR>
cnoremap <buffer><nowait> <C-N> <ScriptCmd>Filter({winid}, 'j')<CR>
cnoremap <buffer><nowait> <C-P> <ScriptCmd>Filter({winid}, 'k')<CR>
cnoremap <buffer><nowait> <Down> <ScriptCmd>Filter({winid}, 'j')<CR>
cnoremap <buffer><nowait> <Up> <ScriptCmd>Filter({winid}, 'k')<CR>
cnoremap <buffer><nowait> <C-N> <ScriptCmd>Filter({winid}, 'j')<CR>
cnoremap <buffer><nowait> <C-P> <ScriptCmd>Filter({winid}, 'k')<CR>
END
input_mappings->execute()
var look_for: string
try
popup_setoptions(winid, {mapping: true})
look_for = input('look for: ', '', $'custom,{Complete->string()}') | redraw | echo ''
look_for = input('look for: ', '', $'custom,{Complete->string()}')
| redraw
| echo ''
catch /Vim:Interrupt/
TearDown()
finally
@ -718,9 +1034,9 @@ def FuzzySearch(winid: number) #{{{2
return
endif
# We match against *all* entries; not just the currently visible ones.
# Rationale: If we use a (fuzzy) search, we're probably lost. We don't know
# where the info is.
# We match against *all* entries; not just the currently visible ones.
# Rationale: If we use a (fuzzy) search, we're probably lost. We don't
# know where the info is.
var matches: list<list<any>> = b:toc.entries
->copy()
->matchfuzzypos(look_for, {key: 'text'})
@ -764,7 +1080,7 @@ def PrintEntry(winid: number) #{{{2
enddef
def CollapseOrExpand(winid: number, key: string) #{{{2
# Must be saved before we reset the popup contents, so we can
# Must be saved before we reset the popup contents, so we can
# automatically select the least unexpected entry in the updated popup.
var buf_lnum: number = GetBufLnum(winid)
@ -798,11 +1114,11 @@ def CollapseOrExpand(winid: number, key: string) #{{{2
endwhile
endif
# update the popup contents
# Update the popup contents
var toc_entries: list<dict<any>> = GetTocEntries()
Popup_settext(winid, toc_entries)
# Try to select the same entry; if it's no longer visible, select its
# Try to select the same entry; if it's no longer visible, select its
# direct parent.
var toc_lnum: number = 0
for entry: dict<any> in toc_entries
@ -834,6 +1150,8 @@ def Callback(winid: number, choice: number) #{{{2
if choice == -1
fuzzy_entries = null_list
return
elseif choice == -2 # Button X is clicked (when close: 'button')
return
endif
var lnum: number = GetTocEntries()
@ -851,36 +1169,38 @@ def Callback(winid: number, choice: number) #{{{2
enddef
def ToggleHelp(menu_winid: number) #{{{2
# Show/hide HELP_TEXT in a second popup when '?' is typed{{{
# (when a helptoc popup is open). A scrollbar on this popup makes sense
# because it is very long and, even if it's not used for scrolling, works
# well as an indicator of how far through the HELP_TEXT popup you are. }}}
if help_winid == 0
var height: number = [HELP_TEXT->len(), winheight(0) * 2 / 3]->min()
var longest_line: number = HELP_TEXT
->copy()
->map((_, line: string) => line->strcharlen())
->max()
var width: number = [longest_line, winwidth(0) * 2 / 3]->min()
var pos: dict<number> = popup_getpos(menu_winid)
var [line: number, col: number] = [pos.line, pos.col]
--col
var width: number = [longest_line, winwidth(0) - 4]->min()
var zindex: number = popup_getoptions(menu_winid).zindex
++zindex
help_winid = HELP_TEXT->popup_create({
line: line,
col: col,
pos: 'topright',
pos: 'center',
minheight: height,
maxheight: height,
minwidth: width,
maxwidth: width,
border: [],
borderchars: ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
highlight: &buftype == 'terminal' ? 'Terminal' : 'Normal',
scrollbar: false,
zindex: zindex,
border: g:helptoc.popup_border,
borderchars: g:helptoc.popup_borderchars,
borderhighlight: g:helptoc.popup_borderhighlight,
close: g:helptoc.popup_close,
scrollbar: true,
})
setwinvar(help_winid, '&cursorline', true)
setwinvar(help_winid, '&linebreak', true)
matchadd('Special', '^<\S\+\|^\S\{,2} \@=', 0, -1, {window: help_winid})
matchadd('Special', '^<\S\+\|^\S\{,2} \@=', 0, -1,
{window: help_winid})
matchadd('Number', '\d\+', 0, -1, {window: help_winid})
for lnum: number in HELP_TEXT->len()->range()
if HELP_TEXT[lnum] =~ '^─\+$'
@ -898,23 +1218,22 @@ def ToggleHelp(menu_winid: number) #{{{2
enddef
def Win_execute(winid: number, cmd: any) #{{{2
# wrapper around `win_execute()` to enforce a redraw, which might be necessary
# wrapper around `win_execute()` to enforce a redraw, which might be necessary
# whenever we change the cursor position
win_execute(winid, cmd)
redraw
enddef
def TearDown() #{{{2
autocmd_delete([{group: AUGROUP}])
autocmd_delete([{group: 'HelpToc'}])
cunmap <buffer> <Down>
cunmap <buffer> <Up>
cunmap <buffer> <C-N>
cunmap <buffer> <C-P>
enddef
#}}}1
# Util {{{1
def GetType(): string #{{{2
return &buftype == 'terminal' ? 'terminal' : &filetype
return &buftype == 'terminal' ? 'terminal' : &filetype
enddef
def GetTocEntries(): list<dict<any>> #{{{2
@ -944,10 +1263,12 @@ enddef
def Complete(..._): string #{{{2
return b:toc.entries
->copy()
->map((_, entry: dict<any>) => entry.text->trim(' ~')->substitute('*', '', 'g'))
->map((_, entry: dict<any>) =>
entry.text->trim(' ~')->substitute('*', '', 'g'))
->filter((_, text: string): bool => text =~ '^[-a-zA-Z0-9_() ]\+$')
->sort()
->uniq()
->join("\n")
enddef
enddef #}}}2
#}}}1
# vim:et:ft=vim:fdm=marker: