diff options
Diffstat (limited to 'autoload/vimtex/state.vim')
-rw-r--r-- | autoload/vimtex/state.vim | 745 |
1 files changed, 745 insertions, 0 deletions
diff --git a/autoload/vimtex/state.vim b/autoload/vimtex/state.vim new file mode 100644 index 00000000..98019f89 --- /dev/null +++ b/autoload/vimtex/state.vim @@ -0,0 +1,745 @@ +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 <buffer> <plug>(vimtex-toggle-main) :VimtexToggleMain<cr> + nnoremap <buffer> <plug>(vimtex-reload-state) :VimtexReloadState<cr> +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 <nomodeline> 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, ''\\\@<!%.*'', '''', '''')')) + + " Extract the graphicspath command from this string + let l:graphicspath = matchstr(l:preamble, + \ g:vimtex#re#not_bslash + \ . '\\graphicspath\s*\{\s*\{\s*\zs.{-}\ze\s*\}\s*\}' + \) + + " Add all parsed graphicspaths + let self.graphicspath = [] + for l:path in split(l:graphicspath, '\s*}\s*{\s*') + let l:path = substitute(l:path, '\/\s*$', '', '') + call add(self.graphicspath, vimtex#paths#is_abs(l:path) + \ ? l:path + \ : simplify(self.root . '/' . l:path)) + endfor +endfunction + +" }}}1 +function! s:vimtex.parse_packages() abort dict " {{{1 + let self.packages = get(self, 'packages', {}) + + " Try to parse .fls file if present, as it is usually more complete. That is, + " it contains a generated list of all the packages that are used. + for l:line in vimtex#parser#fls(self.fls()) + let l:package = matchstr(l:line, '^INPUT \zs.\+\ze\.sty$') + let l:package = fnamemodify(l:package, ':t') + if !empty(l:package) + let self.packages[l:package] = {} + endif + endfor + + " Now parse preamble as well for usepackage and RequirePackage + if !has_key(self, 'preamble') | return | endif + let l:usepackages = filter(copy(self.preamble), 'v:val =~# ''\v%(usep|RequireP)ackage''') + let l:pat = g:vimtex#re#not_comment . g:vimtex#re#not_bslash + \ . '\v\\%(usep|RequireP)ackage\s*%(\[[^[\]]*\])?\s*\{\s*\zs%([^{}]+)\ze\s*\}' + call map(l:usepackages, 'matchstr(v:val, l:pat)') + call map(l:usepackages, 'split(v:val, ''\s*,\s*'')') + + for l:packages in l:usepackages + for l:package in l:packages + let self.packages[l:package] = {} + endfor + endfor +endfunction + +" }}}1 +function! s:vimtex.gather_sources() abort dict " {{{1 + let self.sources = vimtex#parser#tex#parse_files( + \ self.tex, {'root' : self.root}) + + call map(self.sources, 'vimtex#paths#relative(v:val, self.root)') +endfunction + +" }}}1 +function! s:vimtex.pprint_items() abort dict " {{{1 + let l:items = [ + \ ['name', self.name], + \ ['base', self.base], + \ ['root', self.root], + \ ['tex', self.tex], + \ ['out', self.out()], + \ ['log', self.log()], + \ ['aux', self.aux()], + \ ['fls', self.fls()], + \ ['main parser', self.main_parser], + \] + + if self.tex_program !=# '_' + call add(l:items, ['tex program', self.tex_program]) + endif + + if len(self.sources) >= 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 |