-
Notifications
You must be signed in to change notification settings - Fork 2
Fuzzy Table Of Contents
Maxim Kim edited this page Aug 21, 2022
·
13 revisions
I have come up with a wrapper function that can accept
- a title;
- a list of things;
- a handler.
This function, FilterMenu, shows popup with a list of things and lets user narrow it down to a specific list element. Then on Enter, handler process the selected element.
So you need to find all Sections/Headings and provide it to that function...
ONLY FOR VIM9+
Create a file ~/.vim/autoload/popup.vim
and put following there:
vim9script
export def FilterMenu(title: string, items: list<any>, Callback: func(any, string), Setup: func(number) = null_function, close_on_bs: bool = false)
if empty(prop_type_get('FilterMenuMatch'))
hi def link FilterMenuMatch Constant
prop_type_add('FilterMenuMatch', {highlight: "FilterMenuMatch", override: true, priority: 1000, combine: true})
endif
var prompt = ""
var hint = ">>> type to filter <<<"
var items_dict: list<dict<any>>
var items_count = items->len()
if items_count < 1
items_dict = [{text: ""}]
elseif items[0]->type() != v:t_dict
items_dict = items->mapnew((_, v) => {
return {text: v}
})
else
items_dict = items
endif
var filtered_items: list<any> = [items_dict]
def Printify(itemsAny: list<any>, props: list<any>): list<any>
if itemsAny[0]->len() == 0 | return [] | endif
if itemsAny->len() > 1
return itemsAny[0]->mapnew((idx, v) => {
return {text: v.text, props: itemsAny[1][idx]->mapnew((_, c) => {
return {col: v.text->byteidx(c) + 1, length: 1, type: 'FilterMenuMatch'}
})}
})
else
return itemsAny[0]->mapnew((_, v) => {
return {text: v.text}
})
endif
enddef
var height = min([&lines - 6, items->len()])
var pos_top = ((&lines - height) / 2) - 1
var winid = popup_create(Printify(filtered_items, []), {
title: $" ({items_count}/{items_count}) {title}: {hint} ",
line: pos_top,
minwidth: (&columns * 0.6)->float2nr(),
maxwidth: (&columns - 5),
minheight: height,
maxheight: height,
border: [],
borderchars: ['─', '│', '─', '│', '╭', '╮', '╯', '╰'],
drag: 0,
wrap: 1,
cursorline: false,
padding: [0, 1, 0, 1],
mapping: 0,
filter: (id, key) => {
if key == "\<esc>"
popup_close(id, -1)
elseif ["\<cr>", "\<C-j>", "\<C-v>", "\<C-t>"]->index(key) > -1
&& filtered_items[0]->len() > 0 && items_count > 0
popup_close(id, {idx: getcurpos(id)[1], key: key})
elseif key == "\<tab>" || key == "\<C-n>"
var ln = getcurpos(id)[1]
win_execute(id, "normal! j")
if ln == getcurpos(id)[1]
win_execute(id, "normal! gg")
endif
elseif key == "\<S-tab>" || key == "\<C-p>"
var ln = getcurpos(id)[1]
win_execute(id, "normal! k")
if ln == getcurpos(id)[1]
win_execute(id, "normal! G")
endif
elseif ["\<cursorhold>", "\<ignore>"]->index(key) == -1
if key == "\<C-U>" && !empty(prompt)
prompt = ""
filtered_items = [items_dict]
elseif (key == "\<C-h>" || key == "\<bs>")
if empty(prompt) && close_on_bs
popup_close(id, {idx: getcurpos(id)[1], key: key})
return true
endif
prompt = prompt->strcharpart(0, prompt->strchars() - 1)
if empty(prompt)
filtered_items = [items_dict]
else
filtered_items = items_dict->matchfuzzypos(prompt, {key: "text"})
endif
elseif key =~ '\p'
prompt ..= key
filtered_items = items_dict->matchfuzzypos(prompt, {key: "text"})
endif
popup_settext(id, Printify(filtered_items, []))
popup_setoptions(id,
{title: $" ({items_count > 0 ? filtered_items[0]->len() : 0}/{items_count}) {title}: {prompt ?? hint} "})
endif
return true
},
callback: (id, result) => {
if result->type() == v:t_number
if result > 0
Callback(filtered_items[0][result - 1], "")
endif
else
Callback(filtered_items[0][result.idx - 1], result.key)
endif
}
})
win_execute(winid, "setl nu cursorline cursorlineopt=both")
if Setup != null_function
Setup(winid)
endif
enddef
Create a file ~/.vim/after/ftplugin/rst.vim
and put follwing there:
vim9script
import autoload 'popup.vim'
def Toc()
var toc: list<dict<any>> = []
var lvl_ch: list<string> = []
for nr in range(1, line('$'))
var line = getline(nr)
var pline = getline(nr - 1)
var ppline = getline(nr - 2)
if line =~ '^\([-=#*~]\)\1*\s*$'
if pline =~ '\S' && ppline == line
var lvl = lvl_ch->index(line[0] .. line[0])
if lvl == -1
lvl_ch->add(line[0] .. line[0])
lvl = lvl_ch->len() - 1
endif
toc->add({text: $'{repeat("\t", lvl)}{pline->trim()} ({nr - 1})', linenr: nr - 1})
elseif pline =~ '^\S' && pline !~ '^\([-=#*~]\)\1*\s*$'
var lvl = lvl_ch->index(line[0])
if lvl == -1
lvl_ch->add(line[0])
lvl = lvl_ch->len() - 1
endif
toc->add({text: $'{repeat("\t", lvl)}{pline->trim()} ({nr - 1})', linenr: nr - 1})
endif
endif
endfor
popup.FilterMenu("TOC", toc,
(res, key) => {
exe $":{res.linenr}"
normal! zz
},
(winid) => {
win_execute(winid, "setl ts=4 list")
win_execute(winid, $"syn match FilterMenuLineNr '(\\d\\+)$'")
hi def link FilterMenuLineNr Comment
})
enddef
nnoremap <buffer> <space>z <scriptcmd>Toc()<CR>
Instead of the last line nnoremap <buffer> ...
use your own mapping to
launch Table Of Contents.
Don't forget to restart vim.
Have fun!
PS, you can have the same for the markdown...