mirror of
https://github.com/vim/vim.git
synced 2025-07-25 10:54:51 -04:00
runtime(doc): include a TOC Vim9 plugin
closes: #10446 See :h help-TOC Signed-off-by: lagygoill <lacygoill@lacygoill.me> Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
parent
29ce419076
commit
b3ec5643cd
@ -1,4 +1,4 @@
|
||||
*helphelp.txt* For Vim version 9.1. Last change: 2024 Apr 10
|
||||
*helphelp.txt* For Vim version 9.1. Last change: 2024 Nov 02
|
||||
|
||||
|
||||
VIM REFERENCE MANUAL by Bram Moolenaar
|
||||
@ -246,6 +246,61 @@ command: >
|
||||
To rebuild the help tags in the runtime directory
|
||||
(requires write permission there): >
|
||||
:helptags $VIMRUNTIME/doc
|
||||
<
|
||||
*help-TOC* *help-toc-install*
|
||||
|
||||
If you want to access an interactive table of contents, from any position in
|
||||
the file, you can use the helptoc plugin. Load the plugin with: >
|
||||
|
||||
packadd helptoc
|
||||
|
||||
Then you can use the `:HelpToc` command to open a popup menu.
|
||||
The latter supports the following normal commands: >
|
||||
|
||||
key | effect
|
||||
----+---------------------------------------------------------
|
||||
j | select next entry
|
||||
k | select previous entry
|
||||
J | same as j, and jump to corresponding line in main buffer
|
||||
K | same as k, and jump to corresponding line in main buffer
|
||||
c | select nearest entry from cursor position in main buffer
|
||||
g | select first entry
|
||||
G | select last entry
|
||||
H | collapse one level
|
||||
L | expand one level
|
||||
p | print current entry on command-line
|
||||
|
||||
P | same as p but automatically, whenever selection changes
|
||||
| press multiple times to toggle feature on/off
|
||||
|
||||
q | quit menu
|
||||
z | redraw menu with current entry at center
|
||||
+ | increase width of popup menu
|
||||
- | decrease width of popup menu
|
||||
? | show/hide a help window
|
||||
|
||||
<C-D> | scroll down half a page
|
||||
<C-U> | scroll up half a page
|
||||
<PageUp> | scroll down a whole page
|
||||
<PageDown> | scroll up a whole page
|
||||
<Home> | select first entry
|
||||
<End> | select last entry
|
||||
|
||||
The plugin can also provide a table of contents in man pages, markdown files,
|
||||
and terminal buffers. In the latter, the entries will be the past executed
|
||||
shell commands. To find those, the following regex is used: >
|
||||
|
||||
^\w\+@\w\+:\f\+\$\s
|
||||
|
||||
This is meant to match a default bash prompt. If it doesn't match your prompt,
|
||||
you can change the regex with the `shell_prompt` key from the `g:helptoc`
|
||||
dictionary variable: >
|
||||
|
||||
let g:helptoc = {'shell_prompt': 'regex matching your shell prompt'}
|
||||
|
||||
Tip: After inserting a pattern to look for with the `/` command, if you press
|
||||
<Esc> instead of <CR>, you can then get more context for each remaining entry
|
||||
by pressing `J` or `K`.
|
||||
|
||||
==============================================================================
|
||||
2. Translated help files *help-translated*
|
||||
|
@ -8062,11 +8062,13 @@ hasmapto() builtin.txt /*hasmapto()*
|
||||
hebrew hebrew.txt /*hebrew*
|
||||
hebrew.txt hebrew.txt /*hebrew.txt*
|
||||
help helphelp.txt /*help*
|
||||
help-TOC helphelp.txt /*help-TOC*
|
||||
help-buffer-options helphelp.txt /*help-buffer-options*
|
||||
help-context help.txt /*help-context*
|
||||
help-curwin tips.txt /*help-curwin*
|
||||
help-summary usr_02.txt /*help-summary*
|
||||
help-tags tags 1
|
||||
help-toc-install helphelp.txt /*help-toc-install*
|
||||
help-translated helphelp.txt /*help-translated*
|
||||
help-writing helphelp.txt /*help-writing*
|
||||
help-xterm-window helphelp.txt /*help-xterm-window*
|
||||
|
@ -1,4 +1,4 @@
|
||||
*version9.txt* For Vim version 9.1. Last change: 2024 Oct 27
|
||||
*version9.txt* For Vim version 9.1. Last change: 2024 Nov 02
|
||||
|
||||
|
||||
VIM REFERENCE MANUAL by Bram Moolenaar
|
||||
@ -41602,6 +41602,7 @@ Changed~
|
||||
selection in the quickfix list with the "u" action.
|
||||
- the putty terminal is detected using an |TermResponse| autocommand in
|
||||
|defaults.vim| and Vim switches to a dark background
|
||||
- the |help-TOC| package is included to ease navigating the documentation.
|
||||
|
||||
*added-9.2*
|
||||
Added ~
|
||||
|
940
runtime/pack/dist/opt/helptoc/autoload/helptoc.vim
vendored
Normal file
940
runtime/pack/dist/opt/helptoc/autoload/helptoc.vim
vendored
Normal file
@ -0,0 +1,940 @@
|
||||
vim9script noclear
|
||||
|
||||
# Config {{{1
|
||||
|
||||
const SHELL_PROMPT: string = g:
|
||||
->get('helptoc', {})
|
||||
->get('shell_prompt', '^\w\+@\w\+:\f\+\$\s')
|
||||
|
||||
# Init {{{1
|
||||
|
||||
const HELP_TEXT: list<string> =<< trim END
|
||||
normal commands in help window
|
||||
──────────────────────────────
|
||||
? hide this help window
|
||||
<C-J> scroll down one line
|
||||
<C-K> scroll up one line
|
||||
|
||||
normal commands in TOC menu
|
||||
───────────────────────────
|
||||
j select next entry
|
||||
k select previous entry
|
||||
J same as j, and jump to corresponding line in main buffer
|
||||
K same as k, and jump to corresponding line in main buffer
|
||||
c select nearest entry from cursor position in main buffer
|
||||
g select first entry
|
||||
G select last entry
|
||||
H collapse one level
|
||||
L expand one level
|
||||
p print selected entry on command-line
|
||||
|
||||
P same as p but automatically, whenever selection changes
|
||||
press multiple times to toggle feature on/off
|
||||
|
||||
q quit menu
|
||||
z redraw menu with selected entry at center
|
||||
+ increase width of popup menu
|
||||
- decrease width of popup menu
|
||||
/ look for given text with fuzzy algorithm
|
||||
? show help window
|
||||
|
||||
<C-D> scroll down half a page
|
||||
<C-U> scroll up half a page
|
||||
<PageUp> scroll down a whole page
|
||||
<PageDown> scroll up a whole page
|
||||
<Home> select first entry
|
||||
<End> select last entry
|
||||
|
||||
title meaning
|
||||
─────────────
|
||||
example: 12/34 (5/6)
|
||||
broken down:
|
||||
|
||||
12 index of selected entry
|
||||
34 index of last entry
|
||||
5 index of deepest level currently visible
|
||||
6 index of maximum possible level
|
||||
|
||||
tip
|
||||
───
|
||||
after inserting a pattern to look for with the / command,
|
||||
if you press <Esc> instead of <CR>, you can then get
|
||||
more context for each remaining entry by pressing J or K
|
||||
END
|
||||
|
||||
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\+',
|
||||
},
|
||||
|
||||
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 =~ '^######[^#]',
|
||||
},
|
||||
|
||||
terminal: {
|
||||
1: (line: string, _): bool => line =~ SHELL_PROMPT,
|
||||
}
|
||||
}
|
||||
|
||||
const HELP_RULERS: dict<string> = {
|
||||
'=': '^=\{40,}$',
|
||||
'-': '^-\{40,}',
|
||||
}
|
||||
const HELP_RULER: string = HELP_RULERS->values()->join('\|')
|
||||
|
||||
# the regex is copied from the help syntax plugin
|
||||
const HELP_TAG: string = '\*[#-)!+-~]\+\*\%(\s\|$\)\@='
|
||||
|
||||
# Adapted from `$VIMRUNTIME/syntax/help.vim`.{{{
|
||||
#
|
||||
# The original regex is:
|
||||
#
|
||||
# ^[-A-Z .][-A-Z0-9 .()_]*\ze\(\s\+\*\|$\)
|
||||
#
|
||||
# Allowing a space or a hyphen at the start can give false positives, and is
|
||||
# useless, so we don't allow them.
|
||||
#}}}
|
||||
const HELP_HEADLINE: string = '^\C[A-Z.][-A-Z0-9 .()_]*\%(\s\+\*+\@!\|$\)'
|
||||
# ^--^
|
||||
# To prevent some false positives under `:help feature-list`.
|
||||
|
||||
var lvls: dict<number>
|
||||
def InitHelpLvls()
|
||||
lvls = {
|
||||
'*01.1*': 0,
|
||||
'1.': 0,
|
||||
'1.2': 0,
|
||||
'1.2.3': 0,
|
||||
'header ~': 0,
|
||||
HEADLINE: 0,
|
||||
tag: 0,
|
||||
}
|
||||
enddef
|
||||
|
||||
const AUGROUP: string = 'HelpToc'
|
||||
var fuzzy_entries: list<dict<any>>
|
||||
var help_winid: number
|
||||
var print_entry: bool
|
||||
var selected_entry_match: number
|
||||
|
||||
# Interface {{{1
|
||||
export def Open() #{{{2
|
||||
var type: string = GetType()
|
||||
if !MATCH_ENTRY->has_key(type)
|
||||
return
|
||||
endif
|
||||
if 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'
|
||||
return
|
||||
endif
|
||||
|
||||
# invalidate the cache if the buffer's contents has changed
|
||||
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
|
||||
unlet! b:toc
|
||||
endif
|
||||
endif
|
||||
|
||||
if !exists('b:toc')
|
||||
SetToc()
|
||||
endif
|
||||
|
||||
var winpos: list<number> = winnr()->win_screenpos()
|
||||
var height: number = winheight(0) - 2
|
||||
var width: number = winwidth(0)
|
||||
b:toc.width = b:toc.width ?? width / 3
|
||||
# the popup needs enough space to display the help message in its title
|
||||
if b:toc.width < 30
|
||||
b:toc.width = 30
|
||||
endif
|
||||
# Is `popup_menu()` OK with a list of dictionaries?{{{
|
||||
#
|
||||
# Yes, see `:help popup_create-arguments`.
|
||||
# Although, it expects dictionaries with the keys `text` and `props`.
|
||||
# But we use dictionaries with the keys `text` and `lnum`.
|
||||
# IOW, we abuse the feature which lets us use text properties in a popup.
|
||||
#}}}
|
||||
var winid: number = GetTocEntries()
|
||||
->popup_menu({
|
||||
line: winpos[0],
|
||||
col: winpos[1] + width - 1,
|
||||
pos: 'topright',
|
||||
scrollbar: false,
|
||||
highlight: type == 'terminal' ? 'Terminal' : 'Normal',
|
||||
border: [],
|
||||
borderchars: ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
|
||||
minheight: height,
|
||||
maxheight: height,
|
||||
minwidth: b:toc.width,
|
||||
maxwidth: b:toc.width,
|
||||
filter: Filter,
|
||||
callback: Callback,
|
||||
})
|
||||
Win_execute(winid, [$'ownsyntax {&filetype}', '&l:conceallevel = 3'])
|
||||
# In a help file, we might reduce some noisy tags to a trailing asterisk.
|
||||
# Hide those.
|
||||
if type == 'help'
|
||||
matchadd('Conceal', '\*$', 0, -1, {window: winid})
|
||||
endif
|
||||
SelectNearestEntryFromCursor(winid)
|
||||
|
||||
# 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()
|
||||
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
|
||||
# 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'
|
||||
SetTocHelp()
|
||||
return
|
||||
endif
|
||||
|
||||
if 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, {})
|
||||
->items()
|
||||
->sort((l: list<any>, ll: list<any>): number => l[0]->str2nr() - ll[0]->str2nr())
|
||||
|
||||
for lnum: number in range(1, line('$'))
|
||||
nextline = getline(lnum + 1)
|
||||
for [lvl: string, IsEntry: func: bool] in lvl_and_test
|
||||
if IsEntry(curline, nextline)
|
||||
b:toc.entries->add({
|
||||
lnum: lnum,
|
||||
lvl: lvl->str2nr(),
|
||||
text: curline,
|
||||
})
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
curline = nextline
|
||||
endfor
|
||||
|
||||
InitMaxAndCurLvl()
|
||||
enddef
|
||||
|
||||
def SetTocHelp() #{{{2
|
||||
var main_ruler: string
|
||||
for line: string in getline(1, '$')
|
||||
if line =~ HELP_RULER
|
||||
main_ruler = line =~ '=' ? HELP_RULERS['='] : HELP_RULERS['-']
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
|
||||
var prevline: string
|
||||
var curline: string = getline(1)
|
||||
var nextline: string
|
||||
var in_list: bool
|
||||
var last_numbered_entry: number
|
||||
InitHelpLvls()
|
||||
for lnum: number in range(1, line('$'))
|
||||
nextline = getline(lnum + 1)
|
||||
|
||||
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.
|
||||
InitHelpLvls()
|
||||
endif
|
||||
|
||||
# Do not assume that a list ends on an empty line.
|
||||
# See the list at `:help gdb` for a counter-example.
|
||||
if in_list
|
||||
&& curline !~ '^\d\+.\s'
|
||||
&& curline !~ '^\s*$'
|
||||
&& curline !~ '^[< \t]'
|
||||
in_list = false
|
||||
endif
|
||||
|
||||
if prevline =~ '^\d\+\.\s'
|
||||
&& curline !~ '^\s*$'
|
||||
&& curline !~ $'^\s*{HELP_TAG}'
|
||||
in_list = true
|
||||
endif
|
||||
|
||||
# 1.
|
||||
if prevline =~ '^\d\+\.\s'
|
||||
# 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
|
||||
&& !in_list
|
||||
var current_numbered_entry: number = prevline
|
||||
->matchstr('^\d\+\ze\.\s')
|
||||
->str2nr()
|
||||
if current_numbered_entry > last_numbered_entry
|
||||
AddEntryInTocHelp('1.', lnum - 1, prevline)
|
||||
last_numbered_entry = prevline
|
||||
->matchstr('^\d\+\ze\.\s')
|
||||
->str2nr()
|
||||
endif
|
||||
endif
|
||||
|
||||
# 1.2
|
||||
if curline =~ '^\d\+\.\d\+\s'
|
||||
if curline =~ $'\%({HELP_TAG}\s*\|\~\)$'
|
||||
|| (prevline =~ $'^\s*{HELP_TAG}' || nextline =~ $'^\s*{HELP_TAG}')
|
||||
|| (prevline =~ HELP_RULER || nextline =~ HELP_RULER)
|
||||
|| (prevline =~ '^\s*$' && nextline =~ '^\s*$')
|
||||
AddEntryInTocHelp('1.2', lnum, curline)
|
||||
endif
|
||||
# 1.2.3
|
||||
elseif curline =~ '^\s\=\d\+\.\d\+\.\d\+\s'
|
||||
AddEntryInTocHelp('1.2.3', lnum, curline)
|
||||
endif
|
||||
|
||||
# HEADLINE
|
||||
if curline =~ HELP_HEADLINE
|
||||
&& curline !~ '^CTRL-'
|
||||
&& prevline->IsSpecialHelpLine()
|
||||
&& (nextline->IsSpecialHelpLine() || nextline =~ '^\s*(\|^\t\|^N[oO][tT][eE]:')
|
||||
AddEntryInTocHelp('HEADLINE', lnum, curline)
|
||||
endif
|
||||
|
||||
# header ~
|
||||
if curline =~ '\~$'
|
||||
&& curline =~ '\w'
|
||||
&& curline !~ '^[ \t<]\|\t\|---+---\|^NOTE:'
|
||||
&& curline !~ '^\d\+\.\%(\d\+\%(\.\d\+\)\=\)\=\s'
|
||||
&& prevline !~ $'^\s*{HELP_TAG}'
|
||||
&& prevline !~ '\~$'
|
||||
&& nextline !~ '\~$'
|
||||
AddEntryInTocHelp('header ~', lnum, curline)
|
||||
endif
|
||||
|
||||
# *some_tag*
|
||||
if curline =~ HELP_TAG
|
||||
AddEntryInTocHelp('tag', lnum, curline)
|
||||
endif
|
||||
|
||||
# In the Vim user manual, a main section is a special case.{{{
|
||||
#
|
||||
# It's not a simple numbered section:
|
||||
#
|
||||
# 01.1
|
||||
#
|
||||
# It's used as a tag:
|
||||
#
|
||||
# *01.1* Two manuals
|
||||
# ^ ^
|
||||
#}}}
|
||||
if prevline =~ main_ruler && curline =~ '^\*\d\+\.\d\+\*'
|
||||
AddEntryInTocHelp('*01.1*', lnum, curline)
|
||||
endif
|
||||
|
||||
[prevline, curline] = [curline, nextline]
|
||||
endfor
|
||||
|
||||
# let's ignore the tag on the first line (not really interesting)
|
||||
if b:toc.entries->get(0, {})->get('lnum') == 1
|
||||
b:toc.entries->remove(0)
|
||||
endif
|
||||
|
||||
# let's also ignore anything before the first `1.` line
|
||||
var i: number = b:toc.entries
|
||||
->copy()
|
||||
->map((_, entry: dict<any>) => entry.text)
|
||||
->match('^\s*1\.\s')
|
||||
if i > 0
|
||||
b:toc.entries->remove(0, i - 1)
|
||||
endif
|
||||
|
||||
InitMaxAndCurLvl()
|
||||
|
||||
# set level of tag entries to the deepest level
|
||||
var has_tag: bool = b:toc.entries
|
||||
->copy()
|
||||
->map((_, entry: dict<any>) => entry.text)
|
||||
->match(HELP_TAG) >= 0
|
||||
if has_tag
|
||||
++b:toc.maxlvl
|
||||
endif
|
||||
b:toc.entries
|
||||
->map((_, entry: dict<any>) => entry.lvl == 0
|
||||
? entry->extend({lvl: b:toc.maxlvl})
|
||||
: entry)
|
||||
|
||||
# fix indentation
|
||||
var min_lvl: number = b:toc.entries
|
||||
->copy()
|
||||
->map((_, entry: dict<any>) => entry.lvl)
|
||||
->min()
|
||||
for entry: dict<any> in b:toc.entries
|
||||
entry.text = entry.text
|
||||
->substitute('^\s*', () => repeat(' ', (entry.lvl - min_lvl) * 3), '')
|
||||
endfor
|
||||
enddef
|
||||
|
||||
def AddEntryInTocHelp(type: string, lnum: number, line: string) #{{{2
|
||||
# don't add a duplicate entry
|
||||
if lnum == b:toc.entries->get(-1, {})->get('lnum')
|
||||
# For a numbered line containing a tag, *do* add an entry.
|
||||
# But only for its numbered prefix, not for its tag.
|
||||
# The former is the line's most meaningful representation.
|
||||
if b:toc.entries->get(-1, {})->get('type') == 'tag'
|
||||
b:toc.entries->remove(-1)
|
||||
else
|
||||
return
|
||||
endif
|
||||
endif
|
||||
|
||||
var text: string = line
|
||||
if type == 'tag'
|
||||
var tags: list<string>
|
||||
text->substitute(HELP_TAG, () => !!tags->add(submatch(0)), 'g')
|
||||
text = tags
|
||||
# we ignore errors and warnings because those are meaningless in
|
||||
# a TOC where no context is available
|
||||
->filter((_, tag: string) => tag !~ '\*[EW]\d\+\*')
|
||||
->join()
|
||||
if text !~ HELP_TAG
|
||||
return
|
||||
endif
|
||||
endif
|
||||
|
||||
var maxlvl: number = lvls->values()->max()
|
||||
if type == 'tag'
|
||||
lvls[type] = 0
|
||||
elseif type == '1.2'
|
||||
lvls[type] = lvls[type] ?? lvls->get('1.', maxlvl) + 1
|
||||
elseif type == '1.2.3'
|
||||
lvls[type] = lvls[type] ?? lvls->get('1.2', maxlvl) + 1
|
||||
else
|
||||
lvls[type] = lvls[type] ?? maxlvl + 1
|
||||
endif
|
||||
|
||||
# Ignore noisy tags.{{{
|
||||
#
|
||||
# 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.
|
||||
#}}}
|
||||
# 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.*
|
||||
var after_numbered: string = '^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*'
|
||||
# 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}'
|
||||
text = text->substitute(noisy_tags, '', '')
|
||||
# We don't remove the trailing asterisk, because the help syntax plugin
|
||||
# might need it to highlight some headlines.
|
||||
|
||||
b:toc.entries->add({
|
||||
lnum: lnum,
|
||||
lvl: lvls[type],
|
||||
text: text,
|
||||
type: type,
|
||||
})
|
||||
enddef
|
||||
|
||||
def InitMaxAndCurLvl() #{{{2
|
||||
b:toc.maxlvl = b:toc.entries
|
||||
->copy()
|
||||
->map((_, entry: dict<any>) => entry.lvl)
|
||||
->max()
|
||||
b:toc.curlvl = b:toc.maxlvl
|
||||
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
|
||||
# `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.
|
||||
if entries->get(0, {})->has_key('props')
|
||||
text = entries
|
||||
else
|
||||
text = entries
|
||||
->copy()
|
||||
->map((_, entry: dict<any>): string => entry.text)
|
||||
endif
|
||||
popup_settext(winid, text)
|
||||
SetTitle(winid)
|
||||
redraw
|
||||
enddef
|
||||
|
||||
def SetTitle(winid: number) #{{{2
|
||||
var curlnum: number
|
||||
var lastlnum: number = line('$', winid)
|
||||
var is_empty: bool = lastlnum == 1
|
||||
&& winid->winbufnr()->getbufoneline(1) == ''
|
||||
if is_empty
|
||||
[curlnum, lastlnum] = [0, 0]
|
||||
else
|
||||
curlnum = line('.', winid)
|
||||
endif
|
||||
var newtitle: string = printf(' %*d/%d (%d/%d)',
|
||||
len(lastlnum), curlnum,
|
||||
lastlnum,
|
||||
b:toc.curlvl,
|
||||
b:toc.maxlvl,
|
||||
)
|
||||
|
||||
var width: number = winid->popup_getoptions().minwidth
|
||||
newtitle = printf('%s%*s',
|
||||
newtitle,
|
||||
width - newtitle->strlen(),
|
||||
'press ? for help ')
|
||||
|
||||
popup_setoptions(winid, {title: newtitle})
|
||||
enddef
|
||||
|
||||
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)
|
||||
->len()
|
||||
if firstline == 0
|
||||
return
|
||||
endif
|
||||
Win_execute(winid, $'normal! {firstline}Gzz')
|
||||
enddef
|
||||
|
||||
def Filter(winid: number, key: string): bool #{{{2
|
||||
# support various normal commands for moving/scrolling
|
||||
if [
|
||||
'j', 'J', 'k', 'K', "\<Down>", "\<Up>", "\<C-N>", "\<C-P>",
|
||||
"\<C-D>", "\<C-U>",
|
||||
"\<PageUp>", "\<PageDown>",
|
||||
'g', 'G', "\<Home>", "\<End>",
|
||||
'z'
|
||||
]->index(key) >= 0
|
||||
var scroll_cmd: string = {
|
||||
J: 'j',
|
||||
K: 'k',
|
||||
g: '1G',
|
||||
"\<Home>": '1G',
|
||||
"\<End>": 'G',
|
||||
z: 'zz'
|
||||
}->get(key, key)
|
||||
|
||||
var old_lnum: number = line('.', winid)
|
||||
Win_execute(winid, $'normal! {scroll_cmd}')
|
||||
var new_lnum: number = line('.', winid)
|
||||
|
||||
if print_entry
|
||||
PrintEntry(winid)
|
||||
endif
|
||||
|
||||
# wrap around the edges
|
||||
if new_lnum == old_lnum
|
||||
scroll_cmd = {
|
||||
j: '1G',
|
||||
J: '1G',
|
||||
k: 'G',
|
||||
K: 'G',
|
||||
"\<Down>": '1G',
|
||||
"\<Up>": 'G',
|
||||
"\<C-N>": '1G',
|
||||
"\<C-P>": 'G',
|
||||
}->get(key, '')
|
||||
if !scroll_cmd->empty()
|
||||
Win_execute(winid, $'normal! {scroll_cmd}')
|
||||
endif
|
||||
endif
|
||||
|
||||
# move the cursor to the corresponding line in the main buffer
|
||||
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
|
||||
MatchDelete()
|
||||
selected_entry_match = matchaddpos('IncSearch', [lnum], 0, -1)
|
||||
endif
|
||||
SetTitle(winid)
|
||||
|
||||
return true
|
||||
|
||||
elseif key == 'c'
|
||||
SelectNearestEntryFromCursor(winid)
|
||||
return true
|
||||
|
||||
# when we press `p`, print the selected line (useful when it's truncated)
|
||||
elseif key == 'p'
|
||||
PrintEntry(winid)
|
||||
return true
|
||||
|
||||
# same thing, but automatically
|
||||
elseif key == 'P'
|
||||
print_entry = !print_entry
|
||||
if print_entry
|
||||
PrintEntry(winid)
|
||||
else
|
||||
echo ''
|
||||
endif
|
||||
return true
|
||||
|
||||
elseif key == 'q'
|
||||
popup_close(winid, -1)
|
||||
return true
|
||||
|
||||
elseif key == '?'
|
||||
ToggleHelp(winid)
|
||||
return true
|
||||
|
||||
# scroll help window
|
||||
elseif key == "\<C-J>" || key == "\<C-K>"
|
||||
var scroll_cmd: string = {"\<C-J>": 'j', "\<C-K>": 'k'}->get(key, key)
|
||||
if scroll_cmd == 'j' && line('.', help_winid) == line('$', help_winid)
|
||||
scroll_cmd = '1G'
|
||||
elseif scroll_cmd == 'k' && line('.', help_winid) == 1
|
||||
scroll_cmd = 'G'
|
||||
endif
|
||||
Win_execute(help_winid, $'normal! {scroll_cmd}')
|
||||
return true
|
||||
|
||||
# increase/decrease the popup's width
|
||||
elseif key == '+' || key == '-'
|
||||
var width: number = winid->popup_getoptions().minwidth
|
||||
if key == '-' && width == 1
|
||||
|| key == '+' && winid->popup_getpos().col == 1
|
||||
return true
|
||||
endif
|
||||
width = width + (key == '+' ? 1 : -1)
|
||||
# remember the last width if we close and re-open the TOC later
|
||||
b:toc.width = width
|
||||
popup_setoptions(winid, {minwidth: width, maxwidth: width})
|
||||
return true
|
||||
|
||||
elseif key == 'H' && b:toc.curlvl > 1
|
||||
|| key == 'L' && b:toc.curlvl < b:toc.maxlvl
|
||||
CollapseOrExpand(winid, key)
|
||||
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.
|
||||
DisplayNonFuzzyToc(winid)
|
||||
|
||||
[{
|
||||
group: AUGROUP,
|
||||
event: 'CmdlineChanged',
|
||||
pattern: '@',
|
||||
cmd: $'FuzzySearch({winid})',
|
||||
replace: true,
|
||||
}, {
|
||||
group: AUGROUP,
|
||||
event: 'CmdlineLeave',
|
||||
pattern: '@',
|
||||
cmd: 'TearDown()',
|
||||
replace: true,
|
||||
}]->autocmd_add()
|
||||
|
||||
# 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
|
||||
#}}}
|
||||
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>
|
||||
END
|
||||
input_mappings->execute()
|
||||
|
||||
var look_for: string
|
||||
try
|
||||
popup_setoptions(winid, {mapping: true})
|
||||
look_for = input('look for: ', '', $'custom,{Complete->string()}') | redraw | echo ''
|
||||
catch /Vim:Interrupt/
|
||||
TearDown()
|
||||
finally
|
||||
popup_setoptions(winid, {mapping: false})
|
||||
endtry
|
||||
return look_for == '' ? true : popup_filter_menu(winid, "\<CR>")
|
||||
endif
|
||||
|
||||
return popup_filter_menu(winid, key)
|
||||
enddef
|
||||
|
||||
def FuzzySearch(winid: number) #{{{2
|
||||
var look_for: string = getcmdline()
|
||||
if look_for == ''
|
||||
DisplayNonFuzzyToc(winid)
|
||||
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.
|
||||
var matches: list<list<any>> = b:toc.entries
|
||||
->copy()
|
||||
->matchfuzzypos(look_for, {key: 'text'})
|
||||
|
||||
fuzzy_entries = matches->get(0, [])->copy()
|
||||
var pos: list<list<number>> = matches->get(1, [])
|
||||
|
||||
var text: list<dict<any>>
|
||||
if !has('textprop')
|
||||
text = matches->get(0, [])
|
||||
else
|
||||
var buf: number = winid->winbufnr()
|
||||
if prop_type_get('help-fuzzy-toc', {bufnr: buf}) == {}
|
||||
prop_type_add('help-fuzzy-toc', {
|
||||
bufnr: buf,
|
||||
combine: false,
|
||||
highlight: 'IncSearch',
|
||||
})
|
||||
endif
|
||||
text = matches
|
||||
->get(0, [])
|
||||
->map((i: number, match: dict<any>) => ({
|
||||
text: match.text,
|
||||
props: pos[i]->copy()->map((_, col: number) => ({
|
||||
col: col + 1,
|
||||
length: 1,
|
||||
type: 'help-fuzzy-toc',
|
||||
}))}))
|
||||
endif
|
||||
Win_execute(winid, 'normal! 1Gzt')
|
||||
Popup_settext(winid, text)
|
||||
enddef
|
||||
|
||||
def DisplayNonFuzzyToc(winid: number) #{{{2
|
||||
fuzzy_entries = null_list
|
||||
Popup_settext(winid, GetTocEntries())
|
||||
enddef
|
||||
|
||||
def PrintEntry(winid: number) #{{{2
|
||||
echo GetTocEntries()[line('.', winid) - 1]['text']
|
||||
enddef
|
||||
|
||||
def CollapseOrExpand(winid: number, key: string) #{{{2
|
||||
# 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)
|
||||
|
||||
# find the nearest lower level for which the contents of the TOC changes
|
||||
if key == 'H'
|
||||
while b:toc.curlvl > 1
|
||||
var old: list<dict<any>> = GetTocEntries()
|
||||
--b:toc.curlvl
|
||||
var new: list<dict<any>> = GetTocEntries()
|
||||
# In `:help`, there are only entries in levels 3.
|
||||
# We don't want to collapse to level 2, nor 1.
|
||||
# It would clear the TOC which is confusing.
|
||||
if new->empty()
|
||||
++b:toc.curlvl
|
||||
break
|
||||
endif
|
||||
var did_change: bool = new != old
|
||||
if did_change || b:toc.curlvl == 1
|
||||
break
|
||||
endif
|
||||
endwhile
|
||||
# find the nearest upper level for which the contents of the TOC changes
|
||||
else
|
||||
while b:toc.curlvl < b:toc.maxlvl
|
||||
var old: list<dict<any>> = GetTocEntries()
|
||||
++b:toc.curlvl
|
||||
var did_change: bool = GetTocEntries() != old
|
||||
if did_change || b:toc.curlvl == b:toc.maxlvl
|
||||
break
|
||||
endif
|
||||
endwhile
|
||||
endif
|
||||
|
||||
# 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
|
||||
# direct parent.
|
||||
var toc_lnum: number = 0
|
||||
for entry: dict<any> in toc_entries
|
||||
if entry.lnum > buf_lnum
|
||||
break
|
||||
endif
|
||||
++toc_lnum
|
||||
endfor
|
||||
Win_execute(winid, $'normal! {toc_lnum ?? 1}Gzz')
|
||||
enddef
|
||||
|
||||
def MatchDelete() #{{{2
|
||||
if selected_entry_match == 0
|
||||
return
|
||||
endif
|
||||
|
||||
selected_entry_match->matchdelete()
|
||||
selected_entry_match = 0
|
||||
enddef
|
||||
|
||||
def Callback(winid: number, choice: number) #{{{2
|
||||
MatchDelete()
|
||||
|
||||
if help_winid != 0
|
||||
help_winid->popup_close()
|
||||
help_winid = 0
|
||||
endif
|
||||
|
||||
if choice == -1
|
||||
fuzzy_entries = null_list
|
||||
return
|
||||
endif
|
||||
|
||||
var lnum: number = GetTocEntries()
|
||||
->get(choice - 1, {})
|
||||
->get('lnum')
|
||||
|
||||
fuzzy_entries = null_list
|
||||
|
||||
if lnum == 0
|
||||
return
|
||||
endif
|
||||
|
||||
cursor(lnum, 1)
|
||||
normal! zvzt
|
||||
enddef
|
||||
|
||||
def ToggleHelp(menu_winid: number) #{{{2
|
||||
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 zindex: number = popup_getoptions(menu_winid).zindex
|
||||
++zindex
|
||||
help_winid = HELP_TEXT->popup_create({
|
||||
line: line,
|
||||
col: col,
|
||||
pos: 'topright',
|
||||
minheight: height,
|
||||
maxheight: height,
|
||||
minwidth: width,
|
||||
maxwidth: width,
|
||||
border: [],
|
||||
borderchars: ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
|
||||
highlight: &buftype == 'terminal' ? 'Terminal' : 'Normal',
|
||||
scrollbar: false,
|
||||
zindex: zindex,
|
||||
})
|
||||
|
||||
setwinvar(help_winid, '&cursorline', true)
|
||||
setwinvar(help_winid, '&linebreak', true)
|
||||
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] =~ '^─\+$'
|
||||
matchaddpos('Title', [lnum], 0, -1, {window: help_winid})
|
||||
endif
|
||||
endfor
|
||||
|
||||
else
|
||||
if IsVisible(help_winid)
|
||||
popup_hide(help_winid)
|
||||
else
|
||||
popup_show(help_winid)
|
||||
endif
|
||||
endif
|
||||
enddef
|
||||
|
||||
def Win_execute(winid: number, cmd: any) #{{{2
|
||||
# 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}])
|
||||
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
|
||||
enddef
|
||||
|
||||
def GetTocEntries(): list<dict<any>> #{{{2
|
||||
return fuzzy_entries ?? b:toc.entries
|
||||
->copy()
|
||||
->filter((_, entry: dict<any>): bool => entry.lvl <= b:toc.curlvl)
|
||||
enddef
|
||||
|
||||
def GetBufLnum(winid: number): number #{{{2
|
||||
var toc_lnum: number = line('.', winid)
|
||||
return GetTocEntries()
|
||||
->get(toc_lnum - 1, {})
|
||||
->get('lnum')
|
||||
enddef
|
||||
|
||||
def IsVisible(win: number): bool #{{{2
|
||||
return win->popup_getpos()->get('visible')
|
||||
enddef
|
||||
|
||||
def IsSpecialHelpLine(line: string): bool #{{{2
|
||||
return line =~ '^[<>]\=\s*$'
|
||||
|| line =~ '^\s*\*'
|
||||
|| line =~ HELP_RULER
|
||||
|| line =~ HELP_HEADLINE
|
||||
enddef
|
||||
|
||||
def Complete(..._): string #{{{2
|
||||
return b:toc.entries
|
||||
->copy()
|
||||
->map((_, entry: dict<any>) => entry.text->trim(' ~')->substitute('*', '', 'g'))
|
||||
->filter((_, text: string): bool => text =~ '^[-a-zA-Z0-9_() ]\+$')
|
||||
->sort()
|
||||
->uniq()
|
||||
->join("\n")
|
||||
enddef
|
||||
|
5
runtime/pack/dist/opt/helptoc/plugin/helptoc.vim
vendored
Normal file
5
runtime/pack/dist/opt/helptoc/plugin/helptoc.vim
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
vim9script noclear
|
||||
|
||||
import autoload '../autoload/helptoc.vim'
|
||||
|
||||
command -bar HelpToc helptoc.Open()
|
Loading…
x
Reference in New Issue
Block a user