mirror of
https://github.com/sheerun/vim-polyglot.git
synced 2025-11-08 11:33:52 -05:00
1090 lines
31 KiB
VimL
1090 lines
31 KiB
VimL
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
|