diff --git a/autoload/vimtex/context/cite.vim b/autoload/vimtex/context/cite.vim index ef04d14956..7dc8f0feed 100644 --- a/autoload/vimtex/context/cite.vim +++ b/autoload/vimtex/context/cite.vim @@ -164,7 +164,6 @@ function! s:actions.open_pdf() abort dict " {{{1 let l:file = vimtex#ui#select(l:readable, { \ 'prompt': 'Open file:', - \ 'abort': v:false, \}) if empty(l:file) | return | endif diff --git a/autoload/vimtex/state.vim b/autoload/vimtex/state.vim index 26fec997ab..5fe09802bd 100644 --- a/autoload/vimtex/state.vim +++ b/autoload/vimtex/state.vim @@ -479,8 +479,8 @@ function! s:get_main_choose(list) abort " {{{1 unsilent return vimtex#ui#select(l:choices, { \ 'prompt': 'Please select an appropriate main file:', - \ 'abort': v:false, \ 'return': 'key', + \ 'force_choice': v:true, \}) endif endfunction diff --git a/autoload/vimtex/ui.vim b/autoload/vimtex/ui.vim index c5826528b4..f05485e053 100644 --- a/autoload/vimtex/ui.vim +++ b/autoload/vimtex/ui.vim @@ -20,51 +20,24 @@ function! vimtex#ui#echo(input, ...) abort " {{{1 endfunction " }}}1 -function! vimtex#ui#input(opts) abort " {{{1 - let l:opts = extend({'prompt': '> ', 'text': ''}, a:opts) - if g:vimtex_echo_verbose_input && has_key(l:opts, 'info') - redraw! - call vimtex#ui#echo(l:opts.info) - endif - - echohl VimtexMsg - let l:reply = has_key(l:opts, 'completion') - \ ? input(l:opts.prompt, l:opts.text, l:opts.completion) - \ : input(l:opts.prompt, l:opts.text) - echohl None - return l:reply -endfunction - -" }}}1 -function! vimtex#ui#input_quick_from(prompt, choices) abort " {{{1 - while v:true - redraw! - call vimtex#ui#echo(a:prompt) - let l:input = nr2char(getchar()) - - if index(["\", "\"], l:input) >= 0 - echon 'aborted!' - return '' - endif - - if index(a:choices, l:input) >= 0 - echon l:input - return l:input - endif - endwhile +function! vimtex#ui#confirm(prompt) abort " {{{1 + return has('nvim') + \ ? vimtex#ui#nvim#confirm(a:prompt) + \ : vimtex#ui#vim#confirm(a:prompt) endfunction " }}}1 -function! vimtex#ui#confirm(prompt) abort " {{{1 - if type(a:prompt) != v:t_list - let l:prompt = [a:prompt] - else - let l:prompt = a:prompt - endif - let l:prompt[-1] .= ' [y]es/[n]o: ' - - return vimtex#ui#input_quick_from(l:prompt, ['y', 'n']) ==# 'y' +function! vimtex#ui#input(options) abort " {{{1 + let l:options = extend({ + \ 'prompt': '> ', + \ 'text': '', + \ 'info': '', + \}, a:options) + + return has('nvim') + \ ? vimtex#ui#nvim#input(l:options) + \ : vimtex#ui#vim#input(l:options) endfunction " }}}1 @@ -102,21 +75,24 @@ endfunction " }}}1 function! vimtex#ui#select(container, ...) abort " {{{1 - if empty(a:container) | return '' | endif - let l:options = extend( \ { - \ 'abort': v:true, \ 'prompt': 'Please choose item:', \ 'return': 'value', + \ 'force_choice': v:false, \ }, \ a:0 > 0 ? a:1 : {}) - let [l:index, l:value] = s:choose_from( - \ type(a:container) == v:t_dict ? values(a:container) : a:container, - \ l:options) - sleep 75m - redraw! + let l:list = type(a:container) == v:t_dict + \ ? values(a:container) + \ : a:container + let [l:index, l:value] = empty(l:list) + \ ? [-1, ''] + \ : (len(l:list) == 1 + \ ? [0, l:list[0]] + \ : (has('nvim') + \ ? vimtex#ui#nvim#select(l:options, l:list) + \ : vimtex#ui#vim#select(l:options, l:list))) if l:options.return ==# 'value' return l:value @@ -131,6 +107,46 @@ endfunction " }}}1 +function! vimtex#ui#get_number(max, digits, force_choice, do_echo) abort " {{{1 + let l:choice = '' + + if a:do_echo + echo '> ' + endif + + while len(l:choice) < a:digits + if len(l:choice) > 0 && (l:choice . '0') > a:max + return l:choice - 1 + endif + + let l:input = nr2char(getchar()) + + if !a:force_choice && index(["\", "\", 'x'], l:input) >= 0 + if a:do_echo + echon 'aborted!' + endif + return -2 + endif + + if len(l:choice) > 0 && l:input ==# "\" + return l:choice - 1 + endif + + if l:input !~# '\d' | continue | endif + + if (l:choice . l:input) > 0 + let l:choice .= l:input + if a:do_echo + echon l:input + endif + endif + endwhile + + return l:choice - 1 +endfunction + +" }}}1 + function! vimtex#ui#get_winwidth() abort " {{{1 let l:numwidth = (&number || &relativenumber) \ ? max([&numberwidth, strlen(line('$')) + 1]) @@ -199,87 +215,3 @@ function! s:echo_dict(dict, opts) abort " {{{1 endfunction " }}}1 - -function! s:choose_from(list, options) abort " {{{1 - let l:length = len(a:list) - let l:digits = len(l:length) - if l:length == 1 | return [0, a:list[0]] | endif - - " Create the menu - let l:menu = [] - let l:format = printf('%%%dd', l:digits) - let l:i = 0 - for l:x in a:list - let l:i += 1 - call add(l:menu, [ - \ ['VimtexWarning', printf(l:format, l:i) . ': '], - \ type(l:x) == v:t_dict ? l:x.name : l:x - \]) - endfor - if a:options.abort - call add(l:menu, [ - \ ['VimtexWarning', repeat(' ', l:digits - 1) . 'x: '], - \ 'Abort' - \]) - endif - - " Loop to get a valid choice - while 1 - redraw! - - call vimtex#ui#echo(a:options.prompt) - for l:line in l:menu - call vimtex#ui#echo(l:line) - endfor - - try - let l:choice = s:get_number(l:length, l:digits, a:options.abort) - if a:options.abort && l:choice == -2 - return [-1, ''] - endif - - if l:choice >= 0 && l:choice < len(a:list) - return [l:choice, a:list[l:choice]] - endif - endtry - endwhile -endfunction - -" }}}1 -function! s:get_number(max, digits, abort) abort " {{{1 - let l:choice = '' - echo '> ' - - while len(l:choice) < a:digits - if len(l:choice) > 0 && (l:choice . '0') > a:max - return l:choice - 1 - endif - - let l:input = nr2char(getchar()) - - if index(["\", "\"], l:input) >= 0 - echon 'aborted!' - return -2 - endif - - if a:abort && l:input ==# 'x' - echon l:input - return -2 - endif - - if len(l:choice) > 0 && l:input ==# "\" - return l:choice - 1 - endif - - if l:input !~# '\d' | continue | endif - - if (l:choice . l:input) > 0 - let l:choice .= l:input - echon l:input - endif - endwhile - - return l:choice - 1 -endfunction - -" }}}1 diff --git a/autoload/vimtex/ui/nvim.vim b/autoload/vimtex/ui/nvim.vim new file mode 100644 index 0000000000..b587283da9 --- /dev/null +++ b/autoload/vimtex/ui/nvim.vim @@ -0,0 +1,257 @@ +" VimTeX - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#ui#nvim#confirm(prompt) abort " {{{1 + let l:content = [s:formatted_to_string(a:prompt)] + let l:content += [''] + let l:content += [' y = Yes'] + let l:content += [' n = No '] + + let l:popup_cfg = { 'content': l:content } + function l:popup_cfg.highlight() abort + syntax match VimtexPopupContent ".*" contains=VimtexPopupPrompt + syntax match VimtexPopupPrompt "[yn] = \(Yes\|No\)" + \ contains=VimtexPopupPromptInput + syntax match VimtexPopupPromptInput "= \(Yes\|No\)" contained + endfunction + let l:popup = vimtex#ui#nvim#popup(l:popup_cfg) + + " Wait for input + while v:true + let l:input = nr2char(getchar()) + if index(["\", "\", 'y', 'Y', 'n', 'N'], l:input) >= 0 + break + endif + endwhile + + call l:popup.close() + return l:input ==? 'y' +endfunction + +" }}}1 +function! vimtex#ui#nvim#input(options) abort " {{{1 + if has_key(a:options, 'completion') + " We can't replicate completion, so let's just fall back. + return vimtex#ui#vim#input(a:options) + endif + + let l:content = empty(a:options.info) + \ ? [] + \ : [s:formatted_to_string(a:options.info)] + let l:content += [a:options.prompt] + let l:popup_cfg = { + \ 'content': l:content, + \ 'min_width': 0.7, + \ 'prompt': a:options.prompt, + \} + function l:popup_cfg.highlight() abort dict + syntax match VimtexPopupContent ".*" contains=VimtexPopupPrompt + execute 'syntax match VimtexPopupPrompt' + \ '"^\s*' . self.prompt . '"' + \ 'nextgroup=VimtexPopupPromptInput' + syntax match VimtexPopupPromptInput ".*" contained + endfunction + let l:popup = vimtex#ui#nvim#popup(l:popup_cfg) + + let l:value = a:options.text + while v:true + call nvim_buf_set_lines(0, -2, -1, v:false, [' > ' . l:value]) + redraw! + + let l:input_raw = getchar() + let l:input = nr2char(l:input_raw) + + if index(["\", "\", "\"], l:input) >= 0 + let l:value = "" + break + endif + + if l:input ==# "\" + break + endif + + if l:input_raw ==# "\" + let l:value = strcharpart(l:value, 0, strchars(l:value) - 1) + elseif l:input ==# "\" + let l:value = "" + else + let l:value .= l:input + endif + endwhile + + call l:popup.close() + return l:value +endfunction + +" }}}1 +function! vimtex#ui#nvim#select(options, list) abort " {{{1 + let l:length = len(a:list) + let l:digits = len(l:length) + + " Prepare menu of choices + let l:content = [s:formatted_to_string(a:options.prompt), ''] + if !a:options.force_choice + call add(l:content, repeat(' ', l:digits - 1) . 'x: Abort') + endif + let l:format = printf('%%%dd: %%s', l:digits) + let l:i = 0 + for l:x in a:list + let l:i += 1 + call add(l:content, printf( + \ l:format, l:i, type(l:x) == v:t_dict ? l:x.name : l:x)) + endfor + + " Create popup window + let l:popup_cfg = { + \ 'content': l:content, + \ 'position': 'window', + \ 'min_width': 0.8, + \ 'hide_cursor': v:true, + \} + function l:popup_cfg.highlight() abort + syntax match VimtexPopupContent ".*" contains=VimtexPopupPrompt + syntax match VimtexPopupPrompt "^\s*\(\d\+\|x\):\s*" + \ nextgroup=VimtexPopupPromptInput + syntax match VimtexPopupPromptInput ".*" contained + endfunction + let l:popup = vimtex#ui#nvim#popup(l:popup_cfg) + + let l:value = [-1, ''] + while v:true + try + let l:choice = vimtex#ui#get_number( + \ l:length, l:digits, a:options.force_choice, v:false) + + if !a:options.force_choice && l:choice == -2 + break + endif + + if l:choice >= 0 && l:choice < l:length + let l:value = [l:choice, a:list[l:choice]] + break + endif + endtry + endwhile + + call l:popup.close() + return l:value +endfunction + +" }}}1 + +function! vimtex#ui#nvim#popup(cfg) abort " {{{1 + let l:popup = extend({ + \ 'content': [], + \ 'padding': 1, + \ 'position': 'cursor', + \ 'min_width': 0.0, + \ 'min_height': 0.0, + \ 'hide_cursor': v:false, + \}, a:cfg) + + " Define default highlight groups + if !hlexists("VimtexHideCursor") + call nvim_set_hl(0, "VimtexHideCursor", #{ blend: 100, nocombine: v:true }) + highlight default link VimtexPopupContent PreProc + highlight default link VimtexPopupPrompt Special + highlight default link VimtexPopupPromptInput Type + endif + + " Prepare content + let l:content = map( + \ repeat([''], l:popup.padding) + deepcopy(l:popup.content), + \ { _, x -> empty(x) ? x : repeat(' ', l:popup.padding) . x } + \) + + " Calculate window dimensions + let l:winheight = winheight(0) + let l:winwidth = winwidth(0) + let l:height = len(l:content) + l:popup.padding + let l:height = max([l:height, float2nr(l:popup.min_height*l:winheight)]) + + let l:width = 0 + for l:line in l:content + if strdisplaywidth(l:line) > l:width + let l:width = strdisplaywidth(l:line) + endif + endfor + let l:width += 2*l:popup.padding + let l:width = max([l:width, float2nr(l:popup.min_width*l:winwidth)]) + + " Create and fill the buffer + let l:bufnr = nvim_create_buf(v:false, v:true) + call nvim_buf_set_lines(l:bufnr, 0, -1, v:false, l:content) + call nvim_buf_set_option(l:bufnr, 'buftype', 'nofile') + + " Create popup window + let l:winopts = #{ + \ width: l:width, + \ height: l:height, + \ style: "minimal", + \ noautocmd: v:true, + \} + if l:popup.position ==# 'cursor' + let l:winopts.relative = 'cursor' + + let l:c = col('.') + if l:width < l:winwidth - l:c - 1 + let l:winopts.row = 1 - l:height/2 + let l:winopts.col = 2 + else + let l:winopts.row = 1 + let l:winopts.col = 1 + " let l:winopts.col = (l:winwidth - width)/2 - l:c + endif + elseif l:popup.position ==# 'window' + let l:winopts.relative = 'win' + let l:winopts.row = (l:winheight - l:height)/3 + let l:winopts.col = (l:winwidth - l:width)/2 + endif + call nvim_open_win(l:bufnr, v:true, l:winopts) + if l:popup.hide_cursor + let l:popup._guicursor = &guicursor + let &guicursor = 'a:VimtexHideCursor' + endif + + " Apply highlighting + if has_key(l:popup, 'highlight') + call l:popup.highlight() + endif + + call extend(l:popup, #{ + \ bufnr: l:bufnr, + \ height: height, + \ width: width, + \}) + + function l:popup.close() abort dict + close + call nvim_buf_delete(self.bufnr, #{force: v:true}) + if self.hide_cursor + let &guicursor = self._guicursor + endif + endfunction + + redraw! + return l:popup +endfunction + +" }}}1 + +function! s:formatted_to_string(list_or_string) abort " {{{1 + " The input can be a string or an echo-formatted list (see vimtex#ui#echo). + " If the latter, then we must "flatten" and join it. + if type(a:list_or_string) == v:t_string + return a:list_or_string + endif + + let l:strings = map( + \ a:list_or_string, + \ { _, x -> type(x) == v:t_list ? x[1] : x }) + return join(l:strings, '') +endfunction + +" }}}1 diff --git a/autoload/vimtex/ui/vim.vim b/autoload/vimtex/ui/vim.vim new file mode 100644 index 0000000000..a757076bcc --- /dev/null +++ b/autoload/vimtex/ui/vim.vim @@ -0,0 +1,106 @@ +" VimTeX - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#ui#vim#confirm(prompt) abort " {{{1 + let l:prompt = type(a:prompt) == v:t_list ? a:prompt : [a:prompt] + let l:prompt[-1] .= ' [y]es/[n]o: ' + + while v:true + redraw! + call vimtex#ui#echo(l:prompt) + + let l:input = nr2char(getchar()) + if index(["\", "\"], l:input) >= 0 + break + endif + + if index(['y', 'Y', 'n', 'N'], l:input) >= 0 + echon l:input + sleep 75m + redraw! + break + endif + endwhile + + return l:input ==? 'y' +endfunction + +" }}}1 +function! vimtex#ui#vim#input(options) abort " {{{1 + if g:vimtex_echo_verbose_input && !empty(a:options.info) + redraw! + call vimtex#ui#echo(a:options.info) + endif + + echohl VimtexMsg + let l:input = has_key(a:options, 'completion') + \ ? input(a:options.prompt, a:options.text, a:options.completion) + \ : input(a:options.prompt, a:options.text) + echohl None + + return l:input +endfunction + +" }}}1 +function! vimtex#ui#vim#select(options, list) abort " {{{1 + let l:length = len(a:list) + let l:digits = len(l:length) + + " Use simple menu when in operator mode + if !empty(&operatorfunc) + let l:choices = map(deepcopy(a:list), { i, x -> (i+1) . ': ' . x }) + let l:choice = inputlist(l:choices) - 1 + return l:choice >= 0 && l:choice < l:length + \ ? [l:choice, a:list[l:choice]] + \ : [-1, ''] + endif + + " Create the menu + let l:menu = [a:options.prompt] + let l:format = printf('%%%dd: ', l:digits) + let l:i = 0 + for l:x in a:list + let l:i += 1 + call add(l:menu, [ + \ ['VimtexWarning', printf(l:format, l:i)], + \ type(l:x) == v:t_dict ? l:x.name : l:x + \]) + endfor + if !a:options.force_choice + call add(l:menu, [ + \ ['VimtexWarning', repeat(' ', l:digits - 1) . 'x: '], + \ 'Abort' + \]) + endif + + " Loop to get a valid choice + let l:value = '' + while v:true + redraw! + + for l:line in l:menu + call vimtex#ui#echo(l:line) + endfor + + let l:choice = vimtex#ui#get_number( + \ l:length, l:digits, a:options.force_choice, v:true) + + if !a:options.force_choice && l:choice == -2 + break + endif + + if l:choice >= 0 && l:choice < l:length + let l:value = a:list[l:choice] + break + endif + endwhile + + sleep 75m + redraw! + return [l:choice, l:value] +endfunction + +" }}}1 diff --git a/compiler/textidote.vim b/compiler/textidote.vim index fdc2951074..188d339f49 100644 --- a/compiler/textidote.vim +++ b/compiler/textidote.vim @@ -33,7 +33,7 @@ endif let s:language = vimtex#ui#select(split(&spelllang, ','), { \ 'prompt': 'Multiple spelllang languages detected, please select one:', - \ 'abort': v:false, + \ 'force_choice': v:true, \}) let &l:makeprg = 'java -jar ' . shellescape(fnamemodify(s:cfg.jar, ':p')) \ . (has_key(s:cfg, 'args') ? ' ' . s:cfg.args : '') diff --git a/compiler/vlty.vim b/compiler/vlty.vim index 66ee762ad9..a808c649dd 100644 --- a/compiler/vlty.vim +++ b/compiler/vlty.vim @@ -75,7 +75,7 @@ let s:packages = join(keys(s:vimtex.packages), ',') if !exists('s:vlty.language') let s:vlty.language = vimtex#ui#select(split(&spelllang, ','), { \ 'prompt': 'Multiple spelllang languages detected, please select one:', - \ 'abort': v:false, + \ 'force_choice': v:true, \}) endif