vim9script var borderchars = ['─', '│', '─', '│', '┌', '┐', '┘', '└'] var bordertitle = ['─┐', '┌'] var borderhighlight = [] var popuphighlight = get(g:, "popuphighlight", '') # Returns winnr of created popup window export def ShowAtCursor(text: any, Setup: func(number) = null_function): number var new_text = text if text->type() == v:t_string new_text = text->trim("\") else new_text = text->mapnew((_, v) => v->trim("\")) endif var winid = popup_atcursor(new_text, { padding: [0, 1, 0, 1], border: [], borderchars: borderchars, borderhighlight: borderhighlight, highlight: popuphighlight, pos: "botleft", mapping: 0, filter: (winid, key) => { if key == "\" win_execute(winid, "normal! \\") return true elseif key == "j" win_execute(winid, "normal! \") return true elseif key == "k" win_execute(winid, "normal! \") return true elseif key == "g" win_execute(winid, "normal! gg") return true elseif key == "G" win_execute(winid, "normal! G") return true endif if key == "\" popup_close(winid) return true endif return true } }) if Setup != null_function Setup(winid) endif return winid enddef # Popup menu with fuzzy filtering # Example usage 1: # FilterMenu("Echo Text", # ["He was aware there were numerous wonders of this world including the", # "unexplained creations of humankind that showed the wonder of our", # "ingenuity. There are huge heads on Easter Island. There are the", # "Egyptian pyramids. There's Stonehenge. But he now stood in front of a", # "newly discovered monument that simply didn't make any sense and he", # "wondered how he was ever going to be able to explain it.", # "The wave crashed and hit the sandcastle head-on. The sandcastle began", # "to melt under the waves force and as the wave receded, half the", # "sandcastle was gone. The next wave hit, not quite as strong, but still", # "managed to cover the remains of the sandcastle and take more of it", # "away. The third wave, a big one, crashed over the sandcastle completely", # "covering and engulfing it. When it receded, there was no trace the", # "sandcastle ever existed and hours of hard work disappeared forever." ], # (res, key) => { # echo res # }) # Example usage 2: # FilterMenu("Buffers", # getbufinfo({'buflisted': 1})->mapnew((_, v) => { # return {bufnr: v.bufnr, text: (v.name ?? $'[{v.bufnr}: No Name]')} # }), # (res, key) => { # if key == "\" # exe $":tab sb {res.bufnr}" # elseif key == "\" # exe $":sb {res.bufnr}" # elseif key == "\" # exe $":vert sb {res.bufnr}" # else # exe $":b {res.bufnr}" # endif # }) export def FilterMenu(title: string, items: list, 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 items_dict: list> 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 = [items_dict] def Printify(itemsAny: list, props: list): list 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, max([items->len(), 5])]) var minwidth = (&columns * 0.6)->float2nr() var pos_top = ((&lines - height) / 2) - 1 var ignore_input = ["\", "\", "\", \ "\", "\", "\", $"\<2-LeftMouse>", \ "\", "\", "\", "\<2-RightMouse>", \ "\", "\", "\", "\<2-MiddleMouse>", \ "\", "\", "\", "\<2-MiddleMouse>", \ "\", "\", "\", "\", "\", "\", \ "\" ] # this sequence of bytes are generated when left/right mouse is pressed and # mouse wheel is rolled var ignore_input_wtf = [128, 253, 100] var winid = popup_create(Printify(filtered_items, []), { title: $" ({items_count}/{items_count}) {title} {bordertitle[0]} {bordertitle[1]}", line: pos_top, minwidth: minwidth, maxwidth: (&columns - 5), minheight: height, maxheight: height, border: [], borderchars: borderchars, borderhighlight: borderhighlight, highlight: popuphighlight, drag: 0, wrap: 1, cursorline: false, padding: [0, 1, 0, 1], mapping: 0, filter: (id, key) => { var new_minwidth = popup_getpos(id).core_width if new_minwidth > minwidth minwidth = new_minwidth popup_move(id, {minwidth: minwidth}) endif if key == "\" popup_close(id, -1) elseif ["\", "\", "\", "\", "\"]->index(key) > -1 && filtered_items[0]->len() > 0 && items_count > 0 popup_close(id, {idx: getcurpos(id)[1], key: key}) elseif key == "\" win_execute(id, 'normal! ' .. "\") elseif key == "\" win_execute(id, 'normal! ' .. "\") elseif key == "\" || key == "\" || key == "\" || key == "\" var ln = getcurpos(id)[1] win_execute(id, "normal! j") if ln == getcurpos(id)[1] win_execute(id, "normal! gg") endif elseif key == "\" || key == "\" || key == "\" || key == "\" var ln = getcurpos(id)[1] win_execute(id, "normal! k") if ln == getcurpos(id)[1] win_execute(id, "normal! G") endif # Ignoring fancy events and double clicks, which are 6 char long: `<80> <80>.` elseif ignore_input->index(key) == -1 && strcharlen(key) != 6 && str2list(key) != ignore_input_wtf if key == "\" prompt = "" filtered_items = [items_dict] elseif (key == "\" || key == "\") 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_setoptions(id, {title: $" ({items_count > 0 ? filtered_items[0]->len() : 0}/{items_count}) {title} {bordertitle[0]} {prompt} {bordertitle[1]}" }) popup_settext(id, Printify(filtered_items, [])) 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