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#state#init_buffer() abort " {{{1 command! -buffer VimtexToggleMain call vimtex#state#toggle_main() command! -buffer VimtexReloadState call vimtex#state#reload() nnoremap (vimtex-toggle-main) :VimtexToggleMain nnoremap (vimtex-reload-state) :VimtexReloadState endfunction " }}}1 function! vimtex#state#init() abort " {{{1 let [l:main, l:main_type] = s:get_main() let l:id = s:get_main_id(l:main) if l:id >= 0 let b:vimtex_id = l:id let b:vimtex = s:vimtex_states[l:id] else let b:vimtex_id = s:vimtex_next_id let b:vimtex = s:vimtex.new(l:main, l:main_type, 0) let s:vimtex_next_id += 1 let s:vimtex_states[b:vimtex_id] = b:vimtex endif endfunction " }}}1 function! vimtex#state#init_local() abort " {{{1 let l:filename = expand('%:p') let l:preserve_root = get(s:, 'subfile_preserve_root') unlet! s:subfile_preserve_root if b:vimtex.tex ==# l:filename | return | endif let l:vimtex_id = s:get_main_id(l:filename) if l:vimtex_id < 0 let l:vimtex_id = s:vimtex_next_id let l:vimtex = s:vimtex.new(l:filename, 'local file', l:preserve_root) let s:vimtex_next_id += 1 let s:vimtex_states[l:vimtex_id] = l:vimtex if !has_key(b:vimtex, 'subids') let b:vimtex.subids = [] endif call add(b:vimtex.subids, l:vimtex_id) let l:vimtex.main_id = b:vimtex_id endif let b:vimtex_local = { \ 'active' : 0, \ 'main_id' : b:vimtex_id, \ 'sub_id' : l:vimtex_id, \} endfunction " }}}1 function! vimtex#state#reload() abort " {{{1 let l:id = s:get_main_id(expand('%:p')) if has_key(s:vimtex_states, l:id) let l:vimtex = remove(s:vimtex_states, l:id) call l:vimtex.cleanup() endif if has_key(s:vimtex_states, get(b:, 'vimtex_id', -1)) let l:vimtex = remove(s:vimtex_states, b:vimtex_id) call l:vimtex.cleanup() endif call vimtex#state#init() call vimtex#state#init_local() endfunction " }}}1 function! vimtex#state#toggle_main() abort " {{{1 if exists('b:vimtex_local') let b:vimtex_local.active = !b:vimtex_local.active let b:vimtex_id = b:vimtex_local.active \ ? b:vimtex_local.sub_id \ : b:vimtex_local.main_id let b:vimtex = vimtex#state#get(b:vimtex_id) call vimtex#log#info('Changed to `' . b:vimtex.base . "' " \ . (b:vimtex_local.active ? '[local]' : '[main]')) endif endfunction " }}}1 function! vimtex#state#list_all() abort " {{{1 return values(s:vimtex_states) endfunction " }}}1 function! vimtex#state#exists(id) abort " {{{1 return has_key(s:vimtex_states, a:id) endfunction " }}}1 function! vimtex#state#get(id) abort " {{{1 return s:vimtex_states[a:id] endfunction " }}}1 function! vimtex#state#get_all() abort " {{{1 return s:vimtex_states endfunction " }}}1 function! vimtex#state#cleanup(id) abort " {{{1 if !vimtex#state#exists(a:id) | return | endif " " Count the number of open buffers for the given blob " let l:buffers = filter(range(1, bufnr('$')), 'buflisted(v:val)') let l:ids = map(l:buffers, 'getbufvar(v:val, ''vimtex_id'', -1)') let l:count = count(l:ids, a:id) " " Don't clean up if there are more than one buffer connected to the current " blob " if l:count > 1 | return | endif let l:vimtex = vimtex#state#get(a:id) " " Handle possible subfiles properly " if has_key(l:vimtex, 'subids') let l:subcount = 0 for l:sub_id in get(l:vimtex, 'subids', []) let l:subcount += count(l:ids, l:sub_id) endfor if l:count + l:subcount > 1 | return | endif for l:sub_id in get(l:vimtex, 'subids', []) call remove(s:vimtex_states, l:sub_id).cleanup() endfor call remove(s:vimtex_states, a:id).cleanup() else call remove(s:vimtex_states, a:id).cleanup() if has_key(l:vimtex, 'main_id') let l:main = vimtex#state#get(l:vimtex.main_id) let l:count_main = count(l:ids, l:vimtex.main_id) for l:sub_id in get(l:main, 'subids', []) let l:count_main += count(l:ids, l:sub_id) endfor if l:count_main + l:count <= 1 call remove(s:vimtex_states, l:vimtex.main_id).cleanup() endif endif endif endfunction " }}}1 function! s:get_main_id(main) abort " {{{1 for [l:id, l:state] in items(s:vimtex_states) if l:state.tex == a:main return str2nr(l:id) endif endfor return -1 endfunction function! s:get_main() abort " {{{1 if exists('s:disabled_modules') unlet s:disabled_modules endif " " Use buffer variable if it exists " if exists('b:vimtex_main') && filereadable(b:vimtex_main) return [fnamemodify(b:vimtex_main, ':p'), 'buffer variable'] endif " " Search for TEX root specifier at the beginning of file. This is used by " several other plugins and editors. " let l:candidate = s:get_main_from_texroot() if !empty(l:candidate) return [l:candidate, 'texroot specifier'] endif " " Check if the current file is a main file " if s:file_is_main(expand('%:p')) return [expand('%:p'), 'current file verified'] endif " " Support for subfiles package " let l:candidate = s:get_main_from_subfile() if !empty(l:candidate) return [l:candidate, 'subfiles'] endif " " Search for .latexmain-specifier " let l:candidate = s:get_main_latexmain(expand('%:p')) if !empty(l:candidate) return [l:candidate, 'latexmain specifier'] endif " " Search for .latexmkrc @default_files specifier " let l:candidate = s:get_main_latexmk() if !empty(l:candidate) return [l:candidate, 'latexmkrc @default_files'] endif " " Check if we are class or style file " if index(['cls', 'sty'], expand('%:e')) >= 0 let l:id = getbufvar('#', 'vimtex_id', -1) if l:id >= 0 && has_key(s:vimtex_states, l:id) return [s:vimtex_states[l:id].tex, 'cls/sty file (inherit from alternate)'] else let s:disabled_modules = ['latexmk', 'view', 'toc'] return [expand('%:p'), 'cls/sty file'] endif endif " " Search for main file recursively through include specifiers " if !get(g:, 'vimtex_disable_recursive_main_file_detection', 0) let l:candidate = s:get_main_choose(s:get_main_recurse()) if !empty(l:candidate) return [l:candidate, 'recursive search'] endif endif " " Use fallback candidate or the current file " let l:candidate = get(s:, 'cand_fallback', expand('%:p')) if exists('s:cand_fallback') unlet s:cand_fallback return [l:candidate, 'fallback'] else return [l:candidate, 'current file'] endif endfunction " }}}1 function! s:get_main_from_texroot() abort " {{{1 for l:line in getline(1, 5) let l:file_pattern = matchstr(l:line, g:vimtex#re#tex_input_root) if empty(l:file_pattern) | continue | endif if !vimtex#paths#is_abs(l:file_pattern) let l:file_pattern = simplify(expand('%:p:h') . '/' . l:file_pattern) endif let l:candidates = glob(l:file_pattern, 0, 1) if len(l:candidates) > 1 return s:get_main_choose(l:candidates) elseif len(l:candidates) == 1 return l:candidates[0] endif endfor return '' endfunction " }}}1 function! s:get_main_from_subfile() abort " {{{1 for l:line in getline(1, 5) let l:filename = matchstr(l:line, \ '^\C\s*\\documentclass\[\zs.*\ze\]{subfiles}') if len(l:filename) > 0 if l:filename !~# '\.tex$' let l:filename .= '.tex' endif if vimtex#paths#is_abs(l:filename) " Specified path is absolute if filereadable(l:filename) | return l:filename | endif else " Try specified path as relative to current file path let l:candidate = simplify(expand('%:p:h') . '/' . l:filename) if filereadable(l:candidate) | return l:candidate | endif " Try specified path as relative to the project main file. This is " difficult, since the main file is the one we are looking for. We " therefore assume that the main file lives somewhere upwards in the " directory tree. let l:candidate = fnamemodify(findfile(l:filename, '.;'), ':p') if filereadable(l:candidate) \ && s:file_reaches_current(l:candidate) let s:subfile_preserve_root = 1 return fnamemodify(candidate, ':p') endif " Check the alternate buffer. This seems sensible e.g. in cases where one " enters an "outer" subfile through a 'gf' motion from the main file. let l:vimtex = getbufvar('#', 'vimtex', {}) for l:file in get(l:vimtex, 'sources', []) if expand('%:p') ==# simplify(l:vimtex.root . '/' . l:file) let s:subfile_preserve_root = 1 return l:vimtex.tex endif endfor endif endif endfor return '' endfunction " }}}1 function! s:get_main_latexmain(file) abort " {{{1 for l:cand in s:findfiles_recursive('*.latexmain', expand('%:p:h')) let l:cand = fnamemodify(l:cand, ':p:r') if s:file_reaches_current(l:cand) return l:cand else let s:cand_fallback = l:cand endif endfor return '' endfunction function! s:get_main_latexmk() abort " {{{1 let l:root = expand('%:p:h') let l:results = vimtex#compiler#latexmk#get_rc_opt( \ l:root, 'default_files', 2, []) if l:results[1] < 1 | return '' | endif for l:candidate in l:results[0] let l:file = l:root . '/' . l:candidate if filereadable(l:file) return l:file endif endfor return '' endfunction function! s:get_main_recurse(...) abort " {{{1 " Either start the search from the original file, or check if the supplied " file is a main file (or invalid) if a:0 == 0 let l:file = expand('%:p') let l:tried = {} else let l:file = a:1 let l:tried = a:2 if s:file_is_main(l:file) return [l:file] elseif !filereadable(l:file) return [] endif endif " Create list of candidates that was already tried for the current file if !has_key(l:tried, l:file) let l:tried[l:file] = [l:file] endif " Apply filters successively (minor optimization) let l:re_filter1 = fnamemodify(l:file, ':t:r') let l:re_filter2 = g:vimtex#re#tex_input . '\s*\f*' . l:re_filter1 " Search through candidates found recursively upwards in the directory tree let l:results = [] for l:cand in s:findfiles_recursive('*.tex', fnamemodify(l:file, ':p:h')) if index(l:tried[l:file], l:cand) >= 0 | continue | endif call add(l:tried[l:file], l:cand) if len(filter(filter(readfile(l:cand), \ 'v:val =~# l:re_filter1'), \ 'v:val =~# l:re_filter2')) > 0 let l:results += s:get_main_recurse(fnamemodify(l:cand, ':p'), l:tried) endif endfor return l:results endfunction " }}}1 function! s:get_main_choose(list) abort " {{{1 let l:list = vimtex#util#uniq_unsorted(a:list) if empty(l:list) | return '' | endif if len(l:list) == 1 | return l:list[0] | endif let l:all = map(copy(l:list), '[s:get_main_id(v:val), v:val]') let l:new = map(filter(copy(l:all), 'v:val[0] < 0'), 'v:val[1]') let l:existing = {} for [l:key, l:val] in filter(copy(l:all), 'v:val[0] >= 0') let l:existing[l:key] = l:val endfor let l:alternate_id = getbufvar('#', 'vimtex_id', -1) if len(l:existing) == 1 return values(l:existing)[0] elseif len(l:existing) > 1 && has_key(l:existing, l:alternate_id) return l:existing[l:alternate_id] elseif len(l:existing) < 1 && len(l:new) == 1 return l:new[0] else let l:choices = {} for l:tex in l:list let l:choices[l:tex] = vimtex#paths#relative(l:tex, getcwd()) endfor return vimtex#echo#choose(l:choices, \ 'Please select an appropriate main file:') endif endfunction " }}}1 function! s:file_is_main(file) abort " {{{1 if !filereadable(a:file) | return 0 | endif " " Check if a:file is a main file by looking for the \documentclass command, " but ignore the following: " " \documentclass[...]{subfiles} " \documentclass[...]{standalone} " let l:lines = readfile(a:file, 0, 50) call filter(l:lines, 'v:val =~# ''\C\\documentclass\_\s*[\[{]''') call filter(l:lines, 'v:val !~# ''{subfiles}''') call filter(l:lines, 'v:val !~# ''{standalone}''') if len(l:lines) == 0 | return 0 | endif " A main file contains `\begin{document}` let l:lines = vimtex#parser#preamble(a:file, { \ 'inclusive' : 1, \ 'root' : fnamemodify(a:file, ':p:h'), \}) call filter(l:lines, 'v:val =~# ''\\begin\s*{document}''') return len(l:lines) > 0 endfunction " }}}1 function! s:file_reaches_current(file) abort " {{{1 " Note: This function assumes that the input a:file is an absolute path if !filereadable(a:file) | return 0 | endif for l:line in filter(readfile(a:file), 'v:val =~# g:vimtex#re#tex_input') let l:file = matchstr(l:line, g:vimtex#re#tex_input . '\zs\f+') if empty(l:file) | continue | endif if !vimtex#paths#is_abs(l:file) let l:file = fnamemodify(a:file, ':h') . '/' . l:file endif if l:file !~# '\.tex$' let l:file .= '.tex' endif if expand('%:p') ==# l:file \ || s:file_reaches_current(l:file) return 1 endif endfor return 0 endfunction " }}}1 function! s:findfiles_recursive(expr, path) abort " {{{1 let l:path = a:path let l:dirs = l:path while l:path != fnamemodify(l:path, ':h') let l:path = fnamemodify(l:path, ':h') let l:dirs .= ',' . l:path endwhile return split(globpath(fnameescape(l:dirs), a:expr), '\n') endfunction " }}}1 let s:vimtex = {} function! s:vimtex.new(main, main_parser, preserve_root) abort dict " {{{1 let l:new = deepcopy(self) let l:new.tex = a:main let l:new.root = fnamemodify(l:new.tex, ':h') let l:new.base = fnamemodify(l:new.tex, ':t') let l:new.name = fnamemodify(l:new.tex, ':t:r') let l:new.main_parser = a:main_parser if a:preserve_root && exists('b:vimtex') let l:new.root = b:vimtex.root let l:new.base = vimtex#paths#relative(a:main, l:new.root) endif if exists('s:disabled_modules') let l:new.disabled_modules = s:disabled_modules endif " " The preamble content is used to parse for the engine directive, the " documentclass and the package list; we store it as a temporary shared " object variable " let l:new.preamble = vimtex#parser#preamble(l:new.tex, { \ 'root' : l:new.root, \}) call l:new.parse_tex_program() call l:new.parse_documentclass() call l:new.parse_graphicspath() call l:new.gather_sources() call vimtex#view#init_state(l:new) call vimtex#compiler#init_state(l:new) call vimtex#qf#init_state(l:new) call vimtex#toc#init_state(l:new) call vimtex#fold#init_state(l:new) " Parsing packages might depend on the compiler setting for build_dir call l:new.parse_packages() unlet l:new.preamble unlet l:new.new return l:new endfunction " }}}1 function! s:vimtex.cleanup() abort dict " {{{1 if exists('self.compiler.cleanup') call self.compiler.cleanup() endif if exists('#User#VimtexEventQuit') if exists('b:vimtex') let b:vimtex_tmp = b:vimtex endif let b:vimtex = self doautocmd User VimtexEventQuit if exists('b:vimtex_tmp') let b:vimtex = b:vimtex_tmp unlet b:vimtex_tmp else unlet b:vimtex endif endif " Close quickfix window silent! cclose endfunction " }}}1 function! s:vimtex.parse_tex_program() abort dict " {{{1 let l:lines = copy(self.preamble[:20]) let l:tex_program_re = \ '\v^\c\s*\%\s*\!?\s*tex\s+%(TS-)?program\s*\=\s*\zs.*\ze\s*$' call map(l:lines, 'matchstr(v:val, l:tex_program_re)') call filter(l:lines, '!empty(v:val)') let self.tex_program = tolower(get(l:lines, -1, '_')) endfunction " }}}1 function! s:vimtex.parse_documentclass() abort dict " {{{1 let self.documentclass = '' for l:line in self.preamble let l:class = matchstr(l:line, '^\s*\\documentclass.*{\zs\w*\ze}') if !empty(l:class) let self.documentclass = l:class break endif endfor endfunction " }}}1 function! s:vimtex.parse_graphicspath() abort dict " {{{1 " Combine the preamble as one long string of commands let l:preamble = join(map(copy(self.preamble), \ 'substitute(v:val, ''\\\@= 2 call add(l:items, ['source files', self.sources]) endif call add(l:items, ['compiler', get(self, 'compiler', {})]) call add(l:items, ['viewer', get(self, 'viewer', {})]) call add(l:items, ['qf', get(self, 'qf', {})]) if exists('self.documentclass') call add(l:items, ['document class', self.documentclass]) endif if !empty(self.packages) call add(l:items, ['packages', sort(keys(self.packages))]) endif return [['vimtex project', l:items]] endfunction " }}}1 function! s:vimtex.log() abort dict " {{{1 return self.ext('log') endfunction " }}}1 function! s:vimtex.aux() abort dict " {{{1 return self.ext('aux') endfunction " }}}1 function! s:vimtex.fls() abort dict " {{{1 return self.ext('fls') endfunction " }}}1 function! s:vimtex.out(...) abort dict " {{{1 return call(self.ext, ['pdf'] + a:000, self) endfunction " }}}1 function! s:vimtex.ext(ext, ...) abort dict " {{{1 " First check build dir (latexmk -output_directory option) if !empty(get(get(self, 'compiler', {}), 'build_dir', '')) let cand = self.compiler.build_dir . '/' . self.name . '.' . a:ext if !vimtex#paths#is_abs(self.compiler.build_dir) let cand = self.root . '/' . cand endif if a:0 > 0 || filereadable(cand) return fnamemodify(cand, ':p') endif endif " Next check for file in project root folder let cand = self.root . '/' . self.name . '.' . a:ext if a:0 > 0 || filereadable(cand) return fnamemodify(cand, ':p') endif " Finally return empty string if no entry is found return '' endfunction " }}}1 function! s:vimtex.getftime() abort dict " {{{1 return max(map(copy(self.sources), 'getftime(self.root . ''/'' . v:val)')) endfunction " }}}1 " Initialize module let s:vimtex_states = {} let s:vimtex_next_id = 0 endif