mirror of
https://github.com/vim/vim
synced 2025-04-30 05:17:45 +02:00
Follow up on PR 10446 [1] so that changes at run-time (or after loading a vimrc) are reflected at next use. Instead of "uncaching" the variable by computing SHELL_PROMPT on each use, which could negatively impact performance, reload any user settings before creating the TOC. Also make sure, changes to the shell prompt variable do correctly invalidate b:toc, so that the table of content is correctly re-created after user makes any changes. [1]: https://github.com/vim/vim/pull/10446#issuecomment-2485169333 closes: #16097 Signed-off-by: D. Ben Knoble <ben.knoble+github@gmail.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
953 lines
29 KiB
VimL
953 lines
29 KiB
VimL
vim9script noclear
|
|
|
|
# Config {{{1
|
|
|
|
var SHELL_PROMPT: string = ''
|
|
|
|
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
|
|
# invalidate cache: user config has changed
|
|
unlet! b:toc
|
|
endif
|
|
enddef
|
|
|
|
UpdateUserSettings()
|
|
|
|
# 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
|
|
|
|
UpdateUserSettings()
|
|
|
|
# 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
|
|
|