diff options
Diffstat (limited to 'autoload/vimtex/complete.vim')
-rw-r--r-- | autoload/vimtex/complete.vim | 1089 |
1 files changed, 1089 insertions, 0 deletions
diff --git a/autoload/vimtex/complete.vim b/autoload/vimtex/complete.vim new file mode 100644 index 00000000..b23bc0de --- /dev/null +++ b/autoload/vimtex/complete.vim @@ -0,0 +1,1089 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve LervÄg +" Email: karl.yngve@gmail.com +" + +function! vimtex#complete#init_buffer() abort " {{{1 + if !g:vimtex_complete_enabled | return | endif + + if !has_key(b:vimtex, 'complete') + let b:vimtex.complete = {} + endif + + for l:completer in s:completers + if has_key(l:completer, 'init') + call l:completer.init() + endif + endfor + + setlocal omnifunc=vimtex#complete#omnifunc +endfunction + +" }}}1 + +function! vimtex#complete#omnifunc(findstart, base) abort " {{{1 + if a:findstart + if exists('s:completer') | unlet s:completer | endif + + let l:pos = col('.') - 1 + let l:line = getline('.')[:l:pos-1] + for l:completer in s:completers + if !get(l:completer, 'enabled', 1) | continue | endif + + for l:pattern in l:completer.patterns + if l:line =~# l:pattern + let s:completer = l:completer + while l:pos > 0 + if l:line[l:pos - 1] =~# '{\|,\|\[\|\\' + \ || l:line[l:pos-2:l:pos-1] ==# ', ' + let s:completer.context = matchstr(l:line, + \ get(s:completer, 're_context', '\S*$')) + return l:pos + else + let l:pos -= 1 + endif + endwhile + return -2 + endif + endfor + endfor + return -3 + else + if !exists('s:completer') | return [] | endif + + return g:vimtex_complete_close_braces && get(s:completer, 'inside_braces', 1) + \ ? s:close_braces(s:completer.complete(a:base)) + \ : s:completer.complete(a:base) + endif +endfunction + +" }}}1 +function! vimtex#complete#complete(type, input, context) abort " {{{1 + try + let s:completer = s:completer_{a:type} + let s:completer.context = a:context + return s:completer.complete(a:input) + catch /E121/ + return [] + endtry +endfunction + +" }}}1 + +" +" Completers +" +" {{{1 Bibtex + +let s:completer_bib = { + \ 'patterns' : [ + \ '\v\\\a*cite\a*%(\s*\[[^]]*\]){0,2}\s*\{[^}]*$', + \ '\v\\bibentry\s*\{[^}]*$', + \ '\v\\%(text|block)cquote\*?%(\s*\[[^]]*\]){0,2}\{[^}]*$', + \ '\v\\%(for|hy)\w+cquote\*?\{[^}]*\}%(\s*\[[^]]*\]){0,2}\{[^}]*$', + \ ], + \ 'bibs' : '''\v%(%(\\@<!%(\\\\)*)@<=\%.*)@<!' + \ . '\\(%(no)?bibliography|add(bibresource|globalbib|sectionbib))' + \ . '\m\s*{\zs[^}]\+\ze}''', + \ 'initialized' : 0, + \} + +function! s:completer_bib.init() dict abort " {{{2 + if self.initialized | return | endif + let self.initialized = 1 + + let self.patterns += g:vimtex_complete_bib.custom_patterns +endfunction + +function! s:completer_bib.complete(regex) dict abort " {{{2 + let self.candidates = self.gather_candidates() + + if g:vimtex_complete_bib.simple + call s:filter_with_options(self.candidates, a:regex) + else + call s:filter_with_options(self.candidates, a:regex, { + \ 'anchor': 0, + \ 'filter_by_menu': 1, + \}) + endif + + return self.candidates +endfunction + +function! s:completer_bib.gather_candidates() dict abort " {{{2 + let l:entries = [] + + let l:cache = vimtex#cache#open('bibcomplete', { + \ 'local': 1, + \ 'default': {'result': [], 'ftime': -1} + \}) + + " + " Find data from external bib files + " + + " Note: bibtex seems to require that we are in the project root + call vimtex#paths#pushd(b:vimtex.root) + for l:file in self.find_bibs() + if empty(l:file) | continue | endif + + let l:filename = substitute(l:file, '\%(\.bib\)\?$', '.bib', '') + if !filereadable(l:filename) + let l:filename = vimtex#kpsewhich#find(l:filename) + endif + if !filereadable(l:filename) | continue | endif + + let l:current = l:cache.get(l:filename) + let l:ftime = getftime(l:filename) + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + let l:current.result = map( + \ vimtex#parser#bib(l:filename), + \ 'self.convert(v:val)') + let l:cache.modified = 1 + endif + let l:entries += l:current.result + endfor + + call vimtex#paths#popd() + + " + " Find data from 'thebibliography' environments + " + let l:ftime = b:vimtex.getftime() + if l:ftime > 0 + let l:current = l:cache.get(sha256(b:vimtex.tex)) + + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + let l:current.result = [] + + let l:lines = vimtex#parser#tex(b:vimtex.tex, {'detailed' : 0}) + if match(l:lines, '\C\\begin{thebibliography}') >= 0 + call filter(l:lines, 'v:val =~# ''\C\\bibitem''') + + for l:line in l:lines + let l:matches = matchlist(l:line, '\\bibitem\(\[[^]]\]\)\?{\([^}]*\)') + if len(l:matches) > 1 + call add(l:current.result, self.convert({ + \ 'key': l:matches[2], + \ 'type': 'thebibliography', + \ 'author': '', + \ 'year': '', + \ 'title': l:matches[2], + \ })) + endif + endfor + endif + endif + + let l:entries += l:current.result + endif + + " Write cache to file + call l:cache.write() + + return l:entries +endfunction + +function! s:completer_bib.find_bibs() dict abort " {{{2 + " + " Search for added bibliographies + " * Parse commands such as \bibliography{file1,file2.bib,...} + " * This also removes the .bib extensions + " + + let l:cache = vimtex#cache#open('bibfiles', { + \ 'local': 1, + \ 'default': {'files': [], 'ftime': -1} + \}) + + " Handle local file editing (e.g. subfiles package) + let l:id = get(get(b:, 'vimtex_local', {'main_id' : b:vimtex_id}), 'main_id') + let l:vimtex = vimtex#state#get(l:id) + + let l:bibfiles = [] + for l:file in map(copy(l:vimtex.sources), 'l:vimtex.root . ''/'' . v:val') + let l:current = l:cache.get(l:file) + + let l:ftime = getftime(l:file) + if l:ftime > l:current.ftime + let l:cache.modified = 1 + let l:current.ftime = l:ftime + let l:current.files = [] + for l:entry in map( + \ filter(readfile(l:file), 'v:val =~ ' . self.bibs), + \ 'matchstr(v:val, ' . self.bibs . ')') + let l:files = [] + let l:entry = substitute(l:entry, '\\jobname', b:vimtex.name, 'g') + + for l:f in split(l:entry, ',') + if stridx(l:f, '*') >= 0 + let l:files += glob(l:f, 0, 1) + else + let l:files += [fnamemodify(l:f, ':r')] + endif + endfor + + let l:current.files += l:files + endfor + endif + + let l:bibfiles += l:current.files + endfor + + " Write cache to file + call l:cache.write() + + return vimtex#util#uniq(l:bibfiles) +endfunction + +function! s:completer_bib.convert(entry) dict abort " {{{2 + let cand = {'word': a:entry['key']} + + let auth = get(a:entry, 'author', 'Unknown')[:20] + let auth = substitute(auth, '\~', ' ', 'g') + let substitutes = { + \ '@key' : a:entry['key'], + \ '@type' : empty(a:entry['type']) ? '-' : a:entry['type'], + \ '@author_all' : auth, + \ '@author_short' : substitute(auth, ',.*\ze', ' et al.', ''), + \ '@year' : get(a:entry, 'year', get(a:entry, 'date', '?')), + \ '@title' : get(a:entry, 'title', 'No title'), + \} + + " Create menu string + if !empty(g:vimtex_complete_bib.menu_fmt) + let cand.menu = copy(g:vimtex_complete_bib.menu_fmt) + for [key, val] in items(substitutes) + let cand.menu = substitute(cand.menu, key, escape(val, '&'), '') + endfor + endif + + " Create abbreviation string + if !empty(g:vimtex_complete_bib.abbr_fmt) + let cand.abbr = copy(g:vimtex_complete_bib.abbr_fmt) + for [key, val] in items(substitutes) + let cand.abbr = substitute(cand.abbr, key, escape(val, '&'), '') + endfor + endif + + return cand +endfunction + +" }}}1 +" {{{1 Labels + +let s:completer_ref = { + \ 'patterns' : [ + \ '\v\\v?%(auto|eq|[cC]?%(page)?|labelc)?ref%(\s*\{[^}]*|range\s*\{[^,{}]*%(\}\{)?)$', + \ '\\hyperref\s*\[[^]]*$', + \ '\\subref\*\?{[^}]*$', + \ ], + \ 're_context' : '\\\w*{[^}]*$', + \ 'labels' : [], + \ 'initialized' : 0, + \} + +function! s:completer_ref.init() dict abort " {{{2 + if self.initialized | return | endif + let self.initialized = 1 + + " Add custom patterns + let self.patterns += g:vimtex_complete_ref.custom_patterns +endfunction + +function! s:completer_ref.complete(regex) dict abort " {{{2 + let self.candidates = self.get_matches(a:regex) + + if self.context =~# '\\eqref' + \ && !empty(filter(copy(self.matches), 'v:val.word =~# ''^eq:''')) + call filter(self.candidates, 'v:val.word =~# ''^eq:''') + endif + + return self.candidates +endfunction + +function! s:completer_ref.get_matches(regex) dict abort " {{{2 + call self.parse_aux_files() + + " Match number + let self.matches = filter(copy(self.labels), 'v:val.menu =~# ''' . a:regex . '''') + if !empty(self.matches) | return self.matches | endif + + " Match label + let self.matches = filter(copy(self.labels), 'v:val.word =~# ''' . a:regex . '''') + + " Match label and number + if empty(self.matches) + let l:regex_split = split(a:regex) + if len(l:regex_split) > 1 + let l:base = l:regex_split[0] + let l:number = escape(join(l:regex_split[1:], ' '), '.') + let self.matches = filter(copy(self.labels), + \ 'v:val.word =~# ''' . l:base . ''' &&' . + \ 'v:val.menu =~# ''' . l:number . '''') + endif + endif + + return self.matches +endfunction + +function! s:completer_ref.parse_aux_files() dict abort " {{{2 + let l:files = [[b:vimtex.aux(), '']] + + " Handle local file editing (e.g. subfiles package) + if exists('b:vimtex_local') && b:vimtex_local.active + let l:files += [[vimtex#state#get(b:vimtex_local.main_id).aux(), '']] + endif + + " Add externaldocuments (from \externaldocument in preamble) + let l:files += map( + \ vimtex#parser#get_externalfiles(), + \ '[v:val.aux, v:val.opt]') + + let l:cache = vimtex#cache#open('refcomplete', { + \ 'local': 1, + \ 'default': {'labels': [], 'ftime': -1} + \}) + + let self.labels = [] + for [l:file, l:prefix] in filter(l:files, 'filereadable(v:val[0])') + let l:current = l:cache.get(l:file) + let l:ftime = getftime(l:file) + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + let l:current.labels = self.parse_labels(l:file, l:prefix) + let l:cache.modified = 1 + endif + + let self.labels += l:current.labels + endfor + + " Write cache to file + call l:cache.write() + + return self.labels +endfunction + +function! s:completer_ref.parse_labels(file, prefix) dict abort " {{{2 + " + " Searches aux files recursively for commands of the form + " + " \newlabel{name}{{number}{page}.*}.* + " \newlabel{name}{{text {number}}{page}.*}.* + " \newlabel{name}{{number}{page}{...}{type}.*}.* + " + " Returns a list of candidates like {'word': name, 'menu': type number page}. + " + + let l:labels = [] + let l:lines = vimtex#parser#auxiliary(a:file) + let l:lines = filter(l:lines, 'v:val =~# ''\\newlabel{''') + let l:lines = filter(l:lines, 'v:val !~# ''@cref''') + let l:lines = filter(l:lines, 'v:val !~# ''sub@''') + let l:lines = filter(l:lines, 'v:val !~# ''tocindent-\?[0-9]''') + for l:line in l:lines + let l:line = vimtex#util#tex2unicode(l:line) + let l:tree = vimtex#util#tex2tree(l:line)[1:] + let l:name = get(remove(l:tree, 0), 0, '') + if empty(l:name) + continue + else + let l:name = a:prefix . l:name + endif + let l:context = remove(l:tree, 0) + if type(l:context) == type([]) && len(l:context) > 1 + let l:menu = '' + try + let l:type = substitute(l:context[3][0], '\..*$', ' ', '') + let l:type = substitute(l:type, 'AMS', 'Equation', '') + let l:menu .= toupper(l:type[0]) . l:type[1:] + catch + endtry + + let l:number = self.parse_number(l:context[0]) + if l:menu =~# 'Equation' + let l:number = '(' . l:number . ')' + endif + let l:menu .= l:number + + try + let l:menu .= ' [p. ' . l:context[1][0] . ']' + catch + endtry + call add(l:labels, {'word': l:name, 'menu': l:menu}) + endif + endfor + + return l:labels +endfunction + +function! s:completer_ref.parse_number(num_tree) dict abort " {{{2 + if type(a:num_tree) == type([]) + if len(a:num_tree) == 0 + return '-' + else + let l:index = len(a:num_tree) == 1 ? 0 : 1 + return self.parse_number(a:num_tree[l:index]) + endif + else + let l:matches = matchlist(a:num_tree, '\v(^|.*\s)((\u|\d+)(\.\d+)*\l?)($|\s.*)') + return len(l:matches) > 3 ? l:matches[2] : '-' + endif +endfunction + +" }}}1 +" {{{1 Commands + +let s:completer_cmd = { + \ 'patterns' : [g:vimtex#re#not_bslash . '\\\a*$'], + \ 'inside_braces' : 0, + \} + +function! s:completer_cmd.complete(regex) dict abort " {{{2 + let l:candidates = self.gather_candidates() + let l:mode = vimtex#util#in_mathzone() ? 'm' : 'n' + + call s:filter_with_options(l:candidates, a:regex) + call filter(l:candidates, 'l:mode =~# v:val.mode') + + return l:candidates +endfunction + +function! s:completer_cmd.gather_candidates() dict abort " {{{2 + let l:candidates = s:load_from_document('cmd') + let l:candidates += self.gather_candidates_from_lets() + for l:pkg in s:get_packages() + let l:candidates += s:load_from_package(l:pkg, 'cmd') + endfor + let l:candidates += self.gather_candidates_from_glossary_keys() + + return vimtex#util#uniq_unsorted(l:candidates) +endfunction + +function! s:completer_cmd.gather_candidates_from_glossary_keys() dict abort " {{{2 + if !has_key(b:vimtex.packages, 'glossaries') | return [] | endif + + let l:preamble = vimtex#parser#preamble(b:vimtex.tex) + call map(l:preamble, "substitute(v:val, '\\s*%.*', '', 'g')") + let l:glskeys = split(join(l:preamble, "\n"), '\n\s*\\glsaddkey\*\?')[1:] + call map(l:glskeys, "substitute(v:val, '\n\\s*', '', 'g')") + call map(l:glskeys, 'vimtex#util#tex2tree(v:val)[2:6]') + + let l:candidates = map(vimtex#util#flatten(l:glskeys), '{ + \ ''word'' : v:val[1:], + \ ''mode'' : ''.'', + \ ''kind'' : ''[cmd: glossaries]'', + \ }') + + return l:candidates +endfunction + +function! s:completer_cmd.gather_candidates_from_lets() dict abort " {{{2 + let l:preamble = vimtex#parser#preamble(b:vimtex.tex) + + let l:lets = filter(copy(l:preamble), 'v:val =~# ''\\let\>''') + let l:defs = filter(copy(l:preamble), 'v:val =~# ''\\def\>''') + let l:candidates = map(l:lets, '{ + \ ''word'' : matchstr(v:val, ''\\let[^\\]*\\\zs\w*''), + \ ''mode'' : ''.'', + \ ''kind'' : ''[cmd: \let]'', + \ }') + \ + map(l:defs, '{ + \ ''word'' : matchstr(v:val, ''\\def[^\\]*\\\zs\w*''), + \ ''mode'' : ''.'', + \ ''kind'' : ''[cmd: \def]'', + \ }') + + return l:candidates +endfunction + +" }}}1 +" {{{1 Environments + +let s:completer_env = { + \ 'patterns' : ['\v\\%(begin|end)%(\s*\[[^]]*\])?\s*\{[^}]*$'], + \} + +function! s:completer_env.complete(regex) dict abort " {{{2 + if self.context =~# '^\\end\>' + " When completing \end{, search for an unmatched \begin{...} + let l:matching_env = '' + let l:save_pos = vimtex#pos#get_cursor() + let l:pos_val_cursor = vimtex#pos#val(l:save_pos) + + let l:lnum = l:save_pos[1] + 1 + while l:lnum > 1 + let l:open = vimtex#delim#get_prev('env_tex', 'open') + if empty(l:open) || get(l:open, 'name', '') ==# 'document' + break + endif + + let l:close = vimtex#delim#get_matching(l:open) + if empty(l:close.match) + let l:matching_env = l:close.name . (l:close.starred ? '*' : '') + break + endif + + let l:pos_val_try = vimtex#pos#val(l:close) + strlen(l:close.match) + if l:pos_val_try > l:pos_val_cursor + break + else + let l:lnum = l:open.lnum + call vimtex#pos#set_cursor(vimtex#pos#prev(l:open)) + endif + endwhile + + call vimtex#pos#set_cursor(l:save_pos) + + if !empty(l:matching_env) && l:matching_env =~# a:regex + return [{ + \ 'word': l:matching_env, + \ 'kind': '[env: matching]', + \}] + endif + endif + + return s:filter_with_options(copy(self.gather_candidates()), a:regex) +endfunction + +" }}}2 +function! s:completer_env.gather_candidates() dict abort " {{{2 + let l:candidates = s:load_from_document('env') + for l:pkg in s:get_packages() + let l:candidates += s:load_from_package(l:pkg, 'env') + endfor + + return vimtex#util#uniq_unsorted(l:candidates) +endfunction + +" }}}2 +" }}}1 +" {{{1 Filenames (\includegraphics) + +let s:completer_img = { + \ 'patterns' : ['\v\\includegraphics\*?%(\s*\[[^]]*\]){0,2}\s*\{[^}]*$'], + \ 'ext_re' : '\v\.%(' + \ . join(['png', 'jpg', 'eps', 'pdf', 'pgf', 'tikz'], '|') + \ . ')$', + \} + +function! s:completer_img.complete(regex) dict abort " {{{2 + return s:filter_with_options(self.gather_candidates(), a:regex) +endfunction + +function! s:completer_img.gather_candidates() dict abort " {{{2 + let l:added_files = [] + let l:generated_pdf = b:vimtex.out() + + let l:candidates = [] + for l:path in b:vimtex.graphicspath + [b:vimtex.root] + let l:files = globpath(l:path, '**/*.*', 1, 1) + + call filter(l:files, 'v:val =~? self.ext_re') + call filter(l:files, 'v:val !=# l:generated_pdf') + call filter(l:files, 'index(l:added_files, v:val) < 0') + + let l:added_files += l:files + let l:candidates += map(l:files, "{ + \ 'abbr': vimtex#paths#shorten_relative(v:val), + \ 'word': vimtex#paths#relative(v:val, l:path), + \ 'kind': '[graphics]', + \}") + endfor + + return l:candidates +endfunction + +" }}}1 +" {{{1 Filenames (\input, \include, and \subfile) + +let s:completer_inc = { + \ 'patterns' : [ + \ g:vimtex#re#tex_input . '[^}]*$', + \ '\v\\includeonly\s*\{[^}]*$', + \ ], + \} + +function! s:completer_inc.complete(regex) dict abort " {{{2 + let self.candidates = split(globpath(b:vimtex.root, '**/*.tex'), '\n') + let self.candidates = map(self.candidates, + \ 'strpart(v:val, len(b:vimtex.root)+1)') + call s:filter_with_options(self.candidates, a:regex) + + if self.context =~# '\\include' + let self.candidates = map(self.candidates, '{ + \ ''word'' : fnamemodify(v:val, '':r''), + \ ''kind'' : '' [include]'', + \}') + else + let self.candidates = map(self.candidates, '{ + \ ''word'' : v:val, + \ ''kind'' : '' [input]'', + \}') + endif + + return self.candidates +endfunction + +" }}}1 +" {{{1 Filenames (\includepdf) + +let s:completer_pdf = { + \ 'patterns' : ['\v\\includepdf%(\s*\[[^]]*\])?\s*\{[^}]*$'], + \} + +function! s:completer_pdf.complete(regex) dict abort " {{{2 + let self.candidates = split(globpath(b:vimtex.root, '**/*.pdf'), '\n') + let self.candidates = map(self.candidates, + \ 'strpart(v:val, len(b:vimtex.root)+1)') + call s:filter_with_options(self.candidates, a:regex) + let self.candidates = map(self.candidates, '{ + \ ''word'' : v:val, + \ ''kind'' : '' [includepdf]'', + \}') + return self.candidates +endfunction + +" }}}1 +" {{{1 Filenames (\includestandalone) + +let s:completer_sta = { + \ 'patterns' : ['\v\\includestandalone%(\s*\[[^]]*\])?\s*\{[^}]*$'], + \} + +function! s:completer_sta.complete(regex) dict abort " {{{2 + let self.candidates = substitute(globpath(b:vimtex.root, '**/*.tex'), '\.tex', '', 'g') + let self.candidates = split(self.candidates, '\n') + let self.candidates = map(self.candidates, + \ 'strpart(v:val, len(b:vimtex.root)+1)') + call s:filter_with_options(self.candidates, a:regex) + let self.candidates = map(self.candidates, '{ + \ ''word'' : v:val, + \ ''kind'' : '' [includestandalone]'', + \}') + return self.candidates +endfunction + +" }}}1 +" {{{1 Glossary (\gls +++) + +let s:completer_gls = { + \ 'patterns' : [ + \ '\v\\([cpdr]?(gls|Gls|GLS)|acr|Acr|ACR)\a*\s*\{[^}]*$', + \ '\v\\(ac|Ac|AC)\s*\{[^}]*$', + \ ], + \ 'key' : { + \ 'newglossaryentry' : ' [gls]', + \ 'longnewglossaryentry' : ' [gls]', + \ 'newacronym' : ' [acr]', + \ 'newabbreviation' : ' [abbr]', + \ 'glsxtrnewsymbol' : ' [symbol]', + \ }, + \} + +function! s:completer_gls.init() dict abort " {{{2 + if !has_key(b:vimtex.packages, 'glossaries-extra') | return | endif + + " Detect stuff like this: + " \GlsXtrLoadResources[src=glossary.bib] + " \GlsXtrLoadResources[src={glossary.bib}, selection={all}] + " \GlsXtrLoadResources[selection={all},src={glossary.bib}] + " \GlsXtrLoadResources[ + " src={glossary.bib}, + " selection={all}, + " ] + + let l:do_search = 0 + for l:line in vimtex#parser#preamble(b:vimtex.tex) + if line =~# '^\s*\\GlsXtrLoadResources\s*\[' + let l:do_search = 1 + let l:line = matchstr(l:line, '^\s*\\GlsXtrLoadResources\s*\[\zs.*') + endif + if !l:do_search | continue | endif + + let l:matches = split(l:line, '[=,]') + if empty(l:matches) | continue | endif + + while !empty(l:matches) + let l:key = vimtex#util#trim(remove(l:matches, 0)) + if l:key ==# 'src' + let l:value = vimtex#util#trim(remove(l:matches, 0)) + let l:value = substitute(l:value, '^{', '', '') + let l:value = substitute(l:value, '[]}]\s*', '', 'g') + let b:vimtex.complete.glsbib = l:value + break + endif + endwhile + endfor +endfunction + +function! s:completer_gls.complete(regex) dict abort " {{{2 + return s:filter_with_options( + \ self.parse_glsentries() + self.parse_glsbib(), a:regex) +endfunction + +function! s:completer_gls.parse_glsentries() dict abort " {{{2 + let l:candidates = [] + + let l:re_commands = '\v\\(' . join(keys(self.key), '|') . ')' + let l:re_matcher = l:re_commands . '\s*%(\[.*\])=\s*\{([^{}]*)' + + for l:line in filter( + \ vimtex#parser#tex(b:vimtex.tex, {'detailed' : 0}), + \ 'v:val =~# l:re_commands') + let l:matches = matchlist(l:line, l:re_matcher) + call add(l:candidates, { + \ 'word' : l:matches[2], + \ 'menu' : self.key[l:matches[1]], + \}) + endfor + + return l:candidates +endfunction + +function! s:completer_gls.parse_glsbib() dict abort " {{{2 + let l:filename = get(b:vimtex.complete, 'glsbib', '') + if empty(l:filename) | return [] | endif + + let l:candidates = [] + for l:entry in vimtex#parser#bib(l:filename, {'backend': 'bibparse'}) + call add(l:candidates, { + \ 'word': l:entry.key, + \ 'menu': get(l:entry, 'name', '--'), + \}) + endfor + + return l:candidates +endfunction + +" }}}1 +" {{{1 Packages (\usepackage) + +let s:completer_pck = { + \ 'patterns' : [ + \ '\v\\%(usepackage|RequirePackage)%(\s*\[[^]]*\])?\s*\{[^}]*$', + \ '\v\\PassOptionsToPackage\s*\{[^}]*\}\s*\{[^}]*$', + \ ], + \ 'candidates' : [], + \} + +function! s:completer_pck.complete(regex) dict abort " {{{2 + return s:filter_with_options(self.gather_candidates(), a:regex) +endfunction + +function! s:completer_pck.gather_candidates() dict abort " {{{2 + if empty(self.candidates) + let self.candidates = map(s:get_texmf_candidates('sty'), '{ + \ ''word'' : v:val, + \ ''kind'' : '' [package]'', + \}') + endif + + return copy(self.candidates) +endfunction + +" }}}1 +" {{{1 Documentclasses (\documentclass) + +let s:completer_doc = { + \ 'patterns' : ['\v\\documentclass%(\s*\[[^]]*\])?\s*\{[^}]*$'], + \ 'candidates' : [], + \} + +function! s:completer_doc.complete(regex) dict abort " {{{2 + return s:filter_with_options(self.gather_candidates(), a:regex) +endfunction + +function! s:completer_doc.gather_candidates() dict abort " {{{2 + if empty(self.candidates) + let self.candidates = map(s:get_texmf_candidates('cls'), '{ + \ ''word'' : v:val, + \ ''kind'' : '' [documentclass]'', + \}') + endif + + return copy(self.candidates) +endfunction + +" }}}1 +" {{{1 Bibliographystyles (\bibliographystyle) + +let s:completer_bst = { + \ 'patterns' : ['\v\\bibliographystyle\s*\{[^}]*$'], + \ 'candidates' : [], + \} + +function! s:completer_bst.complete(regex) dict abort " {{{2 + return s:filter_with_options(self.gather_candidates(), a:regex) +endfunction + +function! s:completer_bst.gather_candidates() dict abort " {{{2 + if empty(self.candidates) + let self.candidates = map(s:get_texmf_candidates('bst'), '{ + \ ''word'' : v:val, + \ ''kind'' : '' [bst files]'', + \}') + endif + + return copy(self.candidates) +endfunction + +" }}}1 + +" +" Functions to parse candidates from packages +" +function! s:get_packages() abort " {{{1 + let l:packages = [ + \ 'default', + \ 'class-' . get(b:vimtex, 'documentclass', ''), + \ ] + keys(b:vimtex.packages) + + call vimtex#paths#pushd(s:complete_dir) + + let l:missing = filter(copy(l:packages), '!filereadable(v:val)') + call filter(l:packages, 'filereadable(v:val)') + + " Parse include statements in complete files + let l:queue = copy(l:packages) + while !empty(l:queue) + let l:current = remove(l:queue, 0) + let l:includes = filter(readfile(l:current), 'v:val =~# ''^\#\s*include:''') + if empty(l:includes) | continue | endif + + call map(l:includes, 'matchstr(v:val, ''include:\s*\zs.*\ze\s*$'')') + let l:missing += filter(filter(copy(l:includes), + \ '!filereadable(v:val)'), + \ 'index(l:missing, v:val) < 0') + call filter(l:includes, 'filereadable(v:val)') + call filter(l:includes, 'index(l:packages, v:val) < 0') + + let l:packages += l:includes + let l:queue += l:includes + endwhile + + call vimtex#paths#popd() + + return l:packages + l:missing +endfunction + +" }}}1 +function! s:load_from_package(pkg, type) abort " {{{1 + let s:pkg_cache = get(s:, 'pkg_cache', + \ vimtex#cache#open('pkgcomplete', {'default': {}})) + let l:current = s:pkg_cache.get(a:pkg) + + let l:pkg_file = s:complete_dir . '/' . a:pkg + if filereadable(l:pkg_file) + if !has_key(l:current, 'candidates') + let s:pkg_cache.modified = 1 + let l:current.candidates + \ = s:_load_candidates_from_complete_file(a:pkg, l:pkg_file) + endif + else + if !has_key(l:current, 'candidates') + let s:pkg_cache.modified = 1 + let l:current.candidates = {'cmd': [], 'env': []} + endif + + let l:filename = a:pkg =~# '^class-' + \ ? vimtex#kpsewhich#find(a:pkg[6:] . '.cls') + \ : vimtex#kpsewhich#find(a:pkg . '.sty') + + let l:ftime = getftime(l:filename) + if l:ftime > get(l:current, 'ftime', -1) + let s:pkg_cache.modified = 1 + let l:current.ftime = l:ftime + let l:current.candidates = s:_load_candidates_from_source( + \ readfile(l:filename), a:pkg) + endif + endif + + " Write cache to file + call s:pkg_cache.write() + + return copy(l:current.candidates[a:type]) +endfunction + +" }}}1 +function! s:load_from_document(type) abort " {{{1 + let s:pkg_cache = get(s:, 'pkg_cache', + \ vimtex#cache#open('pkgcomplete', {'default': {}})) + + let l:ftime = b:vimtex.getftime() + if l:ftime < 0 | return [] | endif + + let l:current = s:pkg_cache.get(sha256(b:vimtex.tex)) + if l:ftime > get(l:current, 'ftime', -1) + let l:current.ftime = l:ftime + let l:current.candidates = s:_load_candidates_from_source( + \ vimtex#parser#tex(b:vimtex.tex, {'detailed' : 0}), + \ 'local') + + " Write cache to file + let s:pkg_cache.modified = 1 + call s:pkg_cache.write() + endif + + return copy(l:current.candidates[a:type]) +endfunction + +" }}}1 +function! s:_load_candidates_from_complete_file(pkg, pkgfile) abort " {{{1 + let l:result = {'cmd': [], 'env': []} + let l:lines = readfile(a:pkgfile) + + let l:candidates = filter(copy(l:lines), 'v:val =~# ''^\a''') + call map(l:candidates, 'split(v:val)') + call map(l:candidates, '{ + \ ''word'' : v:val[0], + \ ''mode'' : ''.'', + \ ''kind'' : ''[cmd: '' . a:pkg . ''] '', + \ ''menu'' : (get(v:val, 1, '''')), + \}') + let l:result.cmd += l:candidates + + let l:candidates = filter(l:lines, 'v:val =~# ''^\\begin{''') + call map(l:candidates, '{ + \ ''word'' : substitute(v:val, ''^\\begin{\|}$'', '''', ''g''), + \ ''mode'' : ''.'', + \ ''kind'' : ''[env: '' . a:pkg . ''] '', + \}') + let l:result.env += l:candidates + + return l:result +endfunction + +" }}}1 +function! s:_load_candidates_from_source(lines, pkg) abort " {{{1 + return { + \ 'cmd': + \ s:gather_candidates_from_newcommands( + \ copy(a:lines), 'cmd: ' . a:pkg), + \ 'env': + \ s:gather_candidates_from_newenvironments( + \ a:lines, 'env: ' . a:pkg) + \} +endfunction + +" }}}1 + +function! s:gather_candidates_from_newcommands(lines, label) abort " {{{1 + " Arguments: + " a:lines Lines of TeX that may contain \newcommands (or some variant, + " e.g. as provided by xparse and standard declaration) + " a:label Label to use in the menu + + call filter(a:lines, 'v:val =~# ''\v\\((provide|renew|new)command|(New|Declare|Provide|Renew)(Expandable)?DocumentCommand)''') + call map(a:lines, '{ + \ ''word'' : matchstr(v:val, ''\v\\((provide|renew|new)command|(New|Declare|Provide|Renew)(Expandable)?DocumentCommand)\*?\{\\?\zs[^}]*''), + \ ''mode'' : ''.'', + \ ''kind'' : ''['' . a:label . '']'', + \ }') + + return a:lines +endfunction + +" }}}1 +function! s:gather_candidates_from_newenvironments(lines, label) abort " {{{1 + " Arguments: + " a:lines Lines of TeX that may contain \newenvironments (or some + " variant, e.g. as provided by xparse and standard declaration) + " a:label Label to use in the menu + + call filter(a:lines, 'v:val =~# ''\v\\((renew|new)environment|(New|Renew|Provide|Declare)DocumentEnvironment)''') + call map(a:lines, '{ + \ ''word'' : matchstr(v:val, ''\v\\((renew|new)environment|(New|Renew|Provide|Declare)DocumentEnvironment)\*?\{\\?\zs[^}]*''), + \ ''mode'' : ''.'', + \ ''kind'' : ''['' . a:label . '']'', + \ }') + + return a:lines +endfunction + +" }}}1 + + +" +" Utility functions +" +function! s:filter_with_options(input, regex, ...) abort " {{{1 + if empty(a:input) | return a:input | endif + + let l:opts = a:0 > 0 ? a:1 : {} + let l:expression = type(a:input[0]) == type({}) + \ ? get(l:opts, 'filter_by_menu') ? 'v:val.menu' : 'v:val.word' + \ : 'v:val' + + if g:vimtex_complete_ignore_case && (!g:vimtex_complete_smart_case || a:regex !~# '\u') + let l:expression .= ' =~? ' + else + let l:expression .= ' =~# ' + endif + + if get(l:opts, 'anchor', 1) + let l:expression .= '''^'' . ' + endif + + let l:expression .= 'a:regex' + + return filter(a:input, l:expression) +endfunction + +" }}}1 +function! s:get_texmf_candidates(filetype) abort " {{{1 + let l:candidates = [] + + let l:texmfhome = $TEXMFHOME + if empty(l:texmfhome) + let l:texmfhome = get(vimtex#kpsewhich#run('--var-value TEXMFHOME'), 0, '') + endif + + " Add locally installed candidates first + if !empty(l:texmfhome) + let l:candidates += glob(l:texmfhome . '/**/*.' . a:filetype, 0, 1) + call map(l:candidates, 'fnamemodify(v:val, '':t:r'')') + endif + + " Then add globally available candidates (based on ls-R files) + for l:file in vimtex#kpsewhich#run('--all ls-R') + let l:candidates += map(filter(readfile(l:file), + \ 'v:val =~# ''\.' . a:filetype . ''''), + \ 'fnamemodify(v:val, '':r'')') + endfor + + return l:candidates +endfunction + +" }}}1 +function! s:close_braces(candidates) abort " {{{1 + if strpart(getline('.'), col('.') - 1) !~# '^\s*[,}]' + for l:cand in a:candidates + if !has_key(l:cand, 'abbr') + let l:cand.abbr = l:cand.word + endif + let l:cand.word = substitute(l:cand.word, '}*$', '}', '') + endfor + endif + + return a:candidates +endfunction + +" }}}1 + + +" +" Initialize module +" +let s:completers = map( + \ filter(items(s:), 'v:val[0] =~# ''^completer_'''), + \ 'v:val[1]') + +let s:complete_dir = fnamemodify(expand('<sfile>'), ':r') . '/' + +endif |