diff options
Diffstat (limited to 'autoload/vimtex')
99 files changed, 16352 insertions, 0 deletions
diff --git a/autoload/vimtex/cache.vim b/autoload/vimtex/cache.vim new file mode 100644 index 00000000..a9ab8be1 --- /dev/null +++ b/autoload/vimtex/cache.vim @@ -0,0 +1,179 @@ +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#cache#open(name, ...) abort " {{{1 + let l:opts = a:0 > 0 ? a:1 : {} + let l:name = get(l:opts, 'local') ? s:local_name(a:name) : a:name + + let s:caches = get(s:, 'caches', {}) + if has_key(s:caches, l:name) + return s:caches[l:name] + endif + + let s:caches[l:name] = s:cache.init(l:name, l:opts) + return s:caches[l:name] +endfunction + +" }}}1 +function! vimtex#cache#close(name) abort " {{{1 + let s:caches = get(s:, 'caches', {}) + + " Try global name first, then local name + let l:name = a:name + if !has_key(s:caches, l:name) + let l:name = s:local_name(l:name) + endif + if !has_key(s:caches, l:name) | return | endif + + let l:cache = s:caches[l:name] + call l:cache.write() + unlet s:caches[l:name] +endfunction + +" }}}1 +function! vimtex#cache#wrap(Func, name, ...) abort " {{{1 + if !has('lambda') + throw 'error: vimtex#cache#wrap requires +lambda' + endif + + let l:opts = a:0 > 0 ? a:1 : {} + let l:cache = vimtex#cache#open(a:name, l:opts) + + function! CachedFunc(key) closure + if l:cache.has(a:key) + return l:cache.get(a:key) + else + return l:cache.set(a:key, a:Func(a:key)) + endif + endfunction + + return function('CachedFunc') +endfunction + +" }}}1 +function! vimtex#cache#clear(name, local) abort " {{{1 + let l:cache = vimtex#cache#open(a:name, {'local': a:local}) + + call l:cache.read() + if !empty(l:cache.data) + let l:cache.data = {} + call l:cache.write() + endif +endfunction + +" }}}1 +function! vimtex#cache#write_all() abort " {{{1 + for l:cache in values(get(s:, 'caches', {})) + call l:cache.write() + endfor +endfunction + +" }}}1 + +let s:cache = {} + +function! s:cache.init(name, opts) dict abort " {{{1 + let new = deepcopy(self) + unlet new.init + + let l:root = get(g:, 'vimtex_cache_root', $HOME . '/.cache/vimtex') + if !isdirectory(l:root) + call mkdir(l:root, 'p') + endif + + let new.name = a:name + let new.path = l:root . '/' . a:name . '.json' + let new.local = get(a:opts, 'local') + let new.persistent = get(a:opts, 'persistent', + \ get(g:, 'vimtex_cache_persistent', 1)) + + if has_key(a:opts, 'default') + let new.default = a:opts.default + endif + + let new.data = {} + let new.ftime = -1 + let new.modified = 0 + + return new +endfunction + +" }}}1 +function! s:cache.get(key) dict abort " {{{1 + call self.read() + + if has_key(self, 'default') && !has_key(self.data, a:key) + let self.data[a:key] = deepcopy(self.default) + endif + + return get(self.data, a:key) +endfunction + +" }}}1 +function! s:cache.has(key) dict abort " {{{1 + call self.read() + + return has_key(self.data, a:key) +endfunction + +" }}}1 +function! s:cache.set(key, value) dict abort " {{{1 + call self.read() + + let self.data[a:key] = a:value + let self.modified = 1 + call self.write() + + return a:value +endfunction + +" }}}1 +function! s:cache.write() dict abort " {{{1 + if !self.persistent + let self.modified = 0 + return + endif + + if !self.modified | return | endif + + call self.read() + call writefile([json_encode(self.data)], self.path) + let self.ftime = getftime(self.path) + let self.modified = 0 +endfunction + +" }}}1 +function! s:cache.read() dict abort " {{{1 + if !self.persistent | return | endif + + if getftime(self.path) > self.ftime + let self.ftime = getftime(self.path) + call extend(self.data, + \ json_decode(join(readfile(self.path))), 'keep') + endif +endfunction + +" }}}1 + +" +" Utility functions +" +function! s:local_name(name) abort " {{{1 + let l:filename = exists('b:vimtex.tex') + \ ? fnamemodify(b:vimtex.tex, ':r') + \ : expand('%:p:r') + let l:filename = substitute(l:filename, '\s\+', '_', 'g') + let l:filename = substitute(l:filename, '\/', '%', 'g') + let l:filename = substitute(l:filename, '\\', '%', 'g') + let l:filename = substitute(l:filename, ':', '%', 'g') + return a:name . l:filename +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/cmd.vim b/autoload/vimtex/cmd.vim new file mode 100644 index 00000000..06b5e14c --- /dev/null +++ b/autoload/vimtex/cmd.vim @@ -0,0 +1,718 @@ +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#cmd#init_buffer() abort " {{{1 + nnoremap <silent><buffer> <plug>(vimtex-cmd-delete) + \ :<c-u>call <sid>operator_setup('delete')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-cmd-change) + \ :<c-u>call <sid>operator_setup('change')<bar>normal! g@l<cr> + + inoremap <silent><buffer> <plug>(vimtex-cmd-create) + \ <c-r>=vimtex#cmd#create_insert()<cr> + + nnoremap <silent><buffer> <plug>(vimtex-cmd-create) + \ :<c-u>call <sid>operator_setup('create')<bar>normal! g@l<cr> + + xnoremap <silent><buffer> <plug>(vimtex-cmd-create) + \ :<c-u>call vimtex#cmd#create_visual()<cr> + + nnoremap <silent><buffer> <plug>(vimtex-cmd-toggle-star) + \ :<c-u>call <sid>operator_setup('toggle_star')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-cmd-toggle-frac) + \ :<c-u>call <sid>operator_setup('toggle_frac')<bar>normal! g@l<cr> + + xnoremap <silent><buffer> <plug>(vimtex-cmd-toggle-frac) + \ :<c-u>call vimtex#cmd#toggle_frac_visual()<cr> +endfunction + +" }}}1 + +function! vimtex#cmd#change(new_name) abort " {{{1 + let l:cmd = vimtex#cmd#get_current() + if empty(l:cmd) | return | endif + + let l:old_name = l:cmd.name + let l:lnum = l:cmd.pos_start.lnum + let l:cnum = l:cmd.pos_start.cnum + + " Get new command name + let l:new_name = substitute(a:new_name, '^\\', '', '') + if empty(l:new_name) | return | endif + + " Update current position + let l:save_pos = vimtex#pos#get_cursor() + if strlen(l:new_name) < strlen(l:old_name) + let l:col = searchpos('\\\k', 'bcnW')[1] + strlen(l:new_name) + if l:col < l:save_pos[2] + let l:save_pos[2] = l:col + endif + endif + + " Perform the change + let l:line = getline(l:lnum) + call setline(l:lnum, + \ strpart(l:line, 0, l:cnum) + \ . l:new_name + \ . strpart(l:line, l:cnum + strlen(l:old_name) - 1)) + + " Restore cursor position + cal vimtex#pos#set_cursor(l:save_pos) +endfunction + +function! vimtex#cmd#delete(...) abort " {{{1 + if a:0 > 0 + let l:cmd = call('vimtex#cmd#get_at', a:000) + else + let l:cmd = vimtex#cmd#get_current() + endif + if empty(l:cmd) | return | endif + + " Save current position + let l:save_pos = vimtex#pos#get_cursor() + let l:lnum_cur = l:save_pos[1] + let l:cnum_cur = l:save_pos[2] + + " Remove closing bracket (if exactly one argument) + if len(l:cmd.args) == 1 + let l:lnum = l:cmd.args[0].close.lnum + let l:cnum = l:cmd.args[0].close.cnum + let l:line = getline(l:lnum) + call setline(l:lnum, + \ strpart(l:line, 0, l:cnum - 1) + \ . strpart(l:line, l:cnum)) + + let l:cnum2 = l:cmd.args[0].open.cnum + endif + + " Remove command (and possibly the opening bracket) + let l:lnum = l:cmd.pos_start.lnum + let l:cnum = l:cmd.pos_start.cnum + let l:cnum2 = get(l:, 'cnum2', l:cnum + strlen(l:cmd.name) - 1) + let l:line = getline(l:lnum) + call setline(l:lnum, + \ strpart(l:line, 0, l:cnum - 1) + \ . strpart(l:line, l:cnum2)) + + " Restore appropriate cursor position + if l:lnum_cur == l:lnum + if l:cnum_cur > l:cnum2 + let l:save_pos[2] -= l:cnum2 - l:cnum + 1 + else + let l:save_pos[2] -= l:cnum_cur - l:cnum + endif + endif + cal vimtex#pos#set_cursor(l:save_pos) +endfunction + +function! vimtex#cmd#delete_all(...) abort " {{{1 + if a:0 > 0 + let l:cmd = call('vimtex#cmd#get_at', a:000) + else + let l:cmd = vimtex#cmd#get_current() + endif + if empty(l:cmd) | return | endif + + call vimtex#pos#set_cursor(l:cmd.pos_start) + normal! v + call vimtex#pos#set_cursor(l:cmd.pos_end) + normal! d +endfunction + +function! vimtex#cmd#create_insert() abort " {{{1 + if mode() !=# 'i' | return | endif + + let l:re = '\v%(^|\A)\zs\a+\ze%(\A|$)' + let l:c0 = col('.') - 1 + + let [l:l1, l:c1] = searchpos(l:re, 'bcn', line('.')) + let l:c1 -= 1 + let l:line = getline(l:l1) + let l:match = matchstr(l:line, l:re, l:c1) + let l:c2 = l:c1 + strlen(l:match) + + if l:c0 > l:c2 + call vimtex#log#warning('Could not create command') + return '' + endif + + let l:strpart1 = strpart(l:line, 0, l:c1) + let l:strpart2 = '\' . strpart(l:match, 0, l:c0 - l:c1) . '{' + let l:strpart3 = strpart(l:line, l:c0) + call setline(l:l1, l:strpart1 . l:strpart2 . l:strpart3) + + call vimtex#pos#set_cursor(l:l1, l:c2+3) + return '' +endfunction + +" }}}1 +function! vimtex#cmd#create(cmd, visualmode) abort " {{{1 + if empty(a:cmd) | return | endif + + " Avoid autoindent (disable indentkeys) + let l:save_indentkeys = &l:indentkeys + setlocal indentkeys= + + if a:visualmode + let l:pos_start = getpos("'<") + let l:pos_end = getpos("'>") + + if visualmode() ==# '' + normal! gvA} + execute 'normal! gvI\' . a:cmd . '{' + + let l:pos_end[2] += strlen(a:cmd) + 3 + else + normal! `>a} + normal! `< + execute 'normal! i\' . a:cmd . '{' + + let l:pos_end[2] += + \ l:pos_end[1] == l:pos_start[1] ? strlen(a:cmd) + 3 : 1 + endif + + call vimtex#pos#set_cursor(l:pos_end) + else + let l:pos = vimtex#pos#get_cursor() + let l:save_reg = getreg('"') + let l:pos[2] += strlen(a:cmd) + 2 + execute 'normal! ciw\' . a:cmd . '{"}' + call setreg('"', l:save_reg) + call vimtex#pos#set_cursor(l:pos) + endif + + " Restore indentkeys setting + let &l:indentkeys = l:save_indentkeys +endfunction + +" }}}1 +function! vimtex#cmd#create_visual() abort " {{{1 + let l:cmd = vimtex#echo#input({ + \ 'info' : + \ ['Create command: ', ['VimtexWarning', '(empty to cancel)']], + \}) + let l:cmd = substitute(l:cmd, '^\\', '', '') + call vimtex#cmd#create(l:cmd, 1) +endfunction + +" }}}1 +function! vimtex#cmd#toggle_star() abort " {{{1 + let l:cmd = vimtex#cmd#get_current() + if empty(l:cmd) | return | endif + + let l:old_name = l:cmd.name + let l:lnum = l:cmd.pos_start.lnum + let l:cnum = l:cmd.pos_start.cnum + + " Set new command name + if match(l:old_name, '\*$') == -1 + let l:new_name = l:old_name.'*' + else + let l:new_name = strpart(l:old_name, 0, strlen(l:old_name)-1) + endif + let l:new_name = substitute(l:new_name, '^\\', '', '') + if empty(l:new_name) | return | endif + + " Update current position + let l:save_pos = vimtex#pos#get_cursor() + let l:save_pos[2] += strlen(l:new_name) - strlen(l:old_name) + 1 + + " Perform the change + let l:line = getline(l:lnum) + call setline(l:lnum, + \ strpart(l:line, 0, l:cnum) + \ . l:new_name + \ . strpart(l:line, l:cnum + strlen(l:old_name) - 1)) + + " Restore cursor position + cal vimtex#pos#set_cursor(l:save_pos) +endfunction + +" }}}1 +function! vimtex#cmd#toggle_frac() abort " {{{1 + let l:frac = s:get_frac_cmd() + if empty(l:frac) + let l:frac = s:get_frac_inline() + endif + if empty(l:frac) | return | endif + + let l:lnum = line('.') + let l:line = getline(l:lnum) + call setline(l:lnum, + \ strpart(l:line, 0, l:frac.col_start) + \ . l:frac.text_toggled + \ . strpart(l:line, l:frac.col_end+1)) +endfunction + +" }}}1 +function! vimtex#cmd#toggle_frac_visual() abort " {{{1 + let l:save_reg = getreg('a') + normal! gv"ay + let l:selected = substitute(getreg('a'), '\n\s*', ' ', '') + call setreg('a', l:save_reg) + + let l:frac = s:get_frac_inline_visual(l:selected) + if empty(l:frac) + let l:frac = s:get_frac_cmd_visual(l:selected) + endif + + if empty(l:frac) | return | endif + + let l:save_reg = getreg('a') + call setreg('a', l:frac.text_toggled) + normal! gv"ap + call setreg('a', l:save_reg) +endfunction + +" }}}1 + +function! s:get_frac_cmd() abort " {{{1 + let l:save_pos = vimtex#pos#get_cursor() + while v:true + let l:cmd = s:get_cmd('prev') + if empty(l:cmd) || l:cmd.pos_start.lnum < line('.') + call vimtex#pos#set_cursor(l:save_pos) + return {} + endif + + if l:cmd.name ==# '\frac' + break + endif + + call vimtex#pos#set_cursor(vimtex#pos#prev(l:cmd.pos_start)) + endwhile + call vimtex#pos#set_cursor(l:save_pos) + + let l:frac = { + \ 'type': 'cmd', + \ 'col_start': l:cmd.pos_start.cnum - 1, + \ 'col_end': l:cmd.pos_end.cnum - 1, + \} + + if len(l:cmd.args) >= 2 + let l:consume = [] + let l:frac.denominator = l:cmd.args[0].text + let l:frac.numerator = l:cmd.args[1].text + elseif len(l:cmd.args) == 1 + let l:consume = ['numerator'] + let l:frac.denominator = l:cmd.args[0].text + let l:frac.numerator = '' + else + let l:consume = ['denominator', 'numerator'] + let l:frac.denominator = '' + let l:frac.numerator = '' + endif + + " Handle unfinished cases + let l:line = getline('.') + let l:pos = l:frac.col_end + 1 + for l:key in l:consume + let l:part = strpart(l:line, l:frac.col_end + 1) + + let l:blurp = matchstr(l:part, '^\s*{[^}]*}') + if !empty(l:blurp) + let l:frac[l:key] = vimtex#util#trim(l:blurp)[1:-2] + let l:frac.col_end += len(l:blurp) + continue + endif + + let l:blurp = matchstr(l:part, '^\s*\w') + if !empty(l:blurp) + let l:frac[l:key] = vimtex#util#trim(l:blurp) + let l:frac.col_end += len(l:blurp) + endif + endfor + + " Abort if \frac region does not cover cursor + if l:frac.col_end < col('.') | return {} | endif + + let l:frac.text = strpart(getline('.'), + \ l:frac.col_start, l:frac.col_end - l:frac.col_start + 1) + + return s:get_frac_cmd_aux(l:frac) +endfunction + +" }}}1 +function! s:get_frac_cmd_visual(selected) abort " {{{1 + let l:matches = matchlist(a:selected, '^\s*\\frac\s*{\(.*\)}\s*{\(.*\)}\s*$') + if empty(l:matches) | return {} | endif + + let l:frac = { + \ 'type': 'cmd', + \ 'text': a:selected, + \ 'denominator': l:matches[1], + \ 'numerator': l:matches[2], + \} + + return s:get_frac_cmd_aux(l:frac) +endfunction + +" }}}1 +function! s:get_frac_cmd_aux(frac) abort " {{{1 + let l:denominator = (a:frac.denominator =~# '^\\\?\w*$') + \ ? a:frac.denominator + \ : '(' . a:frac.denominator . ')' + + let l:numerator = (a:frac.numerator =~# '^\\\?\w*$') + \ ? a:frac.numerator + \ : '(' . a:frac.numerator . ')' + + let a:frac.text_toggled = l:denominator . '/' . l:numerator + + return a:frac +endfunction + +" }}}1 +function! s:get_frac_inline() abort " {{{1 + let l:line = getline('.') + let l:col = col('.') - 1 + + let l:pos_after = -1 + let l:pos_before = -1 + while v:true + let l:pos_before = l:pos_after + let l:pos_after = match(l:line, '\/', l:pos_after+1) + if l:pos_after < 0 || l:pos_after >= l:col | break | endif + endwhile + + if l:pos_after == -1 && l:pos_before == -1 + return {} + endif + + let l:positions = [] + if l:pos_before > 0 + let l:positions += [l:pos_before] + endif + if l:pos_after > 0 + let l:positions += [l:pos_after] + endif + + for l:pos in l:positions + let l:frac = {'type': 'inline'} + + " + " Parse numerator + " + let l:before = strpart(l:line, 0, l:pos) + if l:before =~# ')\s*$' + let l:pos_before = s:get_inline_limit(l:before, -1) - 1 + let l:parens = strpart(l:before, l:pos_before) + else + let l:pos_before = match(l:before, '\s*$') + let l:parens = '' + endif + + let l:before = strpart(l:line, 0, l:pos_before) + let l:atoms = matchstr(l:before, '\(\\(\)\?\zs[^-$(){} ]*$') + let l:pos_before = l:pos_before - strlen(l:atoms) + let l:frac.numerator = s:get_inline_trim(l:atoms . l:parens) + let l:frac.col_start = l:pos_before + + " + " Parse denominator + " + let l:after = strpart(l:line, l:pos+1) + let l:atoms = l:after =~# '^\s*[^$()} ]*\\)' + \ ? matchstr(l:after, '^\s*[^$()} ]*\ze\\)') + \ : matchstr(l:after, '^\s*[^$()} ]*') + let l:pos_after = l:pos + strlen(l:atoms) + let l:after = strpart(l:line, l:pos_after+1) + if l:after =~# '^(' + let l:index = s:get_inline_limit(l:after, 1) + let l:pos_after = l:pos_after + l:index + 1 + let l:parens = strpart(l:after, 0, l:index+1) + else + let l:parens = '' + endif + let l:frac.denominator = s:get_inline_trim(l:atoms . l:parens) + let l:frac.col_end = l:pos_after + + " + " Combine/Parse inline and frac expressions + " + let l:frac.text = strpart(l:line, + \ l:frac.col_start, + \ l:frac.col_end - l:frac.col_start + 1) + let l:frac.text_toggled = printf('\frac{%s}{%s}', + \ l:frac.numerator, l:frac.denominator) + + " + " Accept result if the range contains the cursor column + " + if l:col >= l:frac.col_start && l:col <= l:frac.col_end + return l:frac + endif + endfor + + return {} +endfunction + +" }}}1 +function! s:get_frac_inline_visual(selected) abort " {{{1 + let l:parts = split(a:selected, '/') + if len(l:parts) != 2 | return {} | endif + + let l:frac = { + \ 'type': 'inline', + \ 'text': a:selected, + \ 'numerator': s:get_inline_trim(l:parts[0]), + \ 'denominator': s:get_inline_trim(l:parts[1]), + \} + + let l:frac.text_toggled = printf('\frac{%s}{%s}', + \ l:frac.numerator, l:frac.denominator) + + return l:frac +endfunction + +" }}}1 +function! s:get_inline_limit(str, dir) abort " {{{1 + if a:dir > 0 + let l:open = '(' + let l:string = a:str + else + let l:open = ')' + let l:string = join(reverse(split(a:str, '\zs')), '') + endif + + let idx = -1 + let depth = 0 + + while idx < len(l:string) + let idx = match(l:string, '[()]', idx + 1) + if idx < 0 + let idx = len(l:string) + endif + if idx >= len(l:string) || l:string[idx] ==# l:open + let depth += 1 + else + let depth -= 1 + if depth == 0 + return a:dir < 0 ? len(a:str) - idx : idx + endif + endif + endwhile + + return -1 +endfunction + +" }}}1 +function! s:get_inline_trim(str) abort " {{{1 + let l:str = vimtex#util#trim(a:str) + return substitute(l:str, '^(\(.*\))$', '\1', '') +endfunction + +" }}}1 + +function! vimtex#cmd#get_next() abort " {{{1 + return s:get_cmd('next') +endfunction + +" }}}1 +function! vimtex#cmd#get_prev() abort " {{{1 + return s:get_cmd('prev') +endfunction + +" }}}1 +function! vimtex#cmd#get_current() abort " {{{1 + let l:save_pos = vimtex#pos#get_cursor() + let l:pos_val_cursor = vimtex#pos#val(l:save_pos) + + let l:depth = 3 + while l:depth > 0 + let l:depth -= 1 + let l:cmd = s:get_cmd('prev') + if empty(l:cmd) | break | endif + + let l:pos_val = vimtex#pos#val(l:cmd.pos_end) + if l:pos_val >= l:pos_val_cursor + call vimtex#pos#set_cursor(l:save_pos) + return l:cmd + else + call vimtex#pos#set_cursor(vimtex#pos#prev(l:cmd.pos_start)) + endif + endwhile + + call vimtex#pos#set_cursor(l:save_pos) + + return {} +endfunction + +" }}}1 +function! vimtex#cmd#get_at(...) abort " {{{1 + let l:pos_saved = vimtex#pos#get_cursor() + call call('vimtex#pos#set_cursor', a:000) + let l:cmd = vimtex#cmd#get_current() + call vimtex#pos#set_cursor(l:pos_saved) + return l:cmd +endfunction + +" }}}1 + +function! s:operator_setup(operator) abort " {{{1 + let s:operator = a:operator + let &opfunc = s:snr() . 'operator_function' + + " Ask for user input if necessary/relevant + if s:operator ==# 'change' + let l:current = vimtex#cmd#get_current() + if empty(l:current) | return | endif + + let s:operator_cmd_name = substitute(vimtex#echo#input({ + \ 'info' : ['Change command: ', ['VimtexWarning', l:current.name]], + \}), '^\\', '', '') + elseif s:operator ==# 'create' + let s:operator_cmd_name = substitute(vimtex#echo#input({ + \ 'info' : ['Create command: ', ['VimtexWarning', '(empty to cancel)']], + \}), '^\\', '', '') + endif +endfunction + +" }}}1 +function! s:operator_function(_) abort " {{{1 + let l:name = get(s:, 'operator_cmd_name', '') + + execute 'call vimtex#cmd#' . { + \ 'change': 'change(l:name)', + \ 'create': 'create(l:name, 0)', + \ 'delete': 'delete()', + \ 'toggle_star': 'toggle_star()', + \ 'toggle_frac': 'toggle_frac()', + \ }[s:operator] +endfunction + +" }}}1 +function! s:snr() abort " {{{1 + return matchstr(expand('<sfile>'), '<SNR>\d\+_') +endfunction + +" }}}1 + +function! s:get_cmd(direction) abort " {{{1 + let [lnum, cnum, match] = s:get_cmd_name(a:direction ==# 'next') + if lnum == 0 | return {} | endif + + let res = { + \ 'name' : match, + \ 'text' : '', + \ 'pos_start' : { 'lnum' : lnum, 'cnum' : cnum }, + \ 'pos_end' : { 'lnum' : lnum, 'cnum' : cnum + strlen(match) - 1 }, + \ 'args' : [], + \} + + " Environments always start with environment name and allows option + " afterwords + if res.name ==# '\begin' + let arg = s:get_cmd_part('{', res.pos_end) + if empty(arg) | return res | endif + + call add(res.args, arg) + let res.pos_end.lnum = arg.close.lnum + let res.pos_end.cnum = arg.close.cnum + endif + + " Get overlay specification + let res.overlay = s:get_cmd_overlay(res.pos_end.lnum, res.pos_end.cnum) + if !empty(res.overlay) + let res.pos_end.lnum = res.overlay.close.lnum + let res.pos_end.cnum = res.overlay.close.cnum + endif + + " Get options + let res.opt = s:get_cmd_part('[', res.pos_end) + if !empty(res.opt) + let res.pos_end.lnum = res.opt.close.lnum + let res.pos_end.cnum = res.opt.close.cnum + endif + + " Get arguments + let arg = s:get_cmd_part('{', res.pos_end) + while !empty(arg) + call add(res.args, arg) + let res.pos_end.lnum = arg.close.lnum + let res.pos_end.cnum = arg.close.cnum + let arg = s:get_cmd_part('{', res.pos_end) + endwhile + + " Include entire cmd text + let res.text = s:text_between(res.pos_start, res.pos_end, 1) + + return res +endfunction + +" }}}1 +function! s:get_cmd_name(next) abort " {{{1 + let [l:lnum, l:cnum] = searchpos('\v\\\a+\*?', a:next ? 'nW' : 'cbnW') + let l:match = matchstr(getline(l:lnum), '^\v\\\a*\*?', l:cnum-1) + return [l:lnum, l:cnum, l:match] +endfunction + +" }}}1 +function! s:get_cmd_part(part, start_pos) abort " {{{1 + let l:save_pos = vimtex#pos#get_cursor() + call vimtex#pos#set_cursor(a:start_pos) + let l:open = vimtex#delim#get_next('delim_tex', 'open') + call vimtex#pos#set_cursor(l:save_pos) + + " + " Ensure that the delimiter + " 1) exists, + " 2) is of the right type, + " 3) and is the next non-whitespace character. + " + if empty(l:open) + \ || l:open.match !=# a:part + \ || strlen(substitute( + \ s:text_between(a:start_pos, l:open), '\_s', '', 'g')) != 0 + return {} + endif + + let l:close = vimtex#delim#get_matching(l:open) + if empty(l:close) + return {} + endif + + return { + \ 'open' : l:open, + \ 'close' : l:close, + \ 'text' : s:text_between(l:open, l:close), + \} +endfunction + +" }}}1 +function! s:get_cmd_overlay(lnum, cnum) abort " {{{1 + let l:match = matchstr(getline(a:lnum), '^\s*[^>]*>', a:cnum) + + return empty(l:match) + \ ? {} + \ : { + \ 'open' : {'lnum' : a:lnum, 'cnum' : a:cnum + 1}, + \ 'close' : {'lnum' : a:lnum, 'cnum' : a:cnum + strlen(l:match)}, + \ 'text' : l:match + \ } +endfunction + +" }}}1 + +function! s:text_between(p1, p2, ...) abort " {{{1 + let [l1, c1] = [a:p1.lnum, a:p1.cnum - (a:0 > 0)] + let [l2, c2] = [a:p2.lnum, a:p2.cnum - (a:0 <= 0)] + + let lines = getline(l1, l2) + if !empty(lines) + let lines[0] = strpart(lines[0], c1) + let lines[-1] = strpart(lines[-1], 0, + \ l1 == l2 ? c2 - c1 : c2) + endif + return join(lines, "\n") +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/compiler.vim b/autoload/vimtex/compiler.vim new file mode 100644 index 00000000..5ecf260b --- /dev/null +++ b/autoload/vimtex/compiler.vim @@ -0,0 +1,334 @@ +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#compiler#init_buffer() abort " {{{1 + if !g:vimtex_compiler_enabled | return | endif + + " Define commands + command! -buffer VimtexCompile call vimtex#compiler#compile() + command! -buffer -bang VimtexCompileSS call vimtex#compiler#compile_ss() + command! -buffer -range VimtexCompileSelected <line1>,<line2>call vimtex#compiler#compile_selected('cmd') + command! -buffer VimtexCompileOutput call vimtex#compiler#output() + command! -buffer VimtexStop call vimtex#compiler#stop() + command! -buffer VimtexStopAll call vimtex#compiler#stop_all() + command! -buffer -bang VimtexClean call vimtex#compiler#clean(<q-bang> == "!") + command! -buffer -bang VimtexStatus call vimtex#compiler#status(<q-bang> == "!") + + " Define mappings + nnoremap <buffer> <plug>(vimtex-compile) :call vimtex#compiler#compile()<cr> + nnoremap <buffer> <plug>(vimtex-compile-ss) :call vimtex#compiler#compile_ss()<cr> + nnoremap <buffer> <plug>(vimtex-compile-selected) :set opfunc=vimtex#compiler#compile_selected<cr>g@ + xnoremap <buffer> <plug>(vimtex-compile-selected) :<c-u>call vimtex#compiler#compile_selected('visual')<cr> + nnoremap <buffer> <plug>(vimtex-compile-output) :call vimtex#compiler#output()<cr> + nnoremap <buffer> <plug>(vimtex-stop) :call vimtex#compiler#stop()<cr> + nnoremap <buffer> <plug>(vimtex-stop-all) :call vimtex#compiler#stop_all()<cr> + nnoremap <buffer> <plug>(vimtex-clean) :call vimtex#compiler#clean(0)<cr> + nnoremap <buffer> <plug>(vimtex-clean-full) :call vimtex#compiler#clean(1)<cr> + nnoremap <buffer> <plug>(vimtex-status) :call vimtex#compiler#status(0)<cr> + nnoremap <buffer> <plug>(vimtex-status-all) :call vimtex#compiler#status(1)<cr> +endfunction + +" }}}1 +function! vimtex#compiler#init_state(state) abort " {{{1 + if !g:vimtex_compiler_enabled | return | endif + + try + let l:options = { + \ 'root': a:state.root, + \ 'target' : a:state.base, + \ 'target_path' : a:state.tex, + \ 'tex_program' : a:state.tex_program, + \} + let a:state.compiler + \ = vimtex#compiler#{g:vimtex_compiler_method}#init(l:options) + catch /vimtex: Requirements not met/ + call vimtex#log#error('Compiler was not initialized!') + catch /E117/ + call vimtex#log#error( + \ 'Invalid compiler: ' . g:vimtex_compiler_method, + \ 'Please see :h g:vimtex_compiler_method') + endtry +endfunction + +" }}}1 + +function! vimtex#compiler#callback(status) abort " {{{1 + if exists('b:vimtex') && get(b:vimtex.compiler, 'silence_next_callback') + let b:vimtex.compiler.silence_next_callback = 0 + return + endif + + call vimtex#qf#open(0) + redraw + + if exists('s:output') + call s:output.update() + endif + + if a:status + call vimtex#log#info('Compilation completed') + else + call vimtex#log#warning('Compilation failed!') + endif + + if a:status && exists('b:vimtex') + call b:vimtex.parse_packages() + call vimtex#syntax#load#packages() + endif + + for l:hook in g:vimtex_compiler_callback_hooks + if exists('*' . l:hook) + execute 'call' l:hook . '(' . a:status . ')' + endif + endfor + + return '' +endfunction + +" }}}1 + +function! vimtex#compiler#compile() abort " {{{1 + if get(b:vimtex.compiler, 'continuous') + if b:vimtex.compiler.is_running() + call vimtex#compiler#stop() + else + call b:vimtex.compiler.start() + let b:vimtex.compiler.check_timer = s:check_if_running_start() + endif + else + call b:vimtex.compiler.start_single() + endif +endfunction + +" }}}1 +function! vimtex#compiler#compile_ss() abort " {{{1 + call b:vimtex.compiler.start_single() +endfunction + +" }}}1 +function! vimtex#compiler#compile_selected(type) abort range " {{{1 + let l:file = vimtex#parser#selection_to_texfile(a:type) + if empty(l:file) | return | endif + + " Create and initialize temporary compiler + let l:options = { + \ 'root' : l:file.root, + \ 'target' : l:file.base, + \ 'target_path' : l:file.tex, + \ 'backend' : 'process', + \ 'tex_program' : b:vimtex.tex_program, + \ 'background' : 1, + \ 'continuous' : 0, + \ 'callback' : 0, + \} + let l:compiler = vimtex#compiler#{g:vimtex_compiler_method}#init(l:options) + + call vimtex#log#toggle_verbose() + call l:compiler.start() + + " Check if successful + if vimtex#qf#inquire(l:file.base) + call vimtex#log#toggle_verbose() + call vimtex#log#warning('Compiling selected lines ... failed!') + botright cwindow + return + else + call l:compiler.clean(0) + call b:vimtex.viewer.view(l:file.pdf) + call vimtex#log#toggle_verbose() + call vimtex#log#info('Compiling selected lines ... done') + endif +endfunction + +" }}}1 +function! vimtex#compiler#output() abort " {{{1 + let l:file = get(b:vimtex.compiler, 'output', '') + if empty(l:file) + call vimtex#log#warning('No output exists!') + return + endif + + " If window already open, then go there + if exists('s:output') + if bufwinnr(l:file) == s:output.winnr + execute s:output.winnr . 'wincmd w' + return + else + call s:output.destroy() + endif + endif + + " Create new output window + silent execute 'split' l:file + + " Create the output object + let s:output = {} + let s:output.name = l:file + let s:output.bufnr = bufnr('%') + let s:output.winnr = bufwinnr('%') + function! s:output.update() dict abort + if bufwinnr(self.name) != self.winnr + return + endif + + if mode() ==? 'v' || mode() ==# "\<c-v>" + return + endif + + " Go to last line of file if it is not the current window + if bufwinnr('%') != self.winnr + let l:return = bufwinnr('%') + execute 'keepalt' self.winnr . 'wincmd w' + edit + normal! Gzb + execute 'keepalt' l:return . 'wincmd w' + redraw + endif + endfunction + function! s:output.destroy() dict abort + autocmd! vimtex_output_window + augroup! vimtex_output_window + unlet s:output + endfunction + + " Better automatic update + augroup vimtex_output_window + autocmd! + autocmd BufDelete <buffer> call s:output.destroy() + autocmd BufEnter * call s:output.update() + autocmd FocusGained * call s:output.update() + autocmd CursorHold * call s:output.update() + autocmd CursorHoldI * call s:output.update() + autocmd CursorMoved * call s:output.update() + autocmd CursorMovedI * call s:output.update() + augroup END + + " Set some mappings + nnoremap <silent><nowait><buffer> q :bwipeout<cr> + if has('nvim') || has('gui_running') + nnoremap <silent><nowait><buffer> <esc> :bwipeout<cr> + endif + + " Set some buffer options + setlocal autoread + setlocal nomodifiable + setlocal bufhidden=wipe +endfunction + +" }}}1 +function! vimtex#compiler#stop() abort " {{{1 + call b:vimtex.compiler.stop() + silent! call timer_stop(b:vimtex.compiler.check_timer) +endfunction + +" }}}1 +function! vimtex#compiler#stop_all() abort " {{{1 + for l:state in vimtex#state#list_all() + if exists('l:state.compiler.is_running') + \ && l:state.compiler.is_running() + call l:state.compiler.stop() + endif + endfor +endfunction + +" }}}1 +function! vimtex#compiler#clean(full) abort " {{{1 + call b:vimtex.compiler.clean(a:full) + + if empty(b:vimtex.compiler.build_dir) | return | endif + sleep 100m + + " Remove auxilliary output directories if they are empty + let l:build_dir = (vimtex#paths#is_abs(b:vimtex.compiler.build_dir) + \ ? '' : b:vimtex.root . '/') + \ . b:vimtex.compiler.build_dir + let l:tree = glob(l:build_dir . '/**/*', 0, 1) + let l:files = filter(copy(l:tree), 'filereadable(v:val)') + if !empty(l:files) | return | endif + + for l:dir in sort(l:tree) + [l:build_dir] + call delete(l:dir, 'd') + endfor +endfunction + +" }}}1 +function! vimtex#compiler#status(detailed) abort " {{{1 + if a:detailed + let l:running = [] + for l:data in vimtex#state#list_all() + if l:data.compiler.is_running() + let l:name = l:data.tex + if len(l:name) >= winwidth('.') - 20 + let l:name = '...' . l:name[-winwidth('.')+23:] + endif + call add(l:running, printf('%-6s %s', + \ string(l:data.compiler.get_pid()) . ':', l:name)) + endif + endfor + + if empty(l:running) + call vimtex#log#warning('Compiler is not running!') + else + call vimtex#log#info('Compiler is running', l:running) + endif + else + if b:vimtex.compiler.is_running() + call vimtex#log#info('Compiler is running') + else + call vimtex#log#warning('Compiler is not running!') + endif + endif +endfunction + +" }}}1 + + +let s:check_timers = {} +function! s:check_if_running_start() abort " {{{1 + if !exists('*timer_start') | return -1 | endif + + let l:timer = timer_start(50, function('s:check_if_running'), {'repeat': 20}) + + let s:check_timers[l:timer] = { + \ 'compiler' : b:vimtex.compiler, + \ 'vimtex_id' : b:vimtex_id, + \} + + return l:timer +endfunction + +" }}}1 +function! s:check_if_running(timer) abort " {{{1 + if s:check_timers[a:timer].compiler.is_running() | return | endif + + call timer_stop(a:timer) + + if get(b:, 'vimtex_id', -1) == s:check_timers[a:timer].vimtex_id + call vimtex#compiler#output() + endif + call vimtex#log#error('Compiler did not start successfully!') + + unlet s:check_timers[a:timer].compiler.check_timer + unlet s:check_timers[a:timer] +endfunction + +" }}}1 + + +" {{{1 Initialize module + +if !g:vimtex_compiler_enabled | finish | endif + +augroup vimtex_compiler + autocmd! + autocmd VimLeave * call vimtex#compiler#stop_all() +augroup END + +" }}}1 + +endif diff --git a/autoload/vimtex/compiler/arara.vim b/autoload/vimtex/compiler/arara.vim new file mode 100644 index 00000000..8234b77d --- /dev/null +++ b/autoload/vimtex/compiler/arara.vim @@ -0,0 +1,218 @@ +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#compiler#arara#init(options) abort " {{{1 + let l:compiler = deepcopy(s:compiler) + + call l:compiler.init(extend(a:options, + \ get(g:, 'vimtex_compiler_arara', {}), 'keep')) + + return l:compiler +endfunction + +" }}}1 + +let s:compiler = { + \ 'name' : 'arara', + \ 'backend' : has('nvim') ? 'nvim' + \ : v:version >= 800 ? 'jobs' : 'process', + \ 'root' : '', + \ 'target' : '', + \ 'target_path' : '', + \ 'background' : 1, + \ 'output' : tempname(), + \ 'options' : ['--log'], + \} + +function! s:compiler.init(options) abort dict " {{{1 + call extend(self, a:options) + + if !executable('arara') + call vimtex#log#warning('arara is not executable!') + throw 'vimtex: Requirements not met' + endif + + call extend(self, deepcopy(s:compiler_{self.backend})) + + " Processes run with the new jobs api will not run in the foreground + if self.backend !=# 'process' + let self.background = 1 + endif +endfunction + +" }}}1 + +function! s:compiler.build_cmd() abort dict " {{{1 + let l:cmd = 'arara' + + for l:opt in self.options + let l:cmd .= ' ' . l:opt + endfor + + return l:cmd . ' ' . vimtex#util#shellescape(self.target) +endfunction + +" }}}1 +function! s:compiler.cleanup() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.pprint_items() abort dict " {{{1 + let l:configuration = [] + + if self.backend ==# 'process' + call add(l:configuration, ['background', self.background]) + endif + + call add(l:configuration, ['arara options', self.options]) + + let l:list = [] + call add(l:list, ['backend', self.backend]) + if self.background + call add(l:list, ['output', self.output]) + endif + + if self.target_path !=# b:vimtex.tex + call add(l:list, ['root', self.root]) + call add(l:list, ['target', self.target_path]) + endif + + call add(l:list, ['configuration', l:configuration]) + + if has_key(self, 'process') + call add(l:list, ['process', self.process]) + endif + + if has_key(self, 'job') + call add(l:list, ['cmd', self.cmd]) + endif + + return l:list +endfunction + +" }}}1 + +function! s:compiler.clean(...) abort dict " {{{1 + call vimtex#log#warning('Clean not implemented for arara') +endfunction + +" }}}1 +function! s:compiler.start(...) abort dict " {{{1 + call self.exec() + + if self.background + call vimtex#log#info('Compiler started in background') + else + call vimtex#compiler#callback(!vimtex#qf#inquire(self.target)) + endif +endfunction + +" }}}1 +function! s:compiler.start_single() abort dict " {{{1 + call self.start() +endfunction + +" }}}1 +function! s:compiler.stop() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.is_running() abort dict " {{{1 + return 0 +endfunction + +" }}}1 +function! s:compiler.kill() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.get_pid() abort dict " {{{1 + return 0 +endfunction + +" }}}1 + +let s:compiler_process = {} +function! s:compiler_process.exec() abort dict " {{{1 + let self.process = vimtex#process#new() + let self.process.name = 'arara' + let self.process.background = self.background + let self.process.workdir = self.root + let self.process.output = self.output + let self.process.cmd = self.build_cmd() + call self.process.run() +endfunction + +" }}}1 + +let s:compiler_jobs = {} +function! s:compiler_jobs.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + let l:options = { + \ 'out_io' : 'file', + \ 'err_io' : 'file', + \ 'out_name' : self.output, + \ 'err_name' : self.output, + \} + + let s:cb_target = self.target_path !=# b:vimtex.tex ? self.target_path : '' + let l:options.exit_cb = function('s:callback') + + call vimtex#paths#pushd(self.root) + let self.job = job_start(l:cmd, l:options) + call vimtex#paths#popd() +endfunction + +" }}}1 +function! s:callback(ch, msg) abort " {{{1 + call vimtex#compiler#callback(!vimtex#qf#inquire(s:cb_target)) +endfunction + +" }}}1 + +let s:compiler_nvim = {} +function! s:compiler_nvim.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + + let l:shell = { + \ 'on_stdout' : function('s:callback_nvim_output'), + \ 'on_stderr' : function('s:callback_nvim_output'), + \ 'on_exit' : function('s:callback_nvim_exit'), + \ 'cwd' : self.root, + \ 'target' : self.target_path, + \ 'output' : self.output, + \} + + let self.job = jobstart(l:cmd, l:shell) +endfunction + +" }}}1 +function! s:callback_nvim_output(id, data, event) abort dict " {{{1 + if !empty(a:data) + call writefile(filter(a:data, '!empty(v:val)'), self.output, 'a') + endif +endfunction + +" }}}1 +function! s:callback_nvim_exit(id, data, event) abort dict " {{{1 + let l:target = self.target !=# b:vimtex.tex ? self.target : '' + call vimtex#compiler#callback(!vimtex#qf#inquire(l:target)) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/compiler/latexmk.vim b/autoload/vimtex/compiler/latexmk.vim new file mode 100644 index 00000000..d7a36708 --- /dev/null +++ b/autoload/vimtex/compiler/latexmk.vim @@ -0,0 +1,700 @@ +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#compiler#latexmk#init(options) abort " {{{1 + let l:compiler = deepcopy(s:compiler) + + call l:compiler.init(extend(a:options, + \ get(g:, 'vimtex_compiler_latexmk', {}), 'keep')) + + return l:compiler +endfunction + +" }}}1 +function! vimtex#compiler#latexmk#wrap_option(name, value) abort " {{{1 + return has('win32') + \ ? ' -e "$' . a:name . ' = ''' . a:value . '''"' + \ : ' -e ''$' . a:name . ' = "' . a:value . '"''' +endfunction + +"}}}1 + +function! vimtex#compiler#latexmk#get_rc_opt(root, opt, type, default) abort " {{{1 + " + " Parse option from .latexmkrc. + " + " Arguments: + " root Root of LaTeX project + " opt Name of options + " type 0 if string, 1 if integer, 2 if list + " default Value to return if option not found in latexmkrc file + " + " Output: + " [value, location] + " + " value Option value (integer or string) + " location An integer that indicates where option was found + " -1: not found (default value returned) + " 0: global latexmkrc file + " 1: local latexmkrc file + " + + if a:type == 0 + let l:pattern = '^\s*\$' . a:opt . '\s*=\s*[''"]\(.\+\)[''"]' + elseif a:type == 1 + let l:pattern = '^\s*\$' . a:opt . '\s*=\s*\(\d\+\)' + elseif a:type == 2 + let l:pattern = '^\s*@' . a:opt . '\s*=\s*(\(.*\))' + else + throw 'vimtex: argument error' + endif + + " Candidate files + " - each element is a pair [path_to_file, is_local_rc_file]. + let l:files = [ + \ [a:root . '/latexmkrc', 1], + \ [a:root . '/.latexmkrc', 1], + \ [fnamemodify('~/.latexmkrc', ':p'), 0], + \] + if !empty($XDG_CONFIG_HOME) + call add(l:files, [$XDG_CONFIG_HOME . '/latexmk/latexmkrc', 0]) + endif + + let l:result = [a:default, -1] + + for [l:file, l:is_local] in l:files + if filereadable(l:file) + let l:match = matchlist(readfile(l:file), l:pattern) + if len(l:match) > 1 + let l:result = [l:match[1], l:is_local] + break + end + endif + endfor + + " Parse the list + if a:type == 2 && l:result[1] > -1 + let l:array = split(l:result[0], ',') + let l:result[0] = [] + for l:x in l:array + let l:x = substitute(l:x, "^'", '', '') + let l:x = substitute(l:x, "'$", '', '') + let l:result[0] += [l:x] + endfor + endif + + return l:result +endfunction + +" }}}1 + +let s:compiler = { + \ 'name' : 'latexmk', + \ 'executable' : 'latexmk', + \ 'backend' : has('nvim') ? 'nvim' + \ : v:version >= 800 ? 'jobs' : 'process', + \ 'root' : '', + \ 'target' : '', + \ 'target_path' : '', + \ 'background' : 1, + \ 'build_dir' : '', + \ 'callback' : 1, + \ 'continuous' : 1, + \ 'output' : tempname(), + \ 'options' : [ + \ '-verbose', + \ '-file-line-error', + \ '-synctex=1', + \ '-interaction=nonstopmode', + \ ], + \ 'hooks' : [], + \ 'shell' : fnamemodify(&shell, ':t'), + \} + +function! s:compiler.init(options) abort dict " {{{1 + call extend(self, a:options) + + call self.init_check_requirements() + call self.init_build_dir_option() + call self.init_pdf_mode_option() + + call extend(self, deepcopy(s:compiler_{self.backend})) + + " Continuous processes can't run in foreground, neither can processes run + " with the new jobs api + if self.continuous || self.backend !=# 'process' + let self.background = 1 + endif + + if self.backend !=# 'process' + let self.shell = 'sh' + endif +endfunction + +" }}}1 +function! s:compiler.init_build_dir_option() abort dict " {{{1 + " + " Check if .latexmkrc sets the build_dir - if so this should be respected + " + let l:out_dir = + \ vimtex#compiler#latexmk#get_rc_opt(self.root, 'out_dir', 0, '')[0] + + if !empty(l:out_dir) + if !empty(self.build_dir) && (self.build_dir !=# l:out_dir) + call vimtex#log#warning( + \ 'Setting out_dir from latexmkrc overrides build_dir!', + \ 'Changed build_dir from: ' . self.build_dir, + \ 'Changed build_dir to: ' . l:out_dir) + endif + let self.build_dir = l:out_dir + endif +endfunction + +" }}}1 +function! s:compiler.init_pdf_mode_option() abort dict " {{{1 + " If the TeX program directive was not set, and if the pdf_mode is set in + " a .latexmkrc file, then deduce the compiler engine from the value of + " pdf_mode. + + " Parse the pdf_mode option. If not found, it is set to -1. + let [l:pdf_mode, l:is_local] = + \ vimtex#compiler#latexmk#get_rc_opt(self.root, 'pdf_mode', 1, -1) + + " If pdf_mode has a supported value (1: pdflatex, 4: lualatex, 5: xelatex), + " override the value of self.tex_program. + if l:pdf_mode == 1 + let l:tex_program = 'pdflatex' + elseif l:pdf_mode == 3 + let l:tex_program = 'pdfdvi' + elseif l:pdf_mode == 4 + let l:tex_program = 'lualatex' + elseif l:pdf_mode == 5 + let l:tex_program = 'xelatex' + else + return + endif + + if self.tex_program ==# '_' + " The TeX program directive was not specified + let self.tex_program = l:tex_program + elseif l:is_local && self.tex_program !=# l:tex_program + call vimtex#log#warning( + \ 'Value of pdf_mode from latexmkrc is inconsistent with ' . + \ 'TeX program directive!', + \ 'TeX program: ' . self.tex_program, + \ 'pdf_mode: ' . l:tex_program, + \ 'The value of pdf_mode will be ignored.') + endif +endfunction + +" }}}1 +function! s:compiler.init_check_requirements() abort dict " {{{1 + " Check option validity + if self.callback + if !(has('clientserver') || has('nvim') || has('job')) + let self.callback = 0 + call vimtex#log#warning( + \ 'Can''t use callbacks without +job, +nvim, or +clientserver', + \ 'Callback option has been disabled.') + endif + endif + + " Check for required executables + let l:required = [self.executable] + if self.continuous && !(has('win32') || has('win32unix')) + let l:required += ['pgrep'] + endif + let l:missing = filter(l:required, '!executable(v:val)') + + " Disable latexmk if required programs are missing + if len(l:missing) > 0 + for l:cmd in l:missing + call vimtex#log#warning(l:cmd . ' is not executable') + endfor + throw 'vimtex: Requirements not met' + endif +endfunction + +" }}}1 + +function! s:compiler.build_cmd() abort dict " {{{1 + if has('win32') + let l:cmd = 'set max_print_line=2000 & ' . self.executable + else + if self.shell ==# 'fish' + let l:cmd = 'set max_print_line 2000; and ' . self.executable + else + let l:cmd = 'max_print_line=2000 ' . self.executable + endif + endif + + for l:opt in self.options + let l:cmd .= ' ' . l:opt + endfor + + let l:cmd .= ' ' . self.get_engine() + + if !empty(self.build_dir) + let l:cmd .= ' -outdir=' . fnameescape(self.build_dir) + endif + + if self.continuous + let l:cmd .= ' -pvc' + + " Set viewer options + if !get(g:, 'vimtex_view_automatic', 1) + \ || get(get(b:vimtex, 'viewer', {}), 'xwin_id') > 0 + \ || get(s:, 'silence_next_callback', 0) + let l:cmd .= ' -view=none' + elseif g:vimtex_view_enabled + \ && has_key(b:vimtex.viewer, 'latexmk_append_argument') + let l:cmd .= b:vimtex.viewer.latexmk_append_argument() + endif + + if self.callback + if has('job') || has('nvim') + for [l:opt, l:val] in items({ + \ 'success_cmd' : 'vimtex_compiler_callback_success', + \ 'failure_cmd' : 'vimtex_compiler_callback_failure', + \}) + let l:func = 'echo ' . l:val + let l:cmd .= vimtex#compiler#latexmk#wrap_option(l:opt, l:func) + endfor + elseif empty(v:servername) + call vimtex#log#warning('Can''t use callbacks with empty v:servername') + else + " Some notes: + " - We excape the v:servername because this seems necessary on Windows + " for neovim, see e.g. Github Issue #877 + for [l:opt, l:val] in items({'success_cmd' : 1, 'failure_cmd' : 0}) + let l:callback = has('win32') + \ ? '"vimtex#compiler#callback(' . l:val . ')"' + \ : '\"vimtex\#compiler\#callback(' . l:val . ')\"' + let l:func = vimtex#util#shellescape('""') + \ . g:vimtex_compiler_progname + \ . vimtex#util#shellescape('""') + \ . ' --servername ' . vimtex#util#shellescape(v:servername) + \ . ' --remote-expr ' . l:callback + let l:cmd .= vimtex#compiler#latexmk#wrap_option(l:opt, l:func) + endfor + endif + endif + endif + + return l:cmd . ' ' . vimtex#util#shellescape(self.target) +endfunction + +" }}}1 +function! s:compiler.get_engine() abort dict " {{{1 + return get(extend(g:vimtex_compiler_latexmk_engines, + \ { + \ 'pdfdvi' : '-pdfdvi', + \ 'pdflatex' : '-pdf', + \ 'luatex' : '-lualatex', + \ 'lualatex' : '-lualatex', + \ 'xelatex' : '-xelatex', + \ 'context (pdftex)' : '-pdf -pdflatex=texexec', + \ 'context (luatex)' : '-pdf -pdflatex=context', + \ 'context (xetex)' : '-pdf -pdflatex=''texexec --xtx''', + \ }, 'keep'), self.tex_program, '-pdf') +endfunction + +" }}}1 +function! s:compiler.cleanup() abort dict " {{{1 + if self.is_running() + call self.kill() + endif +endfunction + +" }}}1 +function! s:compiler.pprint_items() abort dict " {{{1 + let l:configuration = [ + \ ['continuous', self.continuous], + \ ['callback', self.callback], + \] + + if self.backend ==# 'process' && !self.continuous + call add(l:configuration, ['background', self.background]) + endif + + if !empty(self.build_dir) + call add(l:configuration, ['build_dir', self.build_dir]) + endif + call add(l:configuration, ['latexmk options', self.options]) + call add(l:configuration, ['latexmk engine', self.get_engine()]) + + let l:list = [] + call add(l:list, ['backend', self.backend]) + if self.executable !=# s:compiler.executable + call add(l:list, ['latexmk executable', self.executable]) + endif + if self.background + call add(l:list, ['output', self.output]) + endif + + if self.target_path !=# b:vimtex.tex + call add(l:list, ['root', self.root]) + call add(l:list, ['target', self.target_path]) + endif + + call add(l:list, ['configuration', l:configuration]) + + if has_key(self, 'process') + call add(l:list, ['process', self.process]) + endif + + if has_key(self, 'job') + if self.continuous + if self.backend ==# 'jobs' + call add(l:list, ['job', self.job]) + else + call add(l:list, ['pid', self.get_pid()]) + endif + endif + call add(l:list, ['cmd', self.cmd]) + endif + + return l:list +endfunction + +" }}}1 + +function! s:compiler.clean(full) abort dict " {{{1 + let l:restart = self.is_running() + if l:restart + call self.stop() + endif + + " Define and run the latexmk clean cmd + let l:cmd = (has('win32') + \ ? 'cd /D "' . self.root . '" & ' + \ : 'cd ' . vimtex#util#shellescape(self.root) . '; ') + \ . self.executable . ' ' . (a:full ? '-C ' : '-c ') + if !empty(self.build_dir) + let l:cmd .= printf(' -outdir=%s ', fnameescape(self.build_dir)) + endif + let l:cmd .= vimtex#util#shellescape(self.target) + call vimtex#process#run(l:cmd) + + call vimtex#log#info('Compiler clean finished' . (a:full ? ' (full)' : '')) + + if l:restart + let self.silent_next_callback = 1 + silent call self.start() + endif +endfunction + +" }}}1 +function! s:compiler.start(...) abort dict " {{{1 + if self.is_running() + call vimtex#log#warning( + \ 'Compiler is already running for `' . self.target . "'") + return + endif + + " + " Create build dir if it does not exist + " + if !empty(self.build_dir) + let l:dirs = split(glob(self.root . '/**/*.tex'), '\n') + call map(l:dirs, 'fnamemodify(v:val, '':h'')') + call map(l:dirs, 'strpart(v:val, strlen(self.root) + 1)') + call vimtex#util#uniq(sort(filter(l:dirs, "v:val !=# ''"))) + call map(l:dirs, + \ (vimtex#paths#is_abs(self.build_dir) ? '' : "self.root . '/' . ") + \ . "self.build_dir . '/' . v:val") + call filter(l:dirs, '!isdirectory(v:val)') + + " Create the non-existing directories + for l:dir in l:dirs + call mkdir(l:dir, 'p') + endfor + endif + + call self.exec() + + if self.continuous + call vimtex#log#info('Compiler started in continuous mode' + \ . (a:0 > 0 ? ' (single shot)' : '')) + if exists('#User#VimtexEventCompileStarted') + doautocmd <nomodeline> User VimtexEventCompileStarted + endif + else + if self.background + call vimtex#log#info('Compiler started in background!') + else + call vimtex#compiler#callback(!vimtex#qf#inquire(self.target)) + endif + endif +endfunction + +" }}}1 +function! s:compiler.stop() abort dict " {{{1 + if self.is_running() + call self.kill() + call vimtex#log#info('Compiler stopped (' . self.target . ')') + if exists('#User#VimtexEventCompileStopped') + doautocmd <nomodeline> User VimtexEventCompileStopped + endif + else + call vimtex#log#warning( + \ 'There is no process to stop (' . self.target . ')') + endif +endfunction + +" }}}1 + +let s:compiler_process = {} +function! s:compiler_process.exec() abort dict " {{{1 + let l:process = vimtex#process#new() + let l:process.name = 'latexmk' + let l:process.continuous = self.continuous + let l:process.background = self.background + let l:process.workdir = self.root + let l:process.output = self.output + let l:process.cmd = self.build_cmd() + + if l:process.continuous + if (has('win32') || has('win32unix')) + " Not implemented + else + for l:pid in split(system( + \ 'pgrep -f "^[^ ]*perl.*latexmk.*' . self.target . '"'), "\n") + let l:path = resolve('/proc/' . l:pid . '/cwd') . '/' . self.target + if l:path ==# self.target_path + let l:process.pid = str2nr(l:pid) + break + endif + endfor + endif + endif + + function! l:process.set_pid() abort dict " {{{2 + if (has('win32') || has('win32unix')) + let pidcmd = 'tasklist /fi "imagename eq latexmk.exe"' + let pidinfo = vimtex#process#capture(pidcmd)[-1] + let self.pid = str2nr(split(pidinfo,'\s\+')[1]) + else + let self.pid = str2nr(system('pgrep -nf "^[^ ]*perl.*latexmk"')[:-2]) + endif + + return self.pid + endfunction + + " }}}2 + + let self.process = l:process + call self.process.run() +endfunction + +" }}}1 +function! s:compiler_process.start_single() abort dict " {{{1 + let l:continuous = self.continuous + let self.continuous = self.background && self.callback && !empty(v:servername) + + if self.continuous + let g:vimtex_compiler_callback_hooks += ['VimtexSSCallback'] + function! VimtexSSCallback(status) abort + silent call vimtex#compiler#stop() + call remove(g:vimtex_compiler_callback_hooks, 'VimtexSSCallback') + endfunction + endif + + call self.start(1) + let self.continuous = l:continuous +endfunction + +" }}}1 +function! s:compiler_process.is_running() abort dict " {{{1 + return exists('self.process.pid') && self.process.pid > 0 +endfunction + +" }}}1 +function! s:compiler_process.kill() abort dict " {{{1 + call self.process.stop() +endfunction + +" }}}1 +function! s:compiler_process.get_pid() abort dict " {{{1 + return has_key(self, 'process') ? self.process.pid : 0 +endfunction + +" }}}1 + +let s:compiler_jobs = {} +function! s:compiler_jobs.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + + let l:options = { + \ 'out_io' : 'file', + \ 'err_io' : 'file', + \ 'out_name' : self.output, + \ 'err_name' : self.output, + \} + if self.continuous + let l:options.out_io = 'pipe' + let l:options.err_io = 'pipe' + let l:options.out_cb = function('s:callback_continuous_output') + let l:options.err_cb = function('s:callback_continuous_output') + call writefile([], self.output, 'a') + else + let s:cb_target = self.target_path !=# b:vimtex.tex + \ ? self.target_path : '' + let l:options.exit_cb = function('s:callback') + endif + + call vimtex#paths#pushd(self.root) + let self.job = job_start(l:cmd, l:options) + call vimtex#paths#popd() +endfunction + +" }}}1 +function! s:compiler_jobs.start_single() abort dict " {{{1 + let l:continuous = self.continuous + let self.continuous = 0 + call self.start() + let self.continuous = l:continuous +endfunction + +" }}}1 +function! s:compiler_jobs.kill() abort dict " {{{1 + call job_stop(self.job) +endfunction + +" }}}1 +function! s:compiler_jobs.is_running() abort dict " {{{1 + return has_key(self, 'job') && job_status(self.job) ==# 'run' +endfunction + +" }}}1 +function! s:compiler_jobs.get_pid() abort dict " {{{1 + return has_key(self, 'job') + \ ? get(job_info(self.job), 'process') : 0 +endfunction + +" }}}1 +function! s:callback(ch, msg) abort " {{{1 + call vimtex#compiler#callback(!vimtex#qf#inquire(s:cb_target)) +endfunction + +" }}}1 +function! s:callback_continuous_output(channel, msg) abort " {{{1 + if exists('b:vimtex') && filewritable(b:vimtex.compiler.output) + call writefile([a:msg], b:vimtex.compiler.output, 'a') + endif + + if a:msg ==# 'vimtex_compiler_callback_success' + call vimtex#compiler#callback(1) + elseif a:msg ==# 'vimtex_compiler_callback_failure' + call vimtex#compiler#callback(0) + endif + + try + for l:Hook in get(get(get(b:, 'vimtex', {}), 'compiler', {}), 'hooks', []) + call l:Hook(a:msg) + endfor + catch /E716/ + endtry +endfunction + +" }}}1 + +let s:compiler_nvim = {} +function! s:compiler_nvim.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + + let l:shell = { + \ 'on_stdout' : function('s:callback_nvim_output'), + \ 'on_stderr' : function('s:callback_nvim_output'), + \ 'cwd' : self.root, + \ 'target' : self.target_path, + \ 'output' : self.output, + \} + + if !self.continuous + let l:shell.on_exit = function('s:callback_nvim_exit') + endif + + " Initialize output file + try + call writefile([], self.output) + endtry + + let self.job = jobstart(l:cmd, l:shell) +endfunction + +" }}}1 +function! s:compiler_nvim.start_single() abort dict " {{{1 + let l:continuous = self.continuous + let self.continuous = 0 + call self.start() + let self.continuous = l:continuous +endfunction + +" }}}1 +function! s:compiler_nvim.kill() abort dict " {{{1 + call jobstop(self.job) +endfunction + +" }}}1 +function! s:compiler_nvim.is_running() abort dict " {{{1 + try + let pid = jobpid(self.job) + return 1 + catch + return 0 + endtry +endfunction + +" }}}1 +function! s:compiler_nvim.get_pid() abort dict " {{{1 + try + return jobpid(self.job) + catch + return 0 + endtry +endfunction + +" }}}1 +function! s:callback_nvim_output(id, data, event) abort dict " {{{1 + " Filter out unwanted newlines + let l:data = split(substitute(join(a:data, 'QQ'), '^QQ\|QQ$', '', ''), 'QQ') + + if !empty(l:data) && filewritable(self.output) + call writefile(l:data, self.output, 'a') + endif + + if match(a:data, 'vimtex_compiler_callback_success') != -1 + call vimtex#compiler#callback(!vimtex#qf#inquire(self.target)) + elseif match(a:data, 'vimtex_compiler_callback_failure') != -1 + call vimtex#compiler#callback(0) + endif + + try + for l:Hook in get(get(get(b:, 'vimtex', {}), 'compiler', {}), 'hooks', []) + call l:Hook(join(a:data, "\n")) + endfor + catch /E716/ + endtry +endfunction + +" }}}1 +function! s:callback_nvim_exit(id, data, event) abort dict " {{{1 + let l:target = self.target !=# b:vimtex.tex ? self.target : '' + call vimtex#compiler#callback(!vimtex#qf#inquire(l:target)) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/compiler/latexrun.vim b/autoload/vimtex/compiler/latexrun.vim new file mode 100644 index 00000000..01ebc02f --- /dev/null +++ b/autoload/vimtex/compiler/latexrun.vim @@ -0,0 +1,250 @@ +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#compiler#latexrun#init(options) abort " {{{1 + let l:compiler = deepcopy(s:compiler) + + call l:compiler.init(extend(a:options, + \ get(g:, 'vimtex_compiler_latexrun', {}), 'keep')) + + return l:compiler +endfunction + +" }}}1 + +let s:compiler = { + \ 'name' : 'latexrun', + \ 'backend' : has('nvim') ? 'nvim' + \ : v:version >= 800 ? 'jobs' : 'process', + \ 'root' : '', + \ 'target' : '', + \ 'target_path' : '', + \ 'background' : 1, + \ 'build_dir' : '', + \ 'output' : tempname(), + \ 'options' : [ + \ '--verbose-cmds', + \ '--latex-args="-synctex=1"', + \ ], + \} + +function! s:compiler.init(options) abort dict " {{{1 + call extend(self, a:options) + + if !executable('latexrun') + call vimtex#log#warning('latexrun is not executable!') + throw 'vimtex: Requirements not met' + endif + + call extend(self, deepcopy(s:compiler_{self.backend})) + + " Processes run with the new jobs api will not run in the foreground + if self.backend !=# 'process' + let self.background = 1 + endif +endfunction + +" }}}1 + +function! s:compiler.build_cmd() abort dict " {{{1 + let l:cmd = 'latexrun' + + for l:opt in self.options + let l:cmd .= ' ' . l:opt + endfor + + let l:cmd .= ' --latex-cmd ' . self.get_engine() + + let l:cmd .= ' -O ' + \ . (empty(self.build_dir) ? '.' : fnameescape(self.build_dir)) + + return l:cmd . ' ' . vimtex#util#shellescape(self.target) +endfunction + +" }}}1 +function! s:compiler.get_engine() abort dict " {{{1 + return get(extend(g:vimtex_compiler_latexrun_engines, + \ { + \ '_' : 'pdflatex', + \ 'pdflatex' : 'pdflatex', + \ 'lualatex' : 'lualatex', + \ 'xelatex' : 'xelatex', + \ }, 'keep'), self.tex_program, '_') +endfunction + +" }}}1 +function! s:compiler.cleanup() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.pprint_items() abort dict " {{{1 + let l:configuration = [] + + if self.backend ==# 'process' + call add(l:configuration, ['background', self.background]) + endif + + if !empty(self.build_dir) + call add(l:configuration, ['build_dir', self.build_dir]) + endif + call add(l:configuration, ['latexrun options', self.options]) + call add(l:configuration, ['latexrun engine', self.get_engine()]) + + let l:list = [] + call add(l:list, ['backend', self.backend]) + if self.background + call add(l:list, ['output', self.output]) + endif + + if self.target_path !=# b:vimtex.tex + call add(l:list, ['root', self.root]) + call add(l:list, ['target', self.target_path]) + endif + + call add(l:list, ['configuration', l:configuration]) + + if has_key(self, 'process') + call add(l:list, ['process', self.process]) + endif + + if has_key(self, 'job') + call add(l:list, ['cmd', self.cmd]) + endif + + return l:list +endfunction + +" }}}1 + +function! s:compiler.clean(...) abort dict " {{{1 + let l:cmd = (has('win32') + \ ? 'cd /D "' . self.root . '" & ' + \ : 'cd ' . vimtex#util#shellescape(self.root) . '; ') + \ . 'latexrun --clean-all' + \ . ' -O ' + \ . (empty(self.build_dir) ? '.' : fnameescape(self.build_dir)) + call vimtex#process#run(l:cmd) + + call vimtex#log#info('Compiler clean finished') +endfunction + +" }}}1 +function! s:compiler.start(...) abort dict " {{{1 + call self.exec() + + if self.background + call vimtex#log#info('Compiler started in background') + else + call vimtex#compiler#callback(!vimtex#qf#inquire(self.target)) + endif +endfunction + +" }}}1 +function! s:compiler.start_single() abort dict " {{{1 + call self.start() +endfunction + +" }}}1 +function! s:compiler.stop() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.is_running() abort dict " {{{1 + return 0 +endfunction + +" }}}1 +function! s:compiler.kill() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.get_pid() abort dict " {{{1 + return 0 +endfunction + +" }}}1 + +let s:compiler_process = {} +function! s:compiler_process.exec() abort dict " {{{1 + let self.process = vimtex#process#new() + let self.process.name = 'latexrun' + let self.process.background = self.background + let self.process.workdir = self.root + let self.process.output = self.output + let self.process.cmd = self.build_cmd() + call self.process.run() +endfunction + +" }}}1 + +let s:compiler_jobs = {} +function! s:compiler_jobs.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + let l:options = { + \ 'out_io' : 'file', + \ 'err_io' : 'file', + \ 'out_name' : self.output, + \ 'err_name' : self.output, + \} + + let s:cb_target = self.target_path !=# b:vimtex.tex ? self.target_path : '' + let l:options.exit_cb = function('s:callback') + + call vimtex#paths#pushd(self.root) + let self.job = job_start(l:cmd, l:options) + call vimtex#paths#popd() +endfunction + +" }}}1 +function! s:callback(ch, msg) abort " {{{1 + call vimtex#compiler#callback(!vimtex#qf#inquire(s:cb_target)) +endfunction + +" }}}1 + +let s:compiler_nvim = {} +function! s:compiler_nvim.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + + let l:shell = { + \ 'on_stdout' : function('s:callback_nvim_output'), + \ 'on_stderr' : function('s:callback_nvim_output'), + \ 'on_exit' : function('s:callback_nvim_exit'), + \ 'cwd' : self.root, + \ 'target' : self.target_path, + \ 'output' : self.output, + \} + + let self.job = jobstart(l:cmd, l:shell) +endfunction + +" }}}1 +function! s:callback_nvim_output(id, data, event) abort dict " {{{1 + if !empty(a:data) + call writefile(filter(a:data, '!empty(v:val)'), self.output, 'a') + endif +endfunction + +" }}}1 +function! s:callback_nvim_exit(id, data, event) abort dict " {{{1 + let l:target = self.target !=# b:vimtex.tex ? self.target : '' + call vimtex#compiler#callback(!vimtex#qf#inquire(l:target)) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/compiler/tectonic.vim b/autoload/vimtex/compiler/tectonic.vim new file mode 100644 index 00000000..3b9e8139 --- /dev/null +++ b/autoload/vimtex/compiler/tectonic.vim @@ -0,0 +1,255 @@ +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#compiler#tectonic#init(options) abort " {{{1 + let l:compiler = deepcopy(s:compiler) + + call l:compiler.init(extend(a:options, + \ get(g:, 'vimtex_compiler_tectonic', {}), 'keep')) + + return l:compiler +endfunction + +" }}}1 + +let s:compiler = { + \ 'name' : 'tectonic', + \ 'backend' : has('nvim') ? 'nvim' + \ : v:version >= 800 ? 'jobs' : 'process', + \ 'root' : '', + \ 'target' : '', + \ 'target_path' : '', + \ 'background' : 1, + \ 'build_dir' : '', + \ 'output' : tempname(), + \ 'options' : [ + \ '--keep-logs', + \ '--synctex' + \ ], + \} + +function! s:compiler.init(options) abort dict " {{{1 + call extend(self, a:options) + + if !executable('tectonic') + call vimtex#log#warning('tectonic is not executable!') + throw 'vimtex: Requirements not met' + endif + + call extend(self, deepcopy(s:compiler_{self.backend})) + + " Processes run with the new jobs api will not run in the foreground + if self.backend !=# 'process' + let self.background = 1 + endif +endfunction + +" }}}1 + +function! s:compiler.build_cmd() abort dict " {{{1 + let l:cmd = 'tectonic' + + for l:opt in self.options + if l:opt =~# '^-\%(o\|-outdir\)' + call vimtex#log#warning("Don't use --outdir or -o in compiler options," + \ . ' use build_dir instead, see :help g:vimtex_compiler_tectonic' + \ . ' for more details') + continue + endif + + let l:cmd .= ' ' . l:opt + endfor + + if empty(self.build_dir) + let self.build_dir = fnamemodify(self.target_path, ':p:h') + elseif !isdirectory(self.build_dir) + call vimtex#log#warning( + \ "build_dir doesn't exist, it will be created: " . self.build_dir) + call mkdir(self.build_dir, 'p') + endif + + return l:cmd + \ . ' --outdir=' . self.build_dir + \ . ' ' . vimtex#util#shellescape(self.target) +endfunction + +" }}}1 +function! s:compiler.cleanup() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.pprint_items() abort dict " {{{1 + let l:configuration = [] + + if self.backend ==# 'process' + call add(l:configuration, ['background', self.background]) + endif + + call add(l:configuration, ['tectonic options', self.options]) + + let l:list = [] + call add(l:list, ['backend', self.backend]) + if self.background + call add(l:list, ['output', self.output]) + endif + + if self.target_path !=# b:vimtex.tex + call add(l:list, ['root', self.root]) + call add(l:list, ['target', self.target_path]) + endif + + call add(l:list, ['configuration', l:configuration]) + + if has_key(self, 'process') + call add(l:list, ['process', self.process]) + endif + + if has_key(self, 'job') + call add(l:list, ['cmd', self.cmd]) + endif + + return l:list +endfunction + +" }}}1 + +function! s:compiler.clean(...) abort dict " {{{1 + let l:files = ['synctex.gz', 'toc', 'out', 'aux', 'log'] + + " If a full clean is required + if a:0 > 0 && a:1 + call extend(l:intermediate, ['pdf']) + endif + + let l:basename = self.build_dir . '/' . fnamemodify(self.target_path, ':t:r') + call map(l:files, 'l:basename . v:val') + + call vimtex#process#run('rm -f ' . join(l:files)) + call vimtex#log#info('Compiler clean finished') +endfunction + +" }}}1 +function! s:compiler.start(...) abort dict " {{{1 + call self.exec() + + if self.background + call vimtex#log#info('Compiler started in background') + else + call vimtex#compiler#callback(!vimtex#qf#inquire(self.target)) + endif +endfunction + +" }}}1 +function! s:compiler.start_single() abort dict " {{{1 + call self.start() +endfunction + +" }}}1 +function! s:compiler.stop() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.is_running() abort dict " {{{1 + return 0 +endfunction + +" }}}1 +function! s:compiler.kill() abort dict " {{{1 + " Pass +endfunction + +" }}}1 +function! s:compiler.get_pid() abort dict " {{{1 + return 0 +endfunction + +" }}}1 + +let s:compiler_process = {} +function! s:compiler_process.exec() abort dict " {{{1 + let self.process = vimtex#process#new() + let self.process.name = 'tectonic' + let self.process.background = self.background + let self.process.workdir = self.root + let self.process.output = self.output + let self.process.cmd = self.build_cmd() + call self.process.run() +endfunction + +" }}}1 + +let s:compiler_jobs = {} +function! s:compiler_jobs.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + let l:options = { + \ 'out_io' : 'file', + \ 'err_io' : 'file', + \ 'out_name' : self.output, + \ 'err_name' : self.output, + \} + + let s:cb_target = self.target_path !=# b:vimtex.tex ? self.target_path : '' + let l:options.exit_cb = function('s:callback') + + if !empty(self.root) + let l:save_pwd = getcwd() + execute 'lcd' fnameescape(self.root) + endif + let self.job = job_start(l:cmd, l:options) + if !empty(self.root) + execute 'lcd' fnameescape(l:save_pwd) + endif +endfunction + +" }}}1 +function! s:callback(ch, msg) abort " {{{1 + call vimtex#compiler#callback(!vimtex#qf#inquire(s:cb_target)) +endfunction + +" }}}1 + +let s:compiler_nvim = {} +function! s:compiler_nvim.exec() abort dict " {{{1 + let self.cmd = self.build_cmd() + let l:cmd = has('win32') + \ ? 'cmd /s /c "' . self.cmd . '"' + \ : ['sh', '-c', self.cmd] + + let l:shell = { + \ 'on_stdout' : function('s:callback_nvim_output'), + \ 'on_stderr' : function('s:callback_nvim_output'), + \ 'on_exit' : function('s:callback_nvim_exit'), + \ 'cwd' : self.root, + \ 'target' : self.target_path, + \ 'output' : self.output, + \} + + let self.job = jobstart(l:cmd, l:shell) +endfunction + +" }}}1 +function! s:callback_nvim_output(id, data, event) abort dict " {{{1 + if !empty(a:data) + call writefile(filter(a:data, '!empty(v:val)'), self.output, 'a') + endif +endfunction + +" }}}1 +function! s:callback_nvim_exit(id, data, event) abort dict " {{{1 + let l:target = self.target !=# b:vimtex.tex ? self.target : '' + call vimtex#compiler#callback(!vimtex#qf#inquire(l:target)) +endfunction + +" }}}1 + +endif 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 diff --git a/autoload/vimtex/debug.vim b/autoload/vimtex/debug.vim new file mode 100644 index 00000000..1ddf3aa6 --- /dev/null +++ b/autoload/vimtex/debug.vim @@ -0,0 +1,114 @@ +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#debug#stacktrace(...) abort " {{{1 + " + " This function builds on Luc Hermite's answer on Stack Exchange: + " http://vi.stackexchange.com/a/6024/21 + " + + " + " Get stack and exception + " + if empty(v:throwpoint) + try + throw 'dummy' + catch + let l:stack = reverse(split(v:throwpoint, '\.\.'))[1:] + let l:exception = 'Manual stacktrace' + endtry + else + let l:stack = reverse(split(v:throwpoint, '\.\.')) + let l:exception = v:exception + endif + + " + " Build the quickfix entries + " + let l:qflist = [] + let l:files = {} + for l:func in l:stack + try + let [l:name, l:offset] = (l:func =~# '\S\+\[\d') + \ ? matchlist(l:func, '\(\S\+\)\[\(\d\+\)\]')[1:2] + \ : matchlist(l:func, '\(\S\+\), line \(\d\+\)')[1:2] + catch + let l:name = l:func + let l:offset = 0 + endtry + + if l:name =~# '\v(\<SNR\>|^)\d+_' + let l:sid = matchstr(l:name, '\v(\<SNR\>|^)\zs\d+\ze_') + let l:name = substitute(l:name, '\v(\<SNR\>|^)\d+_', 's:', '') + let l:filename = substitute( + \ vimtex#util#command('scriptnames')[l:sid-1], + \ '^\s*\d\+:\s*', '', '') + else + let l:func_name = l:name =~# '^\d\+$' ? '{' . l:name . '}' : l:name + let l:filename = matchstr( + \ vimtex#util#command('verbose function ' . l:func_name)[1], + \ v:lang[0:1] ==# 'en' + \ ? 'Last set from \zs.*\.vim' : '\f\+\.vim') + endif + + let l:filename = fnamemodify(l:filename, ':p') + if filereadable(l:filename) + if !has_key(l:files, l:filename) + let l:files[l:filename] = reverse(readfile(l:filename)) + endif + + if l:name =~# '^\d\+$' + let l:lnum = 0 + let l:output = vimtex#util#command('function {' . l:name . '}') + let l:text = substitute( + \ matchstr(l:output, '^\s*' . l:offset), + \ '^\d\+\s*', '', '') + else + let l:lnum = l:offset + len(l:files[l:filename]) + \ - match(l:files[l:filename], '^\s*fu\%[nction]!\=\s\+' . l:name .'(') + let l:lnum_rev = len(l:files[l:filename]) - l:lnum + let l:text = substitute(l:files[l:filename][l:lnum_rev], '^\s*', '', '') + endif + else + let l:filename = '' + let l:lnum = 0 + let l:text = '' + endif + + call add(l:qflist, { + \ 'filename': l:filename, + \ 'function': l:name, + \ 'lnum': l:lnum, + \ 'text': len(l:qflist) == 0 ? l:exception : l:text, + \ 'nr': len(l:qflist), + \}) + endfor + + " Fill in empty filenames + let l:prev_filename = '_' + call reverse(l:qflist) + for l:entry in l:qflist + if empty(l:entry.filename) + let l:entry.filename = l:prev_filename + endif + let l:prev_filename = l:entry.filename + endfor + call reverse(l:qflist) + + if a:0 > 0 + call setqflist(l:qflist) + execute 'copen' len(l:qflist) + 2 + wincmd p + endif + + return l:qflist +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/delim.vim b/autoload/vimtex/delim.vim new file mode 100644 index 00000000..1a9b965a --- /dev/null +++ b/autoload/vimtex/delim.vim @@ -0,0 +1,1096 @@ +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#delim#init_buffer() abort " {{{1 + nnoremap <silent><buffer> <plug>(vimtex-delim-toggle-modifier) + \ :<c-u>call <sid>operator_setup('toggle_modifier_next') + \ <bar> normal! <c-r>=v:count ? v:count : ''<cr>g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-delim-toggle-modifier-reverse) + \ :<c-u>call <sid>operator_setup('toggle_modifier_prev') + \ <bar> normal! <c-r>=v:count ? v:count : ''<cr>g@l<cr> + + xnoremap <silent><buffer> <plug>(vimtex-delim-toggle-modifier) + \ :<c-u>call vimtex#delim#toggle_modifier_visual()<cr> + + xnoremap <silent><buffer> <plug>(vimtex-delim-toggle-modifier-reverse) + \ :<c-u>call vimtex#delim#toggle_modifier_visual({'dir': -1})<cr> + + nnoremap <silent><buffer> <plug>(vimtex-delim-change-math) + \ :<c-u>call <sid>operator_setup('change')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-delim-delete) + \ :<c-u>call <sid>operator_setup('delete')<bar>normal! g@l<cr> + + inoremap <silent><buffer> <plug>(vimtex-delim-close) + \ <c-r>=vimtex#delim#close()<cr> +endfunction + +" }}}1 + +function! vimtex#delim#close() abort " {{{1 + let l:save_pos = vimtex#pos#get_cursor() + let l:posval_cursor = vimtex#pos#val(l:save_pos) + let l:posval_current = l:posval_cursor + let l:posval_last = l:posval_cursor + 1 + + while l:posval_current < l:posval_last + let l:open = vimtex#delim#get_prev('all', 'open', + \ { 'syn_exclude' : 'texComment' }) + 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) + call vimtex#pos#set_cursor(l:save_pos) + return l:open.corr + endif + + let l:posval_last = l:posval_current + let l:posval_current = vimtex#pos#val(l:open) + let l:posval_try = vimtex#pos#val(l:close) + strlen(l:close.match) + if l:posval_current != l:posval_cursor + \ && l:posval_try > l:posval_cursor + call vimtex#pos#set_cursor(l:save_pos) + return l:open.corr + endif + + call vimtex#pos#set_cursor(vimtex#pos#prev(l:open)) + endwhile + + call vimtex#pos#set_cursor(l:save_pos) + return '' +endfunction + +" }}}1 +function! vimtex#delim#toggle_modifier(...) abort " {{{1 + let l:args = a:0 > 0 ? a:1 : {} + call extend(l:args, { + \ 'count': v:count1, + \ 'dir': 1, + \ 'repeat': 1, + \ 'openclose': [], + \ }, 'keep') + + let [l:open, l:close] = !empty(l:args.openclose) + \ ? l:args.openclose + \ : vimtex#delim#get_surrounding('delim_modq_math') + if empty(l:open) | return | endif + + let l:direction = l:args.dir < 0 ? -l:args.count : l:args.count + + let newmods = ['', ''] + let modlist = [['', '']] + get(g:, 'vimtex_delim_toggle_mod_list', + \ [['\left', '\right']]) + let n = len(modlist) + for i in range(n) + let j = (i + l:direction) % n + if l:open.mod ==# modlist[i][0] + let newmods = modlist[j] + break + endif + endfor + + let line = getline(l:open.lnum) + let line = strpart(line, 0, l:open.cnum - 1) + \ . newmods[0] + \ . strpart(line, l:open.cnum + len(l:open.mod) - 1) + call setline(l:open.lnum, line) + + let l:cnum = l:close.cnum + if l:open.lnum == l:close.lnum + let n = len(newmods[0]) - len(l:open.mod) + let l:cnum += n + let pos = vimtex#pos#get_cursor() + if pos[2] > l:open.cnum + len(l:open.mod) + let pos[2] += n + call vimtex#pos#set_cursor(pos) + endif + endif + + let line = getline(l:close.lnum) + let line = strpart(line, 0, l:cnum - 1) + \ . newmods[1] + \ . strpart(line, l:cnum + len(l:close.mod) - 1) + call setline(l:close.lnum, line) + + return newmods +endfunction + +" }}}1 +function! vimtex#delim#toggle_modifier_visual(...) abort " {{{1 + let l:args = a:0 > 0 ? a:1 : {} + call extend(l:args, { + \ 'count': v:count1, + \ 'dir': 1, + \ 'reselect': 1, + \ }, 'keep') + + let l:save_pos = vimtex#pos#get_cursor() + let l:start_pos = getpos("'<") + let l:end_pos = getpos("'>") + let l:end_pos_val = vimtex#pos#val(l:end_pos) + 1000 + let l:cur_pos = l:start_pos + + " + " Check if selection is swapped + " + let l:end_pos[1] += 1 + call setpos("'>", l:end_pos) + let l:end_pos[1] -= 1 + normal! gv + let l:swapped = l:start_pos != getpos("'<") + + " + " First we generate a stack of all delimiters that should be toggled + " + let l:stack = [] + while vimtex#pos#val(l:cur_pos) < l:end_pos_val + call vimtex#pos#set_cursor(l:cur_pos) + let l:open = vimtex#delim#get_next('delim_modq_math', 'open') + if empty(l:open) | break | endif + + if vimtex#pos#val(l:open) >= l:end_pos_val + break + endif + + let l:close = vimtex#delim#get_matching(l:open) + if !empty(get(l:close, 'match')) + + if l:end_pos_val >= vimtex#pos#val(l:close) + strlen(l:close.match) - 1 + let l:newmods = vimtex#delim#toggle_modifier({ + \ 'repeat': 0, + \ 'count': l:args.count, + \ 'dir': l:args.dir, + \ 'openclose': [l:open, l:close], + \ }) + + let l:col_diff = (l:open.lnum == l:end_pos[1]) + \ ? strlen(newmods[0]) - strlen(l:open.mod) : 0 + let l:col_diff += (l:close.lnum == l:end_pos[1]) + \ ? strlen(newmods[1]) - strlen(l:close.mod) : 0 + + if l:col_diff != 0 + let l:end_pos[2] += l:col_diff + let l:end_pos_val += l:col_diff + endif + endif + endif + + let l:cur_pos = vimtex#pos#next(l:open) + endwhile + + " + " Finally we return to original position and reselect the region + " + call setpos(l:swapped? "'>" : "'<", l:start_pos) + call setpos(l:swapped? "'<" : "'>", l:end_pos) + call vimtex#pos#set_cursor(l:save_pos) + if l:args.reselect + normal! gv + endif +endfunction + +" }}}1 + +function! vimtex#delim#change(...) abort " {{{1 + let [l:open, l:close] = vimtex#delim#get_surrounding('delim_math') + if empty(l:open) | return | endif + + if a:0 > 0 + let l:new_delim = a:1 + else + let l:name = get(l:open, 'name', l:open.is_open + \ ? l:open.match . ' ... ' . l:open.corr + \ : l:open.match . ' ... ' . l:open.corr) + + let l:new_delim = vimtex#echo#input({ + \ 'info' : + \ ['Change surrounding delimiter: ', ['VimtexWarning', l:name]], + \ 'complete' : 'customlist,vimtex#delim#change_input_complete', + \}) + endif + + if empty(l:new_delim) | return | endif + call vimtex#delim#change_with_args(l:open, l:close, l:new_delim) +endfunction + +" }}}1 +function! vimtex#delim#change_with_args(open, close, new) abort " {{{1 + " + " Set target environment + " + if a:new ==# '' + let [l:beg, l:end] = ['', ''] + elseif index(['{', '}'], a:new) >= 0 + let [l:beg, l:end] = ['{', '}'] + else + let l:side = a:new =~# g:vimtex#delim#re.delim_math.close + let l:index = index(map(copy(g:vimtex#delim#lists.delim_math.name), + \ 'v:val[' . l:side . ']'), + \ a:new) + if l:index >= 0 + let [l:beg, l:end] = g:vimtex#delim#lists.delim_math.name[l:index] + else + let [l:beg, l:end] = [a:new, a:new] + endif + endif + + let l:line = getline(a:open.lnum) + call setline(a:open.lnum, + \ strpart(l:line, 0, a:open.cnum-1) + \ . l:beg + \ . strpart(l:line, a:open.cnum + len(a:open.match) - 1)) + + let l:c1 = a:close.cnum + let l:c2 = a:close.cnum + len(a:close.match) - 1 + if a:open.lnum == a:close.lnum + let n = len(l:beg) - len(a:open.match) + let l:c1 += n + let l:c2 += n + let pos = vimtex#pos#get_cursor() + if pos[2] > a:open.cnum + len(a:open.match) - 1 + let pos[2] += n + call vimtex#pos#set_cursor(pos) + endif + endif + + let l:line = getline(a:close.lnum) + call setline(a:close.lnum, + \ strpart(l:line, 0, l:c1-1) . l:end . strpart(l:line, l:c2)) +endfunction + +" }}}1 +function! vimtex#delim#change_input_complete(lead, cmdline, pos) abort " {{{1 + let l:all = deepcopy(g:vimtex#delim#lists.delim_all.name) + let l:open = map(copy(l:all), 'v:val[0]') + let l:close = map(copy(l:all), 'v:val[1]') + + let l:lead_re = escape(a:lead, '\$[]') + return filter(l:open + l:close, 'v:val =~# ''^' . l:lead_re . '''') +endfunction + +" }}}1 +function! vimtex#delim#delete() abort " {{{1 + let [l:open, l:close] = vimtex#delim#get_surrounding('delim_modq_math') + if empty(l:open) | return | endif + + call vimtex#delim#change_with_args(l:open, l:close, '') +endfunction + +" }}}1 + +function! vimtex#delim#get_next(type, side, ...) abort " {{{1 + return s:get_delim(extend({ + \ 'direction' : 'next', + \ 'type' : a:type, + \ 'side' : a:side, + \}, get(a:, '1', {}))) +endfunction + +" }}}1 +function! vimtex#delim#get_prev(type, side, ...) abort " {{{1 + return s:get_delim(extend({ + \ 'direction' : 'prev', + \ 'type' : a:type, + \ 'side' : a:side, + \}, get(a:, '1', {}))) +endfunction + +" }}}1 +function! vimtex#delim#get_current(type, side, ...) abort " {{{1 + return s:get_delim(extend({ + \ 'direction' : 'current', + \ 'type' : a:type, + \ 'side' : a:side, + \}, get(a:, '1', {}))) +endfunction + +" }}}1 +function! vimtex#delim#get_matching(delim) abort " {{{1 + if empty(a:delim) || !has_key(a:delim, 'lnum') | return {} | endif + + " + " Get the matching position + " + let l:save_pos = vimtex#pos#get_cursor() + call vimtex#pos#set_cursor(a:delim) + let [l:match, l:lnum, l:cnum] = a:delim.get_matching() + call vimtex#pos#set_cursor(l:save_pos) + + " + " Create the match result + " + let l:matching = deepcopy(a:delim) + let l:matching.lnum = l:lnum + let l:matching.cnum = l:cnum + let l:matching.match = l:match + let l:matching.corr = a:delim.match + let l:matching.side = a:delim.is_open ? 'close' : 'open' + let l:matching.is_open = !a:delim.is_open + let l:matching.re.corr = a:delim.re.this + let l:matching.re.this = a:delim.re.corr + + if l:matching.type ==# 'delim' + let l:matching.corr_delim = a:delim.delim + let l:matching.corr_mod = a:delim.mod + let l:matching.delim = a:delim.corr_delim + let l:matching.mod = a:delim.corr_mod + elseif l:matching.type ==# 'env' && has_key(l:matching, 'name') + if l:matching.is_open + let l:matching.env_cmd = vimtex#cmd#get_at(l:lnum, l:cnum) + else + unlet l:matching.env_cmd + endif + endif + + return l:matching +endfunction + +" }}}1 +function! vimtex#delim#get_surrounding(type) abort " {{{1 + let l:save_pos = vimtex#pos#get_cursor() + let l:pos_val_cursor = vimtex#pos#val(l:save_pos) + let l:pos_val_last = l:pos_val_cursor + let l:pos_val_open = l:pos_val_cursor - 1 + + while l:pos_val_open < l:pos_val_last + let l:open = vimtex#delim#get_prev(a:type, 'open') + if empty(l:open) | break | endif + + let l:close = vimtex#delim#get_matching(l:open) + let l:pos_val_try = vimtex#pos#val(l:close) + strlen(l:close.match) - 1 + if l:pos_val_try >= l:pos_val_cursor + call vimtex#pos#set_cursor(l:save_pos) + return [l:open, l:close] + else + call vimtex#pos#set_cursor(vimtex#pos#prev(l:open)) + let l:pos_val_last = l:pos_val_open + let l:pos_val_open = vimtex#pos#val(l:open) + endif + endwhile + + call vimtex#pos#set_cursor(l:save_pos) + return [{}, {}] +endfunction + +" }}}1 + +function! s:operator_setup(operator) abort " {{{1 + let &opfunc = s:snr() . 'operator_function' + + let s:operator = a:operator + + " Ask for user input if necessary/relevant + if s:operator ==# 'change' + let [l:open, l:close] = vimtex#delim#get_surrounding('delim_math') + if empty(l:open) | return | endif + + let l:name = get(l:open, 'name', l:open.is_open + \ ? l:open.match . ' ... ' . l:open.corr + \ : l:open.match . ' ... ' . l:open.corr) + + let s:operator_delim = vimtex#echo#input({ + \ 'info' : + \ ['Change surrounding delimiter: ', ['VimtexWarning', l:name]], + \ 'complete' : 'customlist,vimtex#delim#change_input_complete', + \}) + endif +endfunction + +" }}}1 +function! s:operator_function(_) abort " {{{1 + let l:delim = get(s:, 'operator_delim', '') + + execute 'call vimtex#delim#' . { + \ 'change': 'change(l:delim)', + \ 'delete': 'delete()', + \ 'toggle_modifier_next': 'toggle_modifier()', + \ 'toggle_modifier_prev': "toggle_modifier({'dir': -1})", + \}[s:operator] +endfunction + +" }}}1 +function! s:snr() abort " {{{1 + return matchstr(expand('<sfile>'), '<SNR>\d\+_') +endfunction + +" }}}1 +" + +function! s:get_delim(opts) abort " {{{1 + " + " Arguments: + " opts = { + " 'direction' : next + " prev + " current + " 'type' : env_tex + " env_math + " env_all + " delim_tex + " delim_math + " delim_modq_math (possibly modified math delimiter) + " delim_mod_math (modified math delimiter) + " delim_all + " all + " 'side' : open + " close + " both + " 'syn_exclude' : Don't match in given syntax + " } + " + " Returns: + " delim = { + " type : env | delim + " side : open | close + " name : name of environment [only for type env] + " lnum : number + " cnum : number + " match : unparsed matched delimiter + " corr : corresponding delimiter + " re : { + " open : regexp for the opening part + " close : regexp for the closing part + " } + " remove : method to remove the delimiter + " } + " + let l:save_pos = vimtex#pos#get_cursor() + let l:re = g:vimtex#delim#re[a:opts.type][a:opts.side] + while 1 + let [l:lnum, l:cnum] = a:opts.direction ==# 'next' + \ ? searchpos(l:re, 'cnW', line('.') + g:vimtex_delim_stopline) + \ : a:opts.direction ==# 'prev' + \ ? searchpos(l:re, 'bcnW', max([line('.') - g:vimtex_delim_stopline, 1])) + \ : searchpos(l:re, 'bcnW', line('.')) + if l:lnum == 0 | break | endif + + if has_key(a:opts, 'syn_exclude') + \ && vimtex#util#in_syntax(a:opts.syn_exclude, l:lnum, l:cnum) + call vimtex#pos#set_cursor(vimtex#pos#prev(l:lnum, l:cnum)) + continue + endif + + break + endwhile + call vimtex#pos#set_cursor(l:save_pos) + + let l:match = matchstr(getline(l:lnum), '^' . l:re, l:cnum-1) + + if a:opts.direction ==# 'current' + \ && l:cnum + strlen(l:match) + (mode() ==# 'i' ? 1 : 0) <= col('.') + let l:match = '' + let l:lnum = 0 + let l:cnum = 0 + endif + + let l:result = { + \ 'type' : '', + \ 'lnum' : l:lnum, + \ 'cnum' : l:cnum, + \ 'match' : l:match, + \ 'remove' : function('s:delim_remove'), + \} + + for l:type in s:types + if l:match =~# '^' . l:type.re + let l:result = extend( + \ l:type.parser(l:match, l:lnum, l:cnum, + \ a:opts.side, a:opts.type, a:opts.direction), + \ l:result, 'keep') + break + endif + endfor + + return empty(l:result.type) ? {} : l:result +endfunction + +" }}}1 + +function! s:delim_remove() dict abort " {{{1 + let l:line = getline(self.lnum) + let l:l1 = strpart(l:line, 0, self.cnum-1) + let l:l2 = strpart(l:line, self.cnum + strlen(self.match) - 1) + + if self.side ==# 'close' + let l:l1 = substitute(l:l1, '\s\+$', '', '') + if empty(l:l1) + let l:l2 = substitute(l:l2, '^\s\+', '', '') + endif + else + let l:l2 = substitute(l:l2, '^\s\+', '', '') + if empty(l:l2) + let l:l1 = substitute(l:l1, '\s\+$', '', '') + endif + endif + + call setline(self.lnum, l:l1 . l:l2) +endfunction + +" }}}1 + +function! s:parser_env(match, lnum, cnum, ...) abort " {{{1 + let result = {} + + let result.type = 'env' + let result.name = matchstr(a:match, '{\zs\k*\ze\*\?}') + let result.starred = match(a:match, '\*}$') > 0 + let result.side = a:match =~# '\\begin' ? 'open' : 'close' + let result.is_open = result.side ==# 'open' + let result.get_matching = function('s:get_matching_env') + + let result.gms_flags = result.is_open ? 'nW' : 'bnW' + let result.gms_stopline = result.is_open + \ ? line('.') + g:vimtex_delim_stopline + \ : max([1, line('.') - g:vimtex_delim_stopline]) + + if result.is_open + let result.env_cmd = vimtex#cmd#get_at(a:lnum, a:cnum) + endif + + let result.corr = result.is_open + \ ? substitute(a:match, 'begin', 'end', '') + \ : substitute(a:match, 'end', 'begin', '') + + let result.re = { + \ 'open' : '\m\\begin\s*{' . result.name . '\*\?}', + \ 'close' : '\m\\end\s*{' . result.name . '\*\?}', + \} + + let result.re.this = result.is_open ? result.re.open : result.re.close + let result.re.corr = result.is_open ? result.re.close : result.re.open + + return result +endfunction + +" }}}1 +function! s:parser_tex(match, lnum, cnum, side, type, direction) abort " {{{1 + " + " TeX shorthand are these + " + " $ ... $ (inline math) + " $$ ... $$ (displayed equations) + " + " The notation does not provide the delimiter side directly, which provides + " a slight problem. However, we can utilize the syntax information to parse + " the side. + " + let result = {} + let result.type = 'env' + let result.corr = a:match + let result.re = { + \ 'this' : '\m' . escape(a:match, '$'), + \ 'corr' : '\m' . escape(a:match, '$'), + \ 'open' : '\m' . escape(a:match, '$'), + \ 'close' : '\m' . escape(a:match, '$'), + \} + let result.side = vimtex#util#in_syntax( + \ (a:match ==# '$' ? 'texMathZoneX' : 'texMathZoneY'), + \ a:lnum, a:cnum+1) + \ ? 'open' : 'close' + let result.is_open = result.side ==# 'open' + let result.get_matching = function('s:get_matching_tex') + let result.gms_flags = result.is_open ? 'nW' : 'bnW' + let result.gms_stopline = result.is_open + \ ? line('.') + g:vimtex_delim_stopline + \ : max([1, line('.') - g:vimtex_delim_stopline]) + + if (a:side !=# 'both') && (a:side !=# result.side) + " + " The current match ($ or $$) is not the correct side, so we must + " continue the search recursively. We do this by changing the cursor + " position, since the function searchpos relies on the current cursor + " position. + " + let l:save_pos = vimtex#pos#get_cursor() + + " Move the cursor + call vimtex#pos#set_cursor(a:direction ==# 'next' + \ ? vimtex#pos#next(a:lnum, a:cnum) + \ : vimtex#pos#prev(a:lnum, a:cnum)) + + " Get new result + let result = s:get_delim({ + \ 'direction' : a:direction, + \ 'type' : a:type, + \ 'side' : a:side, + \}) + + " Restore the cursor + call vimtex#pos#set_cursor(l:save_pos) + endif + + return result +endfunction + +" }}}1 +function! s:parser_latex(match, lnum, cnum, ...) abort " {{{1 + let result = {} + + let result.type = 'env' + let result.side = a:match =~# '\\(\|\\\[' ? 'open' : 'close' + let result.is_open = result.side ==# 'open' + let result.get_matching = function('s:get_matching_latex') + let result.gms_flags = result.is_open ? 'nW' : 'bnW' + let result.gms_stopline = result.is_open + \ ? line('.') + g:vimtex_delim_stopline + \ : max([1, line('.') - g:vimtex_delim_stopline]) + + let result.corr = result.is_open + \ ? substitute(substitute(a:match, '\[', ']', ''), '(', ')', '') + \ : substitute(substitute(a:match, '\]', '[', ''), ')', '(', '') + + let result.re = { + \ 'open' : g:vimtex#re#not_bslash + \ . (a:match =~# '\\(\|\\)' ? '\m\\(' : '\m\\\['), + \ 'close' : g:vimtex#re#not_bslash + \ . (a:match =~# '\\(\|\\)' ? '\m\\)' : '\m\\\]'), + \} + + let result.re.this = result.is_open ? result.re.open : result.re.close + let result.re.corr = result.is_open ? result.re.close : result.re.open + + return result +endfunction + +" }}}1 +function! s:parser_delim(match, lnum, cnum, ...) abort " {{{1 + let result = {} + let result.type = 'delim' + let result.side = + \ a:match =~# g:vimtex#delim#re.delim_all.open ? 'open' : 'close' + let result.is_open = result.side ==# 'open' + let result.get_matching = function('s:get_matching_delim') + let result.gms_flags = result.is_open ? 'nW' : 'bnW' + let result.gms_stopline = result.is_open + \ ? line('.') + g:vimtex_delim_stopline + \ : max([1, line('.') - g:vimtex_delim_stopline]) + + " + " Find corresponding delimiter and the regexps + " + if a:match =~# '^' . g:vimtex#delim#re.mods.both + let m1 = matchstr(a:match, '^' . g:vimtex#delim#re.mods.both) + let d1 = substitute(strpart(a:match, len(m1)), '^\s*', '', '') + let s1 = !result.is_open + let re1 = s:parser_delim_get_regexp(m1, s1, 'mods') + \ . '\s*' . s:parser_delim_get_regexp(d1, s1, 'delim_math') + + let m2 = s:parser_delim_get_corr(m1, 'mods') + let d2 = s:parser_delim_get_corr(d1, 'delim_math') + let s2 = result.is_open + let re2 = s:parser_delim_get_regexp(m2, s2, 'mods') . '\s*' + \ . (m1 =~# '\\\%(left\|right\)' + \ ? '\%(' . s:parser_delim_get_regexp(d2, s2, 'delim_math') . '\|\.\)' + \ : s:parser_delim_get_regexp(d2, s2, 'delim_math')) + else + let d1 = a:match + let m1 = '' + let re1 = s:parser_delim_get_regexp(a:match, !result.is_open) + + let d2 = s:parser_delim_get_corr(a:match) + let m2 = '' + let re2 = s:parser_delim_get_regexp(d2, result.is_open) + endif + + let result.delim = d1 + let result.mod = m1 + let result.corr = m2 . d2 + let result.corr_delim = d2 + let result.corr_mod = m2 + let result.re = { + \ 'this' : re1, + \ 'corr' : re2, + \ 'open' : result.is_open ? re1 : re2, + \ 'close' : result.is_open ? re2 : re1, + \} + + return result +endfunction + +" }}}1 +function! s:parser_delim_unmatched(match, lnum, cnum, ...) abort " {{{1 + let result = {} + let result.type = 'delim' + let result.side = + \ a:match =~# g:vimtex#delim#re.delim_all.open ? 'open' : 'close' + let result.is_open = result.side ==# 'open' + let result.get_matching = function('s:get_matching_delim_unmatched') + let result.gms_flags = result.is_open ? 'nW' : 'bnW' + let result.gms_stopline = result.is_open + \ ? line('.') + g:vimtex_delim_stopline + \ : max([1, line('.') - g:vimtex_delim_stopline]) + let result.delim = '.' + let result.corr_delim = '.' + + " + " Find corresponding delimiter and the regexps + " + if result.is_open + let result.mod = '\left' + let result.corr_mod = '\right' + let result.corr = '\right.' + let re1 = '\\left\s*\.' + let re2 = s:parser_delim_get_regexp('\right', 1, 'mods') + \ . '\s*' . s:parser_delim_get_regexp('.', 0) + else + let result.mod = '\right' + let result.corr_mod = '\left' + let result.corr = '\left.' + let re1 = '\\right\s*\.' + let re2 = s:parser_delim_get_regexp('\left', 0, 'mods') + \ . '\s*' . s:parser_delim_get_regexp('.', 0) + endif + + let result.re = { + \ 'this' : re1, + \ 'corr' : re2, + \ 'open' : result.is_open ? re1 : re2, + \ 'close' : result.is_open ? re2 : re1, + \} + + return result +endfunction + +" }}}1 +function! s:parser_delim_get_regexp(delim, side, ...) abort " {{{1 + let l:type = a:0 > 0 ? a:1 : 'delim_all' + + " First check for unmatched math delimiter + if a:delim ==# '.' + return g:vimtex#delim#re.delim_math[a:side ? 'open' : 'close'] + endif + + " Next check normal delimiters + let l:index = index(map(copy(g:vimtex#delim#lists[l:type].name), + \ 'v:val[' . a:side . ']'), + \ a:delim) + return l:index >= 0 + \ ? g:vimtex#delim#lists[l:type].re[l:index][a:side] + \ : '' +endfunction + +" }}}1 +function! s:parser_delim_get_corr(delim, ...) abort " {{{1 + let l:type = a:0 > 0 ? a:1 : 'delim_all' + + for l:pair in g:vimtex#delim#lists[l:type].name + if a:delim ==# l:pair[0] + return l:pair[1] + elseif a:delim ==# l:pair[1] + return l:pair[0] + endif + endfor +endfunction + +" }}}1 + +function! s:get_matching_env() dict abort " {{{1 + try + let [lnum, cnum] = searchpairpos(self.re.open, '', self.re.close, + \ self.gms_flags, '', 0, s:get_timeout()) + catch /E118/ + let [lnum, cnum] = searchpairpos(self.re.open, '', self.re.close, + \ self.gms_flags, '', self.gms_stopline) + endtry + + let match = matchstr(getline(lnum), '^' . self.re.corr, cnum-1) + return [match, lnum, cnum] +endfunction + +" }}}1 +function! s:get_matching_tex() dict abort " {{{1 + let [lnum, cnum] = searchpos(self.re.corr, self.gms_flags, self.gms_stopline) + + let match = matchstr(getline(lnum), '^' . self.re.corr, cnum-1) + return [match, lnum, cnum] +endfunction + +" }}}1 +function! s:get_matching_latex() dict abort " {{{1 + let [lnum, cnum] = searchpos(self.re.corr, self.gms_flags, self.gms_stopline) + + let match = matchstr(getline(lnum), '^' . self.re.corr, cnum-1) + return [match, lnum, cnum] +endfunction + +" }}}1 +function! s:get_matching_delim() dict abort " {{{1 + try + let [lnum, cnum] = searchpairpos(self.re.open, '', self.re.close, + \ self.gms_flags, + \ 'synIDattr(synID(line("."), col("."), 0), "name") =~? "comment"', + \ 0, s:get_timeout()) + catch /E118/ + let [lnum, cnum] = searchpairpos(self.re.open, '', self.re.close, + \ self.gms_flags, + \ 'synIDattr(synID(line("."), col("."), 0), "name") =~? "comment"', + \ self.gms_stopline) + endtry + + let match = matchstr(getline(lnum), '^' . self.re.corr, cnum-1) + return [match, lnum, cnum] +endfunction + +" }}}1 +function! s:get_matching_delim_unmatched() dict abort " {{{1 + let tries = 0 + let misses = [] + while 1 + try + let [lnum, cnum] = searchpairpos(self.re.open, '', self.re.close, + \ self.gms_flags, + \ 'index(misses, [line("."), col(".")]) >= 0', + \ 0, s:get_timeout()) + catch /E118/ + let [lnum, cnum] = searchpairpos(self.re.open, '', self.re.close, + \ self.gms_flags, + \ 'index(misses, [line("."), col(".")]) >= 0', + \ self.gms_stopline) + endtry + let match = matchstr(getline(lnum), '^' . self.re.corr, cnum-1) + if lnum == 0 | break | endif + + let cand = vimtex#delim#get_matching(extend({ + \ 'type' : '', + \ 'lnum' : lnum, + \ 'cnum' : cnum, + \ 'match' : match, + \}, s:parser_delim(match, lnum, cnum))) + + if !empty(cand) && [self.lnum, self.cnum] == [cand.lnum, cand.cnum] + return [match, lnum, cnum] + else + let misses += [[lnum, cnum]] + let tries += 1 + if tries == 10 | break | endif + endif + endwhile + + return ['', 0, 0] +endfunction + +" }}}1 + +function! s:get_timeout() abort " {{{1 + return (empty(v:insertmode) ? mode() : v:insertmode) ==# 'i' + \ ? g:vimtex_delim_insert_timeout + \ : g:vimtex_delim_timeout +endfunction + +" }}}1 + + +function! s:init_delim_lists() abort " {{{1 + " Define the default value + let l:lists = { + \ 'env_tex' : { + \ 'name' : [['begin', 'end']], + \ 're' : [['\\begin\s*{[^}]*}', '\\end\s*{[^}]*}']], + \ }, + \ 'env_math' : { + \ 'name' : [ + \ ['\(', '\)'], + \ ['\[', '\]'], + \ ['$$', '$$'], + \ ['$', '$'], + \ ], + \ 're' : [ + \ ['\\(', '\\)'], + \ ['\\\@<!\\\[', '\\\]'], + \ ['\$\$', '\$\$'], + \ ['\$', '\$'], + \ ], + \ }, + \ 'delim_tex' : { + \ 'name' : [ + \ ['[', ']'], + \ ['{', '}'], + \ ], + \ 're' : [ + \ ['\[', '\]'], + \ ['\\\@<!{', '\\\@<!}'], + \ ] + \ }, + \ 'delim_math' : { + \ 'name' : [ + \ ['(', ')'], + \ ['[', ']'], + \ ['\{', '\}'], + \ ['\langle', '\rangle'], + \ ['\lbrace', '\rbrace'], + \ ['\lvert', '\rvert'], + \ ['\lVert', '\rVert'], + \ ['\lfloor', '\rfloor'], + \ ['\lceil', '\rceil'], + \ ['\ulcorner', '\urcorner'], + \ ] + \ }, + \ 'mods' : { + \ 'name' : [ + \ ['\left', '\right'], + \ ['\bigl', '\bigr'], + \ ['\Bigl', '\Bigr'], + \ ['\biggl', '\biggr'], + \ ['\Biggl', '\Biggr'], + \ ['\big', '\big'], + \ ['\Big', '\Big'], + \ ['\bigg', '\bigg'], + \ ['\Bigg', '\Bigg'], + \ ], + \ 're' : [ + \ ['\\left', '\\right'], + \ ['\\bigl', '\\bigr'], + \ ['\\Bigl', '\\Bigr'], + \ ['\\biggl', '\\biggr'], + \ ['\\Biggl', '\\Biggr'], + \ ['\\big\>', '\\big\>'], + \ ['\\Big\>', '\\Big\>'], + \ ['\\bigg\>', '\\bigg\>'], + \ ['\\Bigg\>', '\\Bigg\>'], + \ ] + \ }, + \} + + " Get user defined lists + call extend(l:lists, get(g:, 'vimtex_delim_list', {})) + + " Generate corresponding regexes if necessary + for l:type in values(l:lists) + if !has_key(l:type, 're') && has_key(l:type, 'name') + let l:type.re = map(deepcopy(l:type.name), + \ 'map(v:val, ''escape(v:val, ''''\$[]'''')'')') + endif + endfor + + " Generate combined lists + let l:lists.env_all = {} + let l:lists.delim_all = {} + let l:lists.all = {} + for k in ['name', 're'] + let l:lists.env_all[k] = l:lists.env_tex[k] + l:lists.env_math[k] + let l:lists.delim_all[k] = l:lists.delim_math[k] + l:lists.delim_tex[k] + let l:lists.all[k] = l:lists.env_all[k] + l:lists.delim_all[k] + endfor + + return l:lists +endfunction + +" }}}1 +function! s:init_delim_regexes() abort " {{{1 + let l:re = {} + let l:re.env_all = {} + let l:re.delim_all = {} + let l:re.all = {} + + let l:re.env_tex = s:init_delim_regexes_generator('env_tex') + let l:re.env_math = s:init_delim_regexes_generator('env_math') + let l:re.delim_tex = s:init_delim_regexes_generator('delim_tex') + let l:re.delim_math = s:init_delim_regexes_generator('delim_math') + let l:re.mods = s:init_delim_regexes_generator('mods') + + let l:o = join(map(copy(g:vimtex#delim#lists.delim_math.re), 'v:val[0]'), '\|') + let l:c = join(map(copy(g:vimtex#delim#lists.delim_math.re), 'v:val[1]'), '\|') + + " + " Matches modified math delimiters + " + let l:re.delim_mod_math = { + \ 'open' : '\%(\%(' . l:re.mods.open . '\)\)\s*\\\@<!\%(' + \ . l:o . '\)\|\\left\s*\.', + \ 'close' : '\%(\%(' . l:re.mods.close . '\)\)\s*\\\@<!\%(' + \ . l:c . '\)\|\\right\s*\.', + \ 'both' : '\%(\%(' . l:re.mods.both . '\)\)\s*\\\@<!\%(' + \ . l:o . '\|' . l:c . '\)\|\\\%(left\|right\)\s*\.', + \} + + " + " Matches possibly modified math delimiters + " + let l:re.delim_modq_math = { + \ 'open' : '\%(\%(' . l:re.mods.open . '\)\s*\)\?\\\@<!\%(' + \ . l:o . '\)\|\\left\s*\.', + \ 'close' : '\%(\%(' . l:re.mods.close . '\)\s*\)\?\\\@<!\%(' + \ . l:c . '\)\|\\right\s*\.', + \ 'both' : '\%(\%(' . l:re.mods.both . '\)\s*\)\?\\\@<!\%(' + \ . l:o . '\|' . l:c . '\)\|\\\%(left\|right\)\s*\.', + \} + + for k in ['open', 'close', 'both'] + let l:re.env_all[k] = l:re.env_tex[k] . '\|' . l:re.env_math[k] + let l:re.delim_all[k] = l:re.delim_modq_math[k] . '\|' . l:re.delim_tex[k] + let l:re.all[k] = l:re.env_all[k] . '\|' . l:re.delim_all[k] + endfor + + " + " Be explicit about regex mode (set magic mode) + " + for l:type in values(l:re) + for l:side in ['open', 'close', 'both'] + let l:type[l:side] = '\m' . l:type[l:side] + endfor + endfor + + return l:re +endfunction + +" }}}1 +function! s:init_delim_regexes_generator(list_name) abort " {{{1 + let l:list = g:vimtex#delim#lists[a:list_name] + let l:open = join(map(copy(l:list.re), 'v:val[0]'), '\|') + let l:close = join(map(copy(l:list.re), 'v:val[1]'), '\|') + + return { + \ 'open' : '\\\@<!\%(' . l:open . '\)', + \ 'close' : '\\\@<!\%(' . l:close . '\)', + \ 'both' : '\\\@<!\%(' . l:open . '\|' . l:close . '\)' + \} +endfunction + + " }}}1 + + +" {{{1 Initialize module + +" +" Initialize lists of delimiter pairs and regexes +" +let g:vimtex#delim#lists = s:init_delim_lists() +let g:vimtex#delim#re = s:init_delim_regexes() + +" +" Initialize script variables +" +let s:types = [ + \ { + \ 're' : '\\\%(begin\|end\)\>', + \ 'parser' : function('s:parser_env'), + \ }, + \ { + \ 're' : '\$\$\?', + \ 'parser' : function('s:parser_tex'), + \ }, + \ { + \ 're' : '\\\%((\|)\|\[\|\]\)', + \ 'parser' : function('s:parser_latex'), + \ }, + \ { + \ 're' : '\\\%(left\|right\)\s*\.', + \ 'parser' : function('s:parser_delim_unmatched'), + \ }, + \ { + \ 're' : g:vimtex#delim#re.delim_all.both, + \ 'parser' : function('s:parser_delim'), + \ }, + \] + +" }}}1 + +endif diff --git a/autoload/vimtex/doc.vim b/autoload/vimtex/doc.vim new file mode 100644 index 00000000..dd42a6ed --- /dev/null +++ b/autoload/vimtex/doc.vim @@ -0,0 +1,251 @@ +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#doc#init_buffer() abort " {{{1 + command! -buffer -nargs=? VimtexDocPackage call vimtex#doc#package(<q-args>) + + nnoremap <buffer> <plug>(vimtex-doc-package) :VimtexDocPackage<cr> +endfunction + +" }}}1 + +function! vimtex#doc#package(word) abort " {{{1 + let l:context = empty(a:word) + \ ? s:packages_get_from_cursor() + \ : { + \ 'type': 'word', + \ 'candidates': [a:word], + \ } + if empty(l:context) | return | endif + + call s:packages_remove_invalid(l:context) + + for l:handler in g:vimtex_doc_handlers + if exists('*' . l:handler) + if call(l:handler, [l:context]) | return | endif + endif + endfor + + call s:packages_open(l:context) +endfunction + +" }}}1 +function! vimtex#doc#make_selection(context) abort " {{{1 + if has_key(a:context, 'selected') | return | endif + + if len(a:context.candidates) == 0 + if exists('a:context.name') + echohl ErrorMsg + echo 'Sorry, no doc for '.a:context.name + echohl NONE + endif + let a:context.selected = '' + return + endif + + if len(a:context.candidates) == 1 + let a:context.selected = a:context.candidates[0] + return + endif + + call vimtex#echo#echo('Multiple candidates detected, please select one:') + let l:count = 0 + for l:package in a:context.candidates + let l:count += 1 + call vimtex#echo#formatted([ + \ ' [' . string(l:count) . '] ', + \ ['VimtexSuccess', l:package] + \]) + endfor + + call vimtex#echo#echo('Type number (everything else cancels): ') + let l:choice = nr2char(getchar()) + if l:choice !~# '\d' + \ || l:choice == 0 + \ || l:choice > len(a:context.candidates) + echohl VimtexWarning + echon l:choice =~# '\d' ? l:choice : '-' + echohl NONE + let a:context.selected = '' + else + echon l:choice + let a:context.selected = a:context.candidates[l:choice-1] + let a:context.ask_before_open = 0 + endif +endfunction + +" }}}1 + +function! s:packages_get_from_cursor() abort " {{{1 + let l:cmd = vimtex#cmd#get_current() + if empty(l:cmd) | return {} | endif + + if l:cmd.name ==# '\usepackage' + return s:packages_from_usepackage(l:cmd) + elseif l:cmd.name ==# '\documentclass' + return s:packages_from_documentclass(l:cmd) + else + return s:packages_from_command(strpart(l:cmd.name, 1)) + endif +endfunction + +" }}}1 +function! s:packages_from_usepackage(cmd) abort " {{{1 + try + " Gather and clean up candidate list + let l:candidates = substitute(a:cmd.args[0].text, '%.\{-}\n', '', 'g') + let l:candidates = substitute(l:candidates, '\s*', '', 'g') + let l:candidates = split(l:candidates, ',') + + let l:context = { + \ 'type': 'usepackage', + \ 'candidates': l:candidates, + \} + + let l:cword = expand('<cword>') + if len(l:context.candidates) > 1 && index(l:context.candidates, l:cword) >= 0 + let l:context.selected = l:cword + endif + + return l:context + catch + call vimtex#log#warning('Could not parse the package from \usepackage!') + return {} + endtry +endfunction + +" }}}1 +function! s:packages_from_documentclass(cmd) abort " {{{1 + try + return { + \ 'type': 'documentclass', + \ 'candidates': [a:cmd.args[0].text], + \} + catch + call vimtex#log#warning('Could not parse the package from \documentclass!') + return {} + endtry +endfunction + +" }}}1 +function! s:packages_from_command(cmd) abort " {{{1 + let l:packages = [ + \ 'default', + \ 'class-' . get(b:vimtex, 'documentclass', ''), + \] + keys(b:vimtex.packages) + call filter(l:packages, 'filereadable(s:complete_dir . v:val)') + + let l:queue = copy(l:packages) + while !empty(l:queue) + let l:current = remove(l:queue, 0) + let l:includes = filter(readfile(s:complete_dir . l:current), 'v:val =~# ''^\#\s*include:''') + if empty(l:includes) | continue | endif + + call map(l:includes, 'matchstr(v:val, ''include:\s*\zs.*\ze\s*$'')') + call filter(l:includes, 'filereadable(s:complete_dir . v:val)') + call filter(l:includes, 'index(l:packages, v:val) < 0') + + let l:packages += l:includes + let l:queue += l:includes + endwhile + + let l:candidates = [] + let l:filter = 'v:val =~# ''^' . a:cmd . '\>''' + for l:package in l:packages + let l:cmds = filter(readfile(s:complete_dir . l:package), l:filter) + if empty(l:cmds) | continue | endif + + if l:package ==# 'default' + call extend(l:candidates, ['latex2e', 'lshort']) + else + call add(l:candidates, substitute(l:package, '^class-', '', '')) + endif + endfor + + return { + \ 'type': 'command', + \ 'name': a:cmd, + \ 'candidates': l:candidates, + \} +endfunction + +" }}}1 +function! s:packages_remove_invalid(context) abort " {{{1 + let l:invalid_packages = filter(copy(a:context.candidates), + \ 'empty(vimtex#kpsewhich#find(v:val . ''.sty'')) && ' + \ . 'empty(vimtex#kpsewhich#find(v:val . ''.cls''))') + + call filter(l:invalid_packages, + \ 'index([''latex2e'', ''lshort''], v:val) < 0') + + " Warn about invalid candidates + if !empty(l:invalid_packages) + if len(l:invalid_packages) == 1 + call vimtex#log#warning( + \ 'Package not recognized: ' . l:invalid_packages[0]) + else + call vimtex#log#warning( + \ 'Packages not recognized:', + \ map(copy(l:invalid_packages), "'- ' . v:val")) + endif + endif + + " Remove invalid candidates + call filter(a:context.candidates, 'index(l:invalid_packages, v:val) < 0') + + " Reset the selection if the selected candidate is not valid + if has_key(a:context, 'selected') + \ && index(a:context.candidates, a:context.selected) < 0 + unlet a:context.selected + endif +endfunction + +" }}}1 +function! s:packages_open(context) abort " {{{1 + if !has_key(a:context, 'selected') + call vimtex#doc#make_selection(a:context) + endif + + if empty(a:context.selected) | return | endif + + if get(a:context, 'ask_before_open', 1) + call vimtex#echo#formatted([ + \ 'Open documentation for ', + \ ['VimtexSuccess', a:context.selected], ' [y/N]? ' + \]) + + let l:choice = nr2char(getchar()) + if l:choice ==# 'y' + echon 'y' + else + echohl VimtexWarning + echon l:choice =~# '\w' ? l:choice : 'N' + echohl NONE + return + endif + endif + + let l:os = vimtex#util#get_os() + let l:url = 'http://texdoc.net/pkg/' . a:context.selected + + silent execute (l:os ==# 'linux' + \ ? '!xdg-open' + \ : (l:os ==# 'mac' + \ ? '!open' + \ : '!start')) + \ . ' ' . l:url + \ . (l:os ==# 'win' ? '' : ' &') + + redraw! +endfunction + +" }}}1 + +let s:complete_dir = fnamemodify(expand('<sfile>'), ':h') . '/complete/' + +endif diff --git a/autoload/vimtex/echo.vim b/autoload/vimtex/echo.vim new file mode 100644 index 00000000..9c761f8f --- /dev/null +++ b/autoload/vimtex/echo.vim @@ -0,0 +1,121 @@ +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#echo#echo(message) abort " {{{1 + echohl VimtexMsg + echo a:message + echohl None +endfunction + +" }}}1 +function! vimtex#echo#input(opts) abort " {{{1 + if g:vimtex_echo_verbose_input + \ && has_key(a:opts, 'info') + call vimtex#echo#formatted(a:opts.info) + endif + + let l:args = [get(a:opts, 'prompt', '> ')] + let l:args += [get(a:opts, 'default', '')] + if has_key(a:opts, 'complete') + let l:args += [a:opts.complete] + endif + + echohl VimtexMsg + let l:reply = call('input', l:args) + echohl None + return l:reply +endfunction + +" }}}1 +function! vimtex#echo#choose(list_or_dict, prompt) abort " {{{1 + if empty(a:list_or_dict) | return '' | endif + + return type(a:list_or_dict) == type({}) + \ ? s:choose_dict(a:list_or_dict, a:prompt) + \ : s:choose_list(a:list_or_dict, a:prompt) +endfunction + +" }}}1 +function! vimtex#echo#formatted(parts) abort " {{{1 + echo '' + try + for part in a:parts + if type(part) == type('') + echohl VimtexMsg + echon part + else + execute 'echohl' part[0] + echon part[1] + endif + unlet part + endfor + finally + echohl None + endtry +endfunction + +" }}}1 + +function! s:choose_dict(dict, prompt) abort " {{{1 + if len(a:dict) == 1 + return values(a:dict)[0] + endif + + while v:true + redraw! + if !empty(a:prompt) + echohl VimtexMsg + unsilent echo a:prompt + echohl None + endif + + let l:choice = 0 + for l:x in values(a:dict) + let l:choice += 1 + unsilent call vimtex#echo#formatted([['VimtexWarning', l:choice], ': ', l:x]) + endfor + + try + let l:choice = str2nr(input('> ')) - 1 + if l:choice >= 0 && l:choice < len(a:dict) + return keys(a:dict)[l:choice] + endif + endtry + endwhile +endfunction + +" }}}1 +function! s:choose_list(list, prompt) abort " {{{1 + if len(a:list) == 1 | return a:list[0] | endif + + while v:true + redraw! + if !empty(a:prompt) + echohl VimtexMsg + unsilent echo a:prompt + echohl None + endif + + let l:choice = 0 + for l:x in a:list + let l:choice += 1 + unsilent call vimtex#echo#formatted([['VimtexWarning', l:choice], ': ', l:x]) + endfor + + try + let l:choice = str2nr(input('> ')) - 1 + if l:choice >= 0 && l:choice < len(a:list) + return a:list[l:choice] + endif + endtry + endwhile +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/env.vim b/autoload/vimtex/env.vim new file mode 100644 index 00000000..9576397f --- /dev/null +++ b/autoload/vimtex/env.vim @@ -0,0 +1,202 @@ +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#env#init_buffer() abort " {{{1 + nnoremap <silent><buffer> <plug>(vimtex-env-delete) + \ :<c-u>call <sid>operator_setup('delete', 'env_tex')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-env-change) + \ :<c-u>call <sid>operator_setup('change', 'env_tex')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-env-delete-math) + \ :<c-u>call <sid>operator_setup('delete', 'env_math')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-env-change-math) + \ :<c-u>call <sid>operator_setup('change', 'env_math')<bar>normal! g@l<cr> + + nnoremap <silent><buffer> <plug>(vimtex-env-toggle-star) + \ :<c-u>call <sid>operator_setup('toggle_star', '')<bar>normal! g@l<cr> +endfunction + +" }}}1 + +function! vimtex#env#change(open, close, new) abort " {{{1 + " + " Set target environment + " + if a:new ==# '' + let [l:beg, l:end] = ['', ''] + elseif a:new ==# '$' + let [l:beg, l:end] = ['$', '$'] + elseif a:new ==# '$$' + let [l:beg, l:end] = ['$$', '$$'] + elseif a:new ==# '\[' + let [l:beg, l:end] = ['\[', '\]'] + elseif a:new ==# '\(' + let [l:beg, l:end] = ['\(', '\)'] + else + let l:beg = '\begin{' . a:new . '}' + let l:end = '\end{' . a:new . '}' + endif + + let l:line = getline(a:open.lnum) + call setline(a:open.lnum, + \ strpart(l:line, 0, a:open.cnum-1) + \ . l:beg + \ . strpart(l:line, a:open.cnum + len(a:open.match) - 1)) + + let l:c1 = a:close.cnum + let l:c2 = a:close.cnum + len(a:close.match) - 1 + if a:open.lnum == a:close.lnum + let n = len(l:beg) - len(a:open.match) + let l:c1 += n + let l:c2 += n + let pos = vimtex#pos#get_cursor() + if pos[2] > a:open.cnum + len(a:open.match) - 1 + let pos[2] += n + call vimtex#pos#set_cursor(pos) + endif + endif + + let l:line = getline(a:close.lnum) + call setline(a:close.lnum, + \ strpart(l:line, 0, l:c1-1) . l:end . strpart(l:line, l:c2)) +endfunction + +function! vimtex#env#change_surrounding_to(type, new) abort " {{{1 + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if empty(l:open) | return | endif + + return vimtex#env#change(l:open, l:close, a:new) +endfunction + +function! vimtex#env#delete(type) abort " {{{1 + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if empty(l:open) | return | endif + + if a:type ==# 'env_tex' + call vimtex#cmd#delete_all(l:close) + call vimtex#cmd#delete_all(l:open) + else + call l:close.remove() + call l:open.remove() + endif + + if getline(l:close.lnum) =~# '^\s*$' + execute l:close.lnum . 'd _' + endif + + if getline(l:open.lnum) =~# '^\s*$' + execute l:open.lnum . 'd _' + endif +endfunction + +function! vimtex#env#toggle_star() abort " {{{1 + let [l:open, l:close] = vimtex#delim#get_surrounding('env_tex') + if empty(l:open) | return | endif + + call vimtex#env#change(l:open, l:close, + \ l:open.starred ? l:open.name : l:open.name . '*') +endfunction + +" }}}1 + +function! vimtex#env#is_inside(env) abort " {{{1 + let l:re_start = '\\begin\s*{' . a:env . '\*\?}' + let l:re_end = '\\end\s*{' . a:env . '\*\?}' + try + return searchpairpos(l:re_start, '', l:re_end, 'bnW', '', 0, 100) + catch /E118/ + let l:stopline = max([line('.') - 500, 1]) + return searchpairpos(l:re_start, '', l:re_end, 'bnW', '', l:stopline) + endtry +endfunction + +" }}}1 +function! vimtex#env#input_complete(lead, cmdline, pos) abort " {{{1 + let l:cands = map(vimtex#complete#complete('env', '', '\begin'), 'v:val.word') + + " Never include document and remove current env (place it first) + call filter(l:cands, 'index([''document'', s:env_name], v:val) < 0') + + " Always include current env and displaymath + let l:cands = [s:env_name] + l:cands + ['\['] + + return filter(l:cands, 'v:val =~# ''^' . a:lead . '''') +endfunction + +" }}}1 + +function! s:change_prompt(type) abort " {{{1 + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if empty(l:open) | return | endif + + if g:vimtex_env_change_autofill + let l:name = get(l:open, 'name', l:open.match) + let s:env_name = l:name + return vimtex#echo#input({ + \ 'prompt' : 'Change surrounding environment: ', + \ 'default' : l:name, + \ 'complete' : 'customlist,vimtex#env#input_complete', + \}) + else + let l:name = get(l:open, 'name', l:open.is_open + \ ? l:open.match . ' ... ' . l:open.corr + \ : l:open.match . ' ... ' . l:open.corr) + let s:env_name = l:name + return vimtex#echo#input({ + \ 'info' : + \ ['Change surrounding environment: ', ['VimtexWarning', l:name]], + \ 'complete' : 'customlist,vimtex#env#input_complete', + \}) + endif +endfunction + +" }}}1 + +function! s:operator_setup(operator, type) abort " {{{1 + let &opfunc = s:snr() . 'operator_function' + + let s:operator_abort = 0 + let s:operator = a:operator + let s:operator_type = a:type + + " Ask for user input if necessary/relevant + if s:operator ==# 'change' + let l:new_env = s:change_prompt(s:operator_type) + if empty(l:new_env) + let s:operator_abort = 1 + return + endif + + let s:operator_name = l:new_env + endif +endfunction + +" }}}1 +function! s:operator_function(_) abort " {{{1 + if get(s:, 'operator_abort', 0) | return | endif + + let l:type = get(s:, 'operator_type', '') + let l:name = get(s:, 'operator_name', '') + + execute 'call vimtex#env#' . { + \ 'change': 'change_surrounding_to(l:type, l:name)', + \ 'delete': 'delete(l:type)', + \ 'toggle_star': 'toggle_star()', + \ }[s:operator] +endfunction + +" }}}1 +function! s:snr() abort " {{{1 + return matchstr(expand('<sfile>'), '<SNR>\d\+_') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold.vim b/autoload/vimtex/fold.vim new file mode 100644 index 00000000..fc897b69 --- /dev/null +++ b/autoload/vimtex/fold.vim @@ -0,0 +1,143 @@ +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#fold#init_buffer() abort " {{{1 + if !g:vimtex_fold_enabled + \ || s:foldmethod_in_modeline() | return | endif + + " Set fold options + setlocal foldmethod=expr + setlocal foldexpr=vimtex#fold#level(v:lnum) + setlocal foldtext=vimtex#fold#text() + + if g:vimtex_fold_manual + " Remap zx to refresh fold levels + nnoremap <silent><nowait><buffer> zx :call vimtex#fold#refresh('zx')<cr> + nnoremap <silent><nowait><buffer> zX :call vimtex#fold#refresh('zX')<cr> + + " Define commands + command! -buffer VimtexRefreshFolds call vimtex#fold#refresh('zx') + + " Ensure that folds are refreshed on startup + augroup vimtex_temporary + autocmd! * <buffer> + autocmd CursorMoved <buffer> + \ call vimtex#fold#refresh('zx') + \ | autocmd! vimtex_temporary CursorMoved <buffer> + augroup END + endif +endfunction + +" }}}1 +function! vimtex#fold#init_state(state) abort " {{{1 + " + " Initialize the enabled fold types + " + let a:state.fold_types_dict = {} + for [l:key, l:val] in items(g:vimtex_fold_types_defaults) + let l:config = extend(deepcopy(l:val), get(g:vimtex_fold_types, l:key, {})) + if get(l:config, 'enabled', 1) + let a:state.fold_types_dict[l:key] = vimtex#fold#{l:key}#new(l:config) + endif + endfor + + " + " Define ordered list and the global fold regex + " + let a:state.fold_types_ordered = [] + let a:state.fold_re = '\v' + \ . '\\%(begin|end)>' + \ . '|^\s*\%' + \ . '|^\s*\]\s*%(\{|$)' + \ . '|^\s*}' + for l:name in [ + \ 'preamble', + \ 'cmd_single', + \ 'cmd_single_opt', + \ 'cmd_multi', + \ 'cmd_addplot', + \ 'sections', + \ 'markers', + \ 'comments', + \ 'envs', + \ 'env_options', + \] + let l:type = get(a:state.fold_types_dict, l:name, {}) + if !empty(l:type) + call add(a:state.fold_types_ordered, l:type) + if exists('l:type.re.fold_re') + let a:state.fold_re .= '|' . l:type.re.fold_re + endif + endif + endfor +endfunction + +" }}}1 + +function! vimtex#fold#refresh(map) abort " {{{1 + setlocal foldmethod=expr + execute 'normal!' a:map + setlocal foldmethod=manual +endfunction + +" }}}1 +function! vimtex#fold#level(lnum) abort " {{{1 + let l:line = getline(a:lnum) + + " Filter out lines that do not start any folds (optimization) + if l:line !~# b:vimtex.fold_re | return '=' | endif + + " Never fold \begin|end{document} + if l:line =~# '^\s*\\\%(begin\|end\){document}' + return 0 + endif + + for l:type in b:vimtex.fold_types_ordered + let l:value = l:type.level(l:line, a:lnum) + if !empty(l:value) | return l:value | endif + endfor + + " Return foldlevel of previous line + return '=' +endfunction + +" }}}1 +function! vimtex#fold#text() abort " {{{1 + let l:line = getline(v:foldstart) + let l:level = v:foldlevel > 1 + \ ? repeat('-', v:foldlevel-2) . g:vimtex_fold_levelmarker + \ : '' + + for l:type in b:vimtex.fold_types_ordered + if l:line =~# l:type.re.start + let l:text = l:type.text(l:line, l:level) + if !empty(l:text) | return l:text | endif + endif + endfor +endfunction + +" }}}1 + + +function! s:foldmethod_in_modeline() abort " {{{1 + let l:cursor_pos = vimtex#pos#get_cursor() + let l:fdm_modeline = 'vim:.*\%(foldmethod\|fdm\)' + + call vimtex#pos#set_cursor(1, 1) + let l:check_top = search(l:fdm_modeline, 'cn', &modelines) + + normal! G$ + let l:check_btm = search(l:fdm_modeline, 'b', line('$') + 1 - &modelines) + + call vimtex#pos#set_cursor(l:cursor_pos) + return l:check_top || l:check_btm +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/cmd_addplot.vim b/autoload/vimtex/fold/cmd_addplot.vim new file mode 100644 index 00000000..9e997681 --- /dev/null +++ b/autoload/vimtex/fold/cmd_addplot.vim @@ -0,0 +1,51 @@ +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#fold#cmd_addplot#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'cmd_addplot', + \ 're' : {}, + \ 'opened' : 0, + \ 'cmds' : [], + \} +function! s:folder.init() abort dict " {{{1 + let l:re = '\v^\s*\\%(' . join(self.cmds, '|') . ')\s*%(\[[^\]]*\])?' + + let self.re.start = l:re . '\s*\w+\s*%(\[[^\]]*\])?\s*\ze\{\s*%($|\%)' + let self.re.end = '^\s*}' + let self.re.fold_re = '\\%(' . join(self.cmds, '|') . ')' + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + let self.opened = 1 + return 'a1' + elseif self.opened && a:line =~# self.re.end + let self.opened = 0 + return 's1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + return matchstr(a:line, self.re.start) . '{...}' + \ . substitute(getline(v:foldend), self.re.end, '', '') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/cmd_multi.vim b/autoload/vimtex/fold/cmd_multi.vim new file mode 100644 index 00000000..cb84d09e --- /dev/null +++ b/autoload/vimtex/fold/cmd_multi.vim @@ -0,0 +1,51 @@ +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#fold#cmd_multi#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'cmd_multi', + \ 're' : {}, + \ 'opened' : 0, + \ 'cmds' : [], + \} +function! s:folder.init() abort dict " {{{1 + let l:re = '\v^\s*\\%(' . join(self.cmds, '|') . ')\*?' + + let self.re.start = l:re . '.*(\{|\[)\s*(\%.*)?$' + let self.re.end = '\v^\s*%(\}\s*\{)*\}\s*%(\%|$)' + let self.re.text = l:re . '\{[^}]*\}' + let self.re.fold_re = '\\%(' . join(self.cmds, '|') . ')' + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + let self.opened += 1 + return 'a1' + elseif self.opened > 0 && a:line =~# self.re.end + let self.opened -= 1 + return 's1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + return a:line +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/cmd_single.vim b/autoload/vimtex/fold/cmd_single.vim new file mode 100644 index 00000000..1403ad9b --- /dev/null +++ b/autoload/vimtex/fold/cmd_single.vim @@ -0,0 +1,52 @@ +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#fold#cmd_single#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'cmd_single', + \ 're' : {}, + \ 'opened' : 0, + \ 'cmds' : [], + \} +function! s:folder.init() abort dict " {{{1 + let l:re = '\v^\s*\\%(' . join(self.cmds, '|') . ')\*?\s*%(\[.*\])?' + + let self.re.start = l:re . '\s*\{\s*%($|\%)' + let self.re.end = '^\s*}' + let self.re.text = l:re + let self.re.fold_re = '\\%(' . join(self.cmds, '|') . ')' + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + let self.opened = 1 + return 'a1' + elseif self.opened && a:line =~# self.re.end + let self.opened = 0 + return 's1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + return matchstr(a:line, self.re.text) . '{...}' + \ . substitute(getline(v:foldend), self.re.end, '', '') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/cmd_single_opt.vim b/autoload/vimtex/fold/cmd_single_opt.vim new file mode 100644 index 00000000..8def0234 --- /dev/null +++ b/autoload/vimtex/fold/cmd_single_opt.vim @@ -0,0 +1,53 @@ +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#fold#cmd_single_opt#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'cmd_single_opt', + \ 're' : {}, + \ 'opened' : 0, + \ 'cmds' : [], + \} +function! s:folder.init() abort dict " {{{1 + let l:re = '\v^\s*\\%(' . join(self.cmds, '|') . ')\*?' + + let self.re.start = l:re . '\s*\[\s*%($|\%)' + let self.re.end = '^\s*\]{' + let self.re.text = l:re + let self.re.fold_re = '\\%(' . join(self.cmds, '|') . ')' + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + let self.opened = 1 + return 'a1' + elseif self.opened && a:line =~# self.re.end + let self.opened = 0 + return 's1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + let l:col = strlen(matchstr(a:line, '^\s*')) + 1 + return matchstr(a:line, self.re.text) . '[...]{' + \ . vimtex#cmd#get_at(v:foldstart, l:col).args[0].text . '}' +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/comments.vim b/autoload/vimtex/fold/comments.vim new file mode 100644 index 00000000..4a313064 --- /dev/null +++ b/autoload/vimtex/fold/comments.vim @@ -0,0 +1,46 @@ +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#fold#comments#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config) +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'comments', + \ 're' : {'start' : '^\s*%'}, + \ 'opened' : 0, + \} +function! s:folder.level(line, lnum) abort dict " {{{1 + if exists('b:vimtex.fold_types_dict.markers.opened') + \ && b:vimtex.fold_types_dict.markers.opened | return | endif + + if a:line =~# self.re.start + let l:next = getline(a:lnum-1) !~# self.re.start + let l:prev = getline(a:lnum+1) !~# self.re.start + if l:next && !l:prev + let self.opened = 1 + return 'a1' + elseif l:prev && !l:next + let self.opened = 0 + return 's1' + endif + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + let l:lines = map(getline(v:foldstart, v:foldend), 'matchstr(v:val, ''%\s*\zs.*\ze\s*'')') + return matchstr(a:line, '^.*\s*%') . join(l:lines, ' ') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/env_options.vim b/autoload/vimtex/fold/env_options.vim new file mode 100644 index 00000000..eab339bf --- /dev/null +++ b/autoload/vimtex/fold/env_options.vim @@ -0,0 +1,53 @@ +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#fold#env_options#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config) +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'envs with options', + \ 're' : { + \ 'start' : g:vimtex#re#not_comment . '\\begin\s*\{.{-}\}\[\s*($|\%)', + \ 'end' : '\s*\]\s*$', + \ }, + \ 'opened' : 0, + \} +function! s:folder.level(line, lnum) abort dict " {{{1 + return self.opened + \ ? self.fold_closed(a:line, a:lnum) + \ : self.fold_opened(a:line, a:lnum) +endfunction + +" }}}1 +function! s:folder.fold_opened(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + let self.opened = 1 + return 'a1' + endif +endfunction + +" }}}1 +function! s:folder.fold_closed(line, lnum) abort dict " {{{1 + if a:line =~# self.re.end + let self.opened = 0 + return 's1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + return a:line . '...] ' +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/envs.vim b/autoload/vimtex/fold/envs.vim new file mode 100644 index 00000000..1eb70f3c --- /dev/null +++ b/autoload/vimtex/fold/envs.vim @@ -0,0 +1,188 @@ +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#fold#envs#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'environments', + \ 're' : { + \ 'start' : g:vimtex#re#not_comment . '\\begin\s*\{.{-}\}', + \ 'end' : g:vimtex#re#not_comment . '\\end\s*\{.{-}\}', + \ 'name' : g:vimtex#re#not_comment . '\\%(begin|end)\s*\{\zs.{-}\ze\}' + \ }, + \ 'whitelist' : [], + \ 'blacklist' : [], + \} +function! s:folder.init() abort dict " {{{1 + " Define the validator as simple as possible + if empty(self.whitelist) && empty(self.blacklist) + function! self.validate(env) abort dict + return 1 + endfunction + elseif empty(self.whitelist) + function! self.validate(env) abort dict + return index(self.blacklist, a:env) < 0 + endfunction + elseif empty(self.blacklist) + function! self.validate(env) abort dict + return index(self.whitelist, a:env) >= 0 + endfunction + else + function! self.validate(env) abort dict + return index(self.whitelist, a:env) >= 0 && index(self.blacklist, a:env) < 0 + endfunction + endif + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + let l:env = matchstr(a:line, self.re.name) + + if !empty(l:env) && self.validate(l:env) + if a:line =~# self.re.start + if a:line !~# '\\end' + return 'a1' + endif + elseif a:line =~# self.re.end + if a:line !~# '\\begin' + return 's1' + endif + endif + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + let env = matchstr(a:line, self.re.name) + if !self.validate(env) | return | endif + + " Set caption/label based on type of environment + if env ==# 'frame' + let label = '' + let caption = self.parse_caption_frame(a:line) + elseif env ==# 'table' + let label = self.parse_label() + let caption = self.parse_caption_table(a:line) + else + let label = self.parse_label() + let caption = self.parse_caption(a:line) + endif + + let width_ind = len(matchstr(a:line, '^\s*')) + let width = winwidth(0) - (&number ? &numberwidth : 0) - 4 - width_ind + + let width_env = 19 + let width_lab = len(label) + 2 > width - width_env + \ ? width - width_env + \ : len(label) + 2 + let width_cap = width - width_env - width_lab + + if !empty(label) + let label = printf('(%.*S)', width_lab, label) + endif + + if !empty(caption) + if strchars(caption) > width_cap + let caption = strpart(caption, 0, width_cap - 4) . '...' + endif + else + let width_env += width_cap + let width_cap = 0 + endif + + if strlen(env) > width_env - 8 + let env = strpart(env, 0, width_env - 11) . '...' + endif + let env = '\begin{' . env . '}' + + let title = printf('%*S%-*S %-*S %*S', + \ width_ind, '', + \ width_env, env, + \ width_cap, caption, + \ width_lab, label) + + return substitute(title, '\s\+$', '', '') +endfunction + +" }}}1 +function! s:folder.parse_label() abort dict " {{{1 + let i = v:foldend + while i >= v:foldstart + if getline(i) =~# '^\s*\\label' + return matchstr(getline(i), '^\s*\\label\%(\[.*\]\)\?{\zs.*\ze}') + end + let i -= 1 + endwhile + return '' +endfunction + +" }}}1 +function! s:folder.parse_caption(line) abort dict " {{{1 + let i = v:foldend + while i >= v:foldstart + if getline(i) =~# '^\s*\\caption' + return matchstr(getline(i), + \ '^\s*\\caption\(\[.*\]\)\?{\zs.\{-1,}\ze\(}\s*\)\?$') + end + let i -= 1 + endwhile + + " If no caption found, check for a caption comment + return matchstr(a:line,'\\begin\*\?{.*}\s*%\s*\zs.*') +endfunction + +" }}}1 +function! s:folder.parse_caption_table(line) abort dict " {{{1 + let i = v:foldstart + while i <= v:foldend + if getline(i) =~# '^\s*\\caption' + return matchstr(getline(i), + \ '^\s*\\caption\s*\(\[.*\]\)\?\s*{\zs.\{-1,}\ze\(}\s*\)\?$') + end + let i += 1 + endwhile + + " If no caption found, check for a caption comment + return matchstr(a:line,'\\begin\*\?{.*}\s*%\s*\zs.*') +endfunction + +" }}}1 +function! s:folder.parse_caption_frame(line) abort dict " {{{1 + " Test simple variants first + let caption1 = matchstr(a:line,'\\begin\*\?{.*}\(\[[^]]*\]\)\?{\zs.\+\ze}') + let caption2 = matchstr(a:line,'\\begin\*\?{.*}\(\[[^]]*\]\)\?{\zs.\+') + if !empty(caption1) + return caption1 + elseif !empty(caption2) + return caption2 + endif + + " Search for \frametitle command + let i = v:foldstart + while i <= v:foldend + if getline(i) =~# '^\s*\\frametitle' + return matchstr(getline(i), + \ '^\s*\\frametitle\(\[.*\]\)\?{\zs.\{-1,}\ze\(}\s*\)\?$') + end + let i += 1 + endwhile + + " If no caption found, check for a caption comment + return matchstr(a:line,'\\begin\*\?{.*}\s*%\s*\zs.*') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/markers.vim b/autoload/vimtex/fold/markers.vim new file mode 100644 index 00000000..00aa1fce --- /dev/null +++ b/autoload/vimtex/fold/markers.vim @@ -0,0 +1,60 @@ +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#fold#markers#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'markers', + \ 'open' : '{{{', + \ 'close' : '}}}', + \ 're' : {}, + \ 'opened' : 0, + \} +function! s:folder.init() abort dict " {{{1 + let self.re.start = '%.*' . self.open + let self.re.end = '%.*' . self.close + let self.re.text = [ + \ [self.re.start . '\d\?\s*\zs.*', '% ' . self.open . ' '], + \ ['%\s*\zs.*\ze' . self.open, '% ' . self.open . ' '], + \ ['^.*\ze\s*%', ''], + \] + + let self.re.fold_re = escape(self.open . '|' . self.close, '{}%+*.') + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + let s:self.opened = 1 + return 'a1' + elseif a:line =~# self.re.end + let s:self.opened = 0 + return 's1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + for [l:re, l:pre] in self.re.text + let l:text = matchstr(a:line, l:re) + if !empty(l:text) | return l:pre . l:text | endif + endfor + + return '% ' . self.open . ' ' . getline(v:foldstart + 1) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/preamble.vim b/autoload/vimtex/fold/preamble.vim new file mode 100644 index 00000000..434064ee --- /dev/null +++ b/autoload/vimtex/fold/preamble.vim @@ -0,0 +1,36 @@ +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#fold#preamble#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config) +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'preamble', + \ 're' : { + \ 'start' : '^\s*\\documentclass', + \ 'fold_re' : '\\documentclass', + \ }, + \} +function! s:folder.level(line, lnum) abort dict " {{{1 + if a:line =~# self.re.start + return '>1' + endif +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + return ' Preamble' +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/fold/sections.vim b/autoload/vimtex/fold/sections.vim new file mode 100644 index 00000000..2929a728 --- /dev/null +++ b/autoload/vimtex/fold/sections.vim @@ -0,0 +1,180 @@ +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#fold#sections#new(config) abort " {{{1 + return extend(deepcopy(s:folder), a:config).init() +endfunction + +" }}}1 + + +let s:folder = { + \ 'name' : 'sections', + \ 'parse_levels' : 0, + \ 're' : {}, + \ 'folds' : [], + \ 'sections' : [], + \ 'parts' : [], + \ 'time' : 0, + \} +function! s:folder.init() abort dict " {{{1 + let self.re.parts = '\v^\s*\\%(' . join(self.parts, '|') . ')' + let self.re.sections = '\v^\s*\\%(' . join(self.sections, '|') . ')' + let self.re.fake_sections = '\v^\s*\% Fake%(' + \ . join(self.sections, '|') . ').*' + let self.re.any_sections = '\v^\s*%(\\|\% Fake)%(' + \ . join(self.sections, '|') . ').*' + + let self.re.start = self.re.parts + \ . '|' . self.re.sections + \ . '|' . self.re.fake_sections + + let self.re.secpat1 = self.re.sections . '\*?\s*\{\zs.*' + let self.re.secpat2 = self.re.sections . '\*?\s*\[\zs.*' + + let self.re.fold_re = '\\%(' . join(self.parts + self.sections, '|') . ')' + + return self +endfunction + +" }}}1 +function! s:folder.level(line, lnum) abort dict " {{{1 + call self.refresh() + + " Fold chapters and sections + for [l:part, l:level] in self.folds + if a:line =~# l:part + return '>' . l:level + endif + endfor +endfunction + +" }}}1 +function! s:folder.text(line, level) abort dict " {{{1 + if a:line =~# '\\frontmatter' + let l:title = 'Frontmatter' + elseif a:line =~# '\\mainmatter' + let l:title = 'Mainmatter' + elseif a:line =~# '\\backmatter' + let l:title = 'Backmatter' + elseif a:line =~# '\\appendix' + let l:title = 'Appendix' + elseif a:line =~# self.re.secpat1 + let l:title = self.parse_title(matchstr(a:line, self.re.secpat1), 0) + elseif a:line =~# self.re.secpat2 + let l:title = self.parse_title(matchstr(a:line, self.re.secpat2), 1) + elseif a:line =~# self.re.fake_sections + let l:title = matchstr(a:line, self.re.fake_sections) + endif + + let l:level = self.parse_level(v:foldstart, a:level) + + return printf('%-5s %-s', l:level, + \ substitute(strpart(l:title, 0, winwidth(0) - 7), '\s\+$', '', '')) +endfunction + +" }}}1 +function! s:folder.parse_level(lnum, level) abort dict " {{{1 + if !self.parse_levels | return a:level | endif + + if !has_key(self, 'toc') + let self.toc = vimtex#toc#new({ + \ 'name' : 'Fold text ToC', + \ 'layers' : ['content'], + \ 'refresh_always' : 0, + \}) + let self.toc_updated = 0 + let self.file_updated = {} + endif + + let l:file = expand('%') + let l:ftime = getftime(l:file) + + if l:ftime > get(self.file_updated, l:file) + \ || localtime() > self.toc_updated + 300 + call self.toc.get_entries(1) + let self.toc_entries = filter( + \ self.toc.get_visible_entries(), + \ '!empty(v:val.number)') + let self.file_updated[l:file] = l:ftime + let self.toc_updated = localtime() + endif + + let l:entries = filter(deepcopy(self.toc_entries), 'v:val.line == a:lnum') + if len(l:entries) > 1 + call filter(l:entries, "v:val.file ==# expand('%:p')") + endif + + return empty(l:entries) ? '' : self.toc.print_number(l:entries[0].number) +endfunction + +" }}}1 +function! s:folder.parse_title(string, type) abort dict " {{{1 + let l:idx = -1 + let l:length = strlen(a:string) + let l:level = 1 + while l:level >= 1 + let l:idx += 1 + if l:idx > l:length + break + elseif a:string[l:idx] ==# ['}',']'][a:type] + let l:level -= 1 + elseif a:string[l:idx] ==# ['{','['][a:type] + let l:level += 1 + endif + endwhile + let l:parsed = strpart(a:string, 0, l:idx) + return empty(l:parsed) + \ ? '<untitled>' : l:parsed +endfunction + +" }}}1 +function! s:folder.refresh() abort dict " {{{1 + " + " Parse current buffer to find which sections to fold and their levels. The + " patterns are predefined to optimize the folding. + " + " We ignore top level parts such as \frontmatter, \appendix, \part, and + " similar, unless there are at least two such commands in a document. + " + + " Only refresh if file has been changed + let l:time = getftime(expand('%')) + if l:time == self.time | return | endif + let self.time = l:time + + " Initialize + let self.folds = [] + let level = 0 + let buffer = getline(1,'$') + + " Parse part commands (frontmatter, appendix, etc) + " Note: We want a minimum of two top level parts + let lines = filter(copy(buffer), 'v:val =~ ''' . self.re.parts . '''') + if len(lines) >= 2 + let level += 1 + call insert(self.folds, [self.re.parts, level]) + endif + + " Parse section commands (part, chapter, [sub...]section) + let lines = filter(copy(buffer), 'v:val =~ ''' . self.re.any_sections . '''') + for part in self.sections + let partpattern = '^\s*\%(\\\|% Fake\)' . part . ':\?\>' + for line in lines + if line =~# partpattern + let level += 1 + call insert(self.folds, [partpattern, level]) + break + endif + endfor + endfor +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/format.vim b/autoload/vimtex/format.vim new file mode 100644 index 00000000..b68aa336 --- /dev/null +++ b/autoload/vimtex/format.vim @@ -0,0 +1,217 @@ +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#format#init_buffer() abort " {{{1 + if !g:vimtex_format_enabled | return | endif + + setlocal formatexpr=vimtex#format#formatexpr() +endfunction + +" }}}1 + +function! vimtex#format#formatexpr() abort " {{{1 + if mode() =~# '[iR]' | return -1 | endif + + " Temporary disable folds and save view + let l:save_view = winsaveview() + let l:foldenable = &l:foldenable + setlocal nofoldenable + + let l:top = v:lnum + let l:bottom = v:lnum + v:count - 1 + let l:lines_old = getline(l:top, l:bottom) + let l:tries = 5 + let s:textwidth = &l:textwidth == 0 ? 79 : &l:textwidth + + " This is a hack to make undo restore the correct position + if mode() !=# 'i' + normal! ix + normal! x + endif + + " Main formatting algorithm + while l:tries > 0 + " Format the range of lines + let l:bottom = s:format(l:top, l:bottom) + + " Ensure proper indentation + if l:top < l:bottom + silent! execute printf('normal! %sG=%sG', l:top+1, l:bottom) + endif + + " Check if any lines have changed + let l:lines_new = getline(l:top, l:bottom) + let l:index = s:compare_lines(l:lines_new, l:lines_old) + let l:top += l:index + if l:top > l:bottom | break | endif + let l:lines_old = l:lines_new[l:index : -1] + let l:tries -= 1 + endwhile + + " Restore fold and view + let &l:foldenable = l:foldenable + call winrestview(l:save_view) + + " Set cursor at appropriate position + execute 'normal!' l:bottom . 'G^' + + " Don't change the text if the formatting algorithm failed + if l:tries == 0 + silent! undo + call vimtex#log#warning('Formatting of selected text failed!') + endif +endfunction + +" }}}1 + +function! s:format(top, bottom) abort " {{{1 + let l:bottom = a:bottom + let l:mark = a:bottom + for l:current in range(a:bottom, a:top, -1) + let l:line = getline(l:current) + + if vimtex#util#in_mathzone(l:current, 1) + \ && vimtex#util#in_mathzone(l:current, col([l:current, '$'])) + let l:mark = l:current - 1 + continue + endif + + " Skip all lines with comments + if l:line =~# '\v%(^|[^\\])\%' + if l:current < l:mark + let l:bottom += s:format_build_lines(l:current+1, l:mark) + endif + let l:mark = l:current - 1 + continue + endif + + " Handle long lines + if strdisplaywidth(l:line) > s:textwidth + let l:bottom += s:format_build_lines(l:current, l:mark) + let l:mark = l:current-1 + endif + + if l:line =~# s:border_end + if l:current < l:mark + let l:bottom += s:format_build_lines(l:current+1, l:mark) + endif + let l:mark = l:current + endif + + if l:line =~# s:border_beginning + if l:current < l:mark + let l:bottom += s:format_build_lines(l:current, l:mark) + endif + let l:mark = l:current-1 + endif + + if l:line =~# '^\s*$' + let l:bottom += s:format_build_lines(l:current+1, l:mark) + let l:mark = l:current-1 + endif + endfor + + if a:top <= l:mark + let l:bottom += s:format_build_lines(a:top, l:mark) + endif + + return l:bottom +endfunction + +" }}}1 +function! s:format_build_lines(start, end) abort " {{{1 + " + " Get the desired text to format as a list of words, but preserve the ending + " line spaces + " + let l:text = join(map(getline(a:start, a:end), + \ 'substitute(v:val, ''^\s*'', '''', '''')'), ' ') + let l:spaces = matchstr(l:text, '\s*$') + let l:words = split(l:text, ' ') + if empty(l:words) | return 0 | endif + + " + " Add the words in properly indented and formatted lines + " + let l:lnum = a:start-1 + let l:current = s:get_indents(indent(a:start)) + for l:word in l:words + if strdisplaywidth(l:word) + strdisplaywidth(l:current) > s:textwidth + call append(l:lnum, substitute(l:current, '\s$', '', '')) + let l:lnum += 1 + let l:current = s:get_indents(VimtexIndent(a:start)) + endif + let l:current .= l:word . ' ' + endfor + if l:current !~# '^\s*$' + call append(l:lnum, substitute(l:current, '\s$', '', '')) + let l:lnum += 1 + endif + + " + " Append the ending line spaces + " + if !empty(l:spaces) + call setline(l:lnum, getline(l:lnum) . l:spaces) + endif + + " + " Remove old text + " + silent! execute printf('%s;+%s delete', l:lnum+1, a:end-a:start) + + " + " Return the difference between number of lines of old and new text + " + return l:lnum - a:end +endfunction + +" }}}1 + +function! s:compare_lines(new, old) abort " {{{1 + let l:min_length = min([len(a:new), len(a:old)]) + for l:i in range(l:min_length) + if a:new[l:i] !=# a:old[l:i] + return l:i + endif + endfor + return l:min_length +endfunction + +" }}}1 +function! s:get_indents(number) abort " {{{1 + return !&l:expandtab && &l:shiftwidth == &l:tabstop + \ ? repeat("\t", a:number/&l:tabstop) + \ : repeat(' ', a:number) +endfunction + +" }}}1 + + +" {{{1 Initialize module + +let s:border_beginning = '\v^\s*%(' . join([ + \ '\\item', + \ '\\begin', + \ '\\end', + \ '%(\\\[|\$\$)\s*$', + \], '|') . ')' + +let s:border_end = '\v\\%(' . join([ + \ '\\\*?', + \ 'clear%(double)?page', + \ 'linebreak', + \ 'new%(line|page)', + \ 'pagebreak', + \ '%(begin|end)\{[^}]*\}', + \ ], '|') . ')\s*$' + \ . '|^\s*%(\\\]|\$\$)\s*$' + +" }}}1 + +endif diff --git a/autoload/vimtex/fzf.vim b/autoload/vimtex/fzf.vim new file mode 100644 index 00000000..c7f62c99 --- /dev/null +++ b/autoload/vimtex/fzf.vim @@ -0,0 +1,114 @@ +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#fzf#run(...) abort " {{{1 + " Arguments: Two optional arguments + " + " First argument: ToC filter (default: 'ctli') + " This may be used to select certain entry types according to the different + " "layers" of vimtex-toc: + " c: content: This is the main part and the "real" ToC + " t: todo: This shows TODOs from comments and `\todo{...}` commands + " l: label: This shows `\label{...}` commands + " i: include: This shows included files + " + " Second argument: Custom options for fzf + " It should be an object containing the parameters passed to fzf#run(). + + " Note: The '--with-nth 3..' option hides the first two words from the fzf + " window. These words are the file name and line number and are used by + " the sink. + let l:opts = extend({ + \ 'source': <sid>parse_toc(a:0 == 0 ? 'ctli' : a:1), + \ 'sink': function('vimtex#fzf#open_selection'), + \ 'options': '--ansi --with-nth 3..', + \}, a:0 > 1 ? a:2 : {}) + + call fzf#run(l:opts) +endfunction + +" }}}1 +function! vimtex#fzf#open_selection(sel) abort " {{{1 + let line = split(a:sel)[0] + let file = split(a:sel)[1] + let curr_file = expand('%:p') + + if curr_file == file + execute 'normal! ' . line . 'gg' + else + execute printf('edit +%s %s', line, file) + endif +endfunction + +" }}}1 + + +function! s:parse_toc(filter) abort " {{{1 + " Parsing is mostly adapted from the Denite source + " (see rplugin/python3/denite/source/vimtex.py) + python3 << EOF +import vim +import json + +def format_number(n): + if not n or not type(n) is dict or not 'chapter' in n: + return '' + + num = [str(n[k]) for k in [ + 'chapter', + 'section', + 'subsection', + 'subsubsection', + 'subsubsubsection'] if n[k] != '0'] + + if n['appendix'] != '0': + num[0] = chr(int(num[0]) + 64) + + return '.'.join(num) + +def colorize(e): + try: + from colorama import Fore, Style + color = {'content' : Fore.WHITE, + 'include' : Fore.BLUE, + 'label' : Fore.GREEN, + 'todo' : Fore.RED}[e['type']] + return f"{color}{e['title']:65}{Style.RESET_ALL}" + except ModuleNotFoundError: + import os + if os.name == 'nt': + # Colour support on Windows requires Colorama + return f"{e['title']:65}" + else: + color = {'content' : "\u001b[37m", + 'include' : "\u001b[34m", + 'label' : "\u001b[32m", + 'todo' : "\u001b[31m"}[e['type']] + return f"{color}{e['title']:65}\u001b[0m" + +def create_candidate(e, depth): + number = format_number(dict(e['number'])) + return f"{e.get('line', 0)} {e['file']} {colorize(e)} {number}" + +entries = vim.eval('vimtex#parser#toc()') +depth = max([int(e['level']) for e in entries]) +filter = vim.eval("a:filter") +candidates = [create_candidate(e, depth) + for e in entries if e['type'][0] in filter] + +# json.dumps will convert single quotes to double quotes +# so that vim understands the ansi escape sequences +vim.command(f"let candidates = {json.dumps(candidates)}") +EOF + + return candidates +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/imaps.vim b/autoload/vimtex/imaps.vim new file mode 100644 index 00000000..6b682f0c --- /dev/null +++ b/autoload/vimtex/imaps.vim @@ -0,0 +1,192 @@ +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#imaps#init_buffer() abort " {{{1 + if !g:vimtex_imaps_enabled | return | endif + + " + " Create imaps + " + let l:maps = g:vimtex_imaps_list + for l:disable in g:vimtex_imaps_disabled + let l:maps = filter(l:maps, 'v:val.lhs !=# ''' . l:disable . '''') + endfor + for l:map in l:maps + get(s:, 'custom_maps', []) + call s:create_map(l:map) + endfor + + " + " Add mappings and commands + " + command! -buffer VimtexImapsList call vimtex#imaps#list() + nnoremap <buffer> <plug>(vimtex-imaps-list) :call vimtex#imaps#list()<cr> +endfunction + +" }}}1 + +function! vimtex#imaps#add_map(map) abort " {{{1 + let s:custom_maps = get(s:, 'custom_maps', []) + [a:map] + + if exists('s:created_maps') + call s:create_map(a:map) + endif +endfunction + +" }}}1 +function! vimtex#imaps#list() abort " {{{1 + silent new vimtex\ imaps + + for l:map in s:created_maps + call append('$', printf('%5S -> %-30S %S', + \ get(l:map, 'leader', get(g:, 'vimtex_imaps_leader', '`')) . l:map.lhs, + \ l:map.rhs, + \ get(l:map, 'wrapper', 'vimtex#imaps#wrap_math'))) + endfor + 0delete _ + + nnoremap <silent><nowait><buffer> q :bwipeout<cr> + nnoremap <silent><nowait><buffer> <esc> :bwipeout<cr> + + setlocal bufhidden=wipe + setlocal buftype=nofile + setlocal concealcursor=nvic + setlocal conceallevel=0 + setlocal cursorline + setlocal nobuflisted + setlocal nolist + setlocal nospell + setlocal noswapfile + setlocal nowrap + setlocal nonumber + setlocal norelativenumber + setlocal nomodifiable + + syntax match VimtexImapsLhs /^.*\ze->/ nextgroup=VimtexImapsArrow + syntax match VimtexImapsArrow /->/ contained nextgroup=VimtexImapsRhs + syntax match VimtexImapsRhs /\s*\S*/ contained nextgroup=VimtexImapsWrapper + syntax match VimtexImapsWrapper /.*/ contained +endfunction + +" }}}1 + +" +" The imap generator +" +function! s:create_map(map) abort " {{{1 + if index(s:created_maps, a:map) >= 0 | return | endif + + let l:leader = get(a:map, 'leader', get(g:, 'vimtex_imaps_leader', '`')) + if l:leader !=# '' && !hasmapto(l:leader, 'i') + silent execute 'inoremap <silent><nowait><buffer>' l:leader . l:leader l:leader + endif + let l:lhs = l:leader . a:map.lhs + + let l:wrapper = get(a:map, 'wrapper', 'vimtex#imaps#wrap_math') + if ! exists('*' . l:wrapper) + echoerr 'vimtex error: imaps wrapper does not exist!' + echoerr ' ' . l:wrapper + return + endif + + " Some wrappers use a context which must be made available to the wrapper + " function at run time. + if has_key(a:map, 'context') + execute 'let l:key = "' . escape(l:lhs, '<') . '"' + let l:key .= a:map.rhs + if !exists('b:vimtex_context') + let b:vimtex_context = {} + endif + let b:vimtex_context[l:key] = a:map.context + endif + + " The rhs may be evaluated before being passed to wrapper, unless expr is + " disabled (which it is by default) + if !get(a:map, 'expr') + let a:map.rhs = string(a:map.rhs) + endif + + silent execute 'inoremap <expr><silent><nowait><buffer>' l:lhs + \ l:wrapper . '("' . escape(l:lhs, '\') . '", ' . a:map.rhs . ')' + + let s:created_maps += [a:map] +endfunction + +" }}}1 + +" +" Wrappers +" +function! vimtex#imaps#wrap_trivial(lhs, rhs) abort " {{{1 + return a:rhs +endfunction + +" }}}1 +function! vimtex#imaps#wrap_math(lhs, rhs) abort " {{{1 + return s:is_math() ? a:rhs : a:lhs +endfunction + +" }}}1 +function! vimtex#imaps#wrap_environment(lhs, rhs) abort " {{{1 + let l:return = a:lhs + let l:cursor = vimtex#pos#val(vimtex#pos#get_cursor()) + let l:value = 0 + + for l:context in b:vimtex_context[a:lhs . a:rhs] + if type(l:context) == type('') + let l:envs = [l:context] + let l:rhs = a:rhs + elseif type(l:context) == type({}) + let l:envs = l:context.envs + let l:rhs = l:context.rhs + endif + + for l:env in l:envs + let l:candidate_value = vimtex#pos#val(vimtex#env#is_inside(l:env)) + if l:candidate_value > l:value + let l:value = l:candidate_value + let l:return = l:rhs + endif + endfor + + unlet l:context + endfor + + return l:return +endfunction + +" }}}1 + +" +" Special rhs styles +" +function! vimtex#imaps#style_math(command) " {{{1 + return s:is_math() + \ ? '\' . a:command . '{' . nr2char(getchar()) . '}' + \ : '' +endfunction + +" }}}1 + +" +" Helpers +" +function! s:is_math() abort " {{{1 + return match(map(synstack(line('.'), max([col('.') - 1, 1])), + \ 'synIDattr(v:val, ''name'')'), '^texMathZone') >= 0 +endfunction + +" }}}1 + + +" {{{1 Initialize module + +let s:created_maps = [] + +" }}}1 + +endif diff --git a/autoload/vimtex/include.vim b/autoload/vimtex/include.vim new file mode 100644 index 00000000..2a58dffe --- /dev/null +++ b/autoload/vimtex/include.vim @@ -0,0 +1,147 @@ +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#include#expr() abort " {{{1 + call s:visited.timeout() + let l:fname = substitute(v:fname, '^\s*\|\s*$', '', 'g') + + " + " Check if v:fname matches exactly + " + if filereadable(l:fname) + return s:visited.check(l:fname) + endif + + " + " Parse \include or \input style lines + " + let l:file = s:input(l:fname, 'tex') + for l:candidate in [l:file, l:file . '.tex'] + if filereadable(l:candidate) + return s:visited.check(l:candidate) + endif + endfor + + " + " Parse \bibliography or \addbibresource + " + let l:candidate = s:input(l:fname, 'bib') + if filereadable(l:candidate) + return s:visited.check(l:candidate) + endif + + " + " Check if v:fname matches in $TEXINPUTS + " + let l:candidate = s:search_candidates_texinputs(l:fname) + if !empty(l:candidate) + return s:visited.check(l:candidate) + endif + + " + " Search for file with kpsewhich + " + if g:vimtex_include_search_enabled + let l:candidate = s:search_candidates_kpsewhich(l:fname) + if !empty(l:candidate) + return s:visited.check(l:candidate) + endif + endif + + return s:visited.check(l:fname) +endfunction + +" }}}1 + +function! s:input(fname, type) abort " {{{1 + let [l:lnum, l:cnum] = searchpos(g:vimtex#re#{a:type}_input, 'bcn', line('.')) + if l:lnum == 0 | return a:fname | endif + + let l:cmd = vimtex#cmd#get_at(l:lnum, l:cnum) + let l:file = join(map( + \ get(l:cmd, 'args', [{}]), + \ "get(v:val, 'text', '')"), + \ '') + let l:file = substitute(l:file, '^\s*"\|"\s*$', '', 'g') + let l:file = substitute(l:file, '\\space', '', 'g') + + if l:file[-3:] !=# a:type + let l:file .= '.' . a:type + endif + + return l:file +endfunction + +" }}}1 +function! s:search_candidates_texinputs(fname) abort " {{{1 + for l:suffix in [''] + split(&l:suffixesadd, ',') + let l:candidates = glob(b:vimtex.root . '/**/' . a:fname . l:suffix, 0, 1) + if !empty(l:candidates) + return l:candidates[0] + endif + endfor + + return '' +endfunction + +" }}}1 +function! s:search_candidates_kpsewhich(fname) abort " {{{1 + " Split input list on commas, and if applicable, ensure that the entry that + " the cursor is on is placed first in the queue + let l:files = split(a:fname, '\s*,\s*') + let l:current = expand('<cword>') + let l:index = index(l:files, l:current) + if l:index >= 0 + call remove(l:files, l:index) + let l:files = [l:current] + l:files + endif + + " Add file extensions to create the final list of candidate files + let l:candidates = [] + for l:file in l:files + if !empty(fnamemodify(l:file, ':e')) + call add(l:candidates, l:file) + else + call extend(l:candidates, map(split(&l:suffixesadd, ','), 'l:file . v:val')) + endif + endfor + + for l:file in l:candidates + let l:candidate = vimtex#kpsewhich#find(l:file) + if !empty(l:candidate) && filereadable(l:candidate) | return l:candidate | endif + endfor + + return '' +endfunction + +" }}}1 + +let s:visited = { + \ 'time' : 0, + \ 'list' : [], + \} +function! s:visited.timeout() abort dict " {{{1 + if localtime() - self.time > 1 + let self.time = localtime() + let self.list = [expand('%:p')] + endif +endfunction + +" }}}1 +function! s:visited.check(fname) abort dict " {{{1 + if index(self.list, fnamemodify(a:fname, ':p')) < 0 + call add(self.list, fnamemodify(a:fname, ':p')) + return a:fname + endif + + return '' +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/info.vim b/autoload/vimtex/info.vim new file mode 100644 index 00000000..1057b578 --- /dev/null +++ b/autoload/vimtex/info.vim @@ -0,0 +1,222 @@ +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#info#init_buffer() abort " {{{1 + command! -buffer -bang VimtexInfo call vimtex#info#open(<q-bang> == '!') + + nnoremap <buffer> <plug>(vimtex-info) :VimtexInfo<cr> + nnoremap <buffer> <plug>(vimtex-info-full) :VimtexInfo!<cr> +endfunction + +" }}}1 +function! vimtex#info#open(global) abort " {{{1 + let s:info.global = a:global + call vimtex#scratch#new(s:info) +endfunction + +" }}}1 + + +let s:info = { + \ 'name' : 'VimtexInfo', + \ 'global' : 0, + \} +function! s:info.print_content() abort dict " {{{1 + for l:line in self.gather_system_info() + call append('$', l:line) + endfor + call append('$', '') + for l:line in self.gather_state_info() + call append('$', l:line) + endfor +endfunction + +" }}}1 +function! s:info.gather_system_info() abort dict " {{{1 + let l:lines = [ + \ 'System info', + \ ' OS: ' . s:get_os_info(), + \ ' Vim version: ' . s:get_vim_info(), + \] + + if has('clientserver') || has('nvim') + call add(l:lines, ' Has clientserver: true') + call add(l:lines, ' Servername: ' + \ . (empty(v:servername) ? 'undefined (vim started without --servername)' : v:servername)) + else + call add(l:lines, ' Has clientserver: false') + endif + + return l:lines +endfunction + +" }}}1 +function! s:info.gather_state_info() abort dict " {{{1 + if self.global + let l:lines = [] + for l:data in vimtex#state#list_all() + let l:lines += s:get_info(l:data) + let l:lines += [''] + endfor + call remove(l:lines, -1) + else + let l:lines = s:get_info(b:vimtex) + endif + + return l:lines +endfunction + +" }}}1 +function! s:info.syntax() abort dict " {{{1 + syntax match VimtexInfoOther /.*/ + syntax match VimtexInfoKey /^.*:/ nextgroup=VimtexInfoValue + syntax match VimtexInfoValue /.*/ contained + syntax match VimtexInfoTitle /vimtex project:/ nextgroup=VimtexInfoValue + syntax match VimtexInfoTitle /System info/ +endfunction + +" }}}1 + +" +" Functions to parse the vimtex state data +" +function! s:get_info(item, ...) abort " {{{1 + if empty(a:item) | return [] | endif + let l:indent = a:0 > 0 ? a:1 : 0 + + if type(a:item) == type({}) + return s:parse_dict(a:item, l:indent) + endif + + if type(a:item) == type([]) + let l:entries = [] + for [l:title, l:Value] in a:item + if type(l:Value) == type({}) + call extend(l:entries, s:parse_dict(l:Value, l:indent, l:title)) + elseif type(l:Value) == type([]) + call extend(l:entries, s:parse_list(l:Value, l:indent, l:title)) + else + call add(l:entries, + \ repeat(' ', l:indent) . printf('%s: %s', l:title, l:Value)) + endif + unlet l:Value + endfor + return l:entries + endif +endfunction + +" }}}1 +function! s:parse_dict(dict, indent, ...) abort " {{{1 + if empty(a:dict) | return [] | endif + let l:dict = a:dict + let l:indent = a:indent + let l:entries = [] + + if a:0 > 0 + let l:title = a:1 + let l:name = '' + if has_key(a:dict, 'name') + let l:dict = deepcopy(a:dict) + let l:name = remove(l:dict, 'name') + endif + call add(l:entries, + \ repeat(' ', l:indent) . printf('%s: %s', l:title, l:name)) + let l:indent += 1 + endif + + let l:items = has_key(l:dict, 'pprint_items') + \ ? l:dict.pprint_items() : items(l:dict) + + return extend(l:entries, s:get_info(l:items, l:indent)) +endfunction + +" }}}1 +function! s:parse_list(list, indent, title) abort " {{{1 + if empty(a:list) | return [] | endif + + let l:entries = [] + let l:indent = repeat(' ', a:indent) + if type(a:list[0]) == type([]) + let l:name = '' + let l:index = 0 + + " l:entry[0] == title + " l:entry[1] == value + for l:entry in a:list + if l:entry[0] ==# 'name' + let l:name = l:entry[1] + break + endif + let l:index += 1 + endfor + + if empty(l:name) + let l:list = a:list + else + let l:list = deepcopy(a:list) + call remove(l:list, l:index) + endif + + call add(l:entries, l:indent . printf('%s: %s', a:title, l:name)) + call extend(l:entries, s:get_info(l:list, a:indent+1)) + else + call add(l:entries, l:indent . printf('%s:', a:title)) + for l:value in a:list + call add(l:entries, l:indent . printf(' %s', l:value)) + endfor + endif + + return l:entries +endfunction + +" }}}1 + +" +" Other utility functions +" +function! s:get_os_info() abort " {{{1 + let l:os = vimtex#util#get_os() + + if l:os ==# 'linux' + let l:result = executable('lsb_release') + \ ? system('lsb_release -d')[12:-2] + \ : system('uname -sr')[:-2] + return substitute(l:result, '^\s*', '', '') + elseif l:os ==# 'mac' + let l:name = system('sw_vers -productName')[:-2] + let l:version = system('sw_vers -productVersion')[:-2] + let l:build = system('sw_vers -buildVersion')[:-2] + return l:name . ' ' . l:version . ' (' . l:build . ')' + else + if !exists('s:win_info') + let s:win_info = vimtex#process#capture('systeminfo') + endif + + let l:name = matchstr(s:win_info[1], ':\s*\zs.*') + let l:version = matchstr(s:win_info[2], ':\s*\zs.*') + return l:name . ' (' . l:version . ')' + endif +endfunction + +" }}}1 +function! s:get_vim_info() abort " {{{1 + let l:info = vimtex#util#command('version') + + if has('nvim') + return l:info[0] + else + let l:version = 'VIM ' . strpart(l:info[0], 18, 3) . ' (' + let l:index = 2 - (l:info[1] =~# ':\s*\d') + let l:version .= matchstr(l:info[l:index], ':\s*\zs.*') . ')' + return l:version + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/kpsewhich.vim b/autoload/vimtex/kpsewhich.vim new file mode 100644 index 00000000..65533e28 --- /dev/null +++ b/autoload/vimtex/kpsewhich.vim @@ -0,0 +1,53 @@ +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#kpsewhich#find(file) abort " {{{1 + return s:find_cached(a:file) +endfunction + +" }}}1 +function! vimtex#kpsewhich#run(args) abort " {{{1 + " kpsewhich should be run at the project root directory + if exists('b:vimtex.root') + call vimtex#paths#pushd(b:vimtex.root) + endif + let l:output = vimtex#process#capture('kpsewhich ' . a:args) + if exists('b:vimtex.root') + call vimtex#paths#popd() + endif + + " Remove warning lines from output + call filter(l:output, 'stridx(v:val, "kpsewhich: warning: ") == -1') + + return l:output +endfunction + +" }}}1 + +function! s:find(file) abort " {{{1 + let l:output = vimtex#kpsewhich#run(fnameescape(a:file)) + if empty(l:output) | return '' | endif + + let l:filename = l:output[0] + + " Ensure absolute path + if !vimtex#paths#is_abs(l:filename) && exists('b:vimtex.root') + let l:filename = simplify(b:vimtex.root . '/' . l:filename) + endif + + return l:filename +endfunction + +" Use caching if possible (requires 'lambda' feature) +let s:find_cached = has('lambda') + \ ? vimtex#cache#wrap(function('s:find'), 'kpsewhich') + \ : function('s:find') + +" }}}1 + +endif diff --git a/autoload/vimtex/log.vim b/autoload/vimtex/log.vim new file mode 100644 index 00000000..f7569a65 --- /dev/null +++ b/autoload/vimtex/log.vim @@ -0,0 +1,137 @@ +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#log#init_buffer() abort " {{{1 + command! -buffer -bang VimtexLog call vimtex#log#open() + + nnoremap <buffer> <plug>(vimtex-log) :VimtexLog<cr> +endfunction + +" }}}1 + +function! vimtex#log#info(...) abort " {{{1 + call s:logger.add(a:000, 'info') +endfunction + +" }}}1 +function! vimtex#log#warning(...) abort " {{{1 + call s:logger.add(a:000, 'warning') +endfunction + +" }}}1 +function! vimtex#log#error(...) abort " {{{1 + call s:logger.add(a:000, 'error') +endfunction + +" }}}1 + +function! vimtex#log#get() abort " {{{1 + return s:logger.entries +endfunction + +" }}}1 + +function! vimtex#log#open() abort " {{{1 + call vimtex#scratch#new(s:logger) +endfunction + +" }}}1 +function! vimtex#log#toggle_verbose() abort " {{{1 + if s:logger.verbose + let s:logger.verbose = 0 + call vimtex#log#info('Logging is now quiet') + else + call vimtex#log#info('Logging is now verbose') + let s:logger.verbose = 1 + endif +endfunction + +" }}}1 + + +let s:logger = { + \ 'name' : 'VimtexMessageLog', + \ 'entries' : [], + \ 'type_to_highlight' : { + \ 'info' : 'VimtexInfo', + \ 'warning' : 'VimtexWarning', + \ 'error' : 'VimtexError', + \ }, + \ 'verbose' : get(g:, 'vimtex_log_verbose', 1), + \} +function! s:logger.add(msg_arg, type) abort dict " {{{1 + let l:msg_list = [] + for l:msg in a:msg_arg + if type(l:msg) == type('') + call add(l:msg_list, l:msg) + elseif type(l:msg) == type([]) + call extend(l:msg_list, filter(l:msg, "type(v:val) == type('')")) + endif + endfor + + let l:entry = {} + let l:entry.type = a:type + let l:entry.time = strftime('%T') + let l:entry.callstack = vimtex#debug#stacktrace()[1:] + let l:entry.msg = l:msg_list + call add(self.entries, l:entry) + + if !self.verbose | return | endif + + " Ignore message + for l:re in get(g:, 'vimtex_log_ignore', []) + if join(l:msg_list) =~# l:re | return | endif + endfor + + call vimtex#echo#formatted([ + \ [self.type_to_highlight[a:type], 'vimtex:'], + \ ' ' . l:msg_list[0] + \]) + for l:line in l:msg_list[1:] + call vimtex#echo#echo(' ' . l:line) + endfor +endfunction + +" }}}1 +function! s:logger.print_content() abort dict " {{{1 + for l:entry in self.entries + call append('$', printf('%s: %s', l:entry.time, l:entry.type)) + for l:stack in l:entry.callstack + if l:stack.lnum > 0 + call append('$', printf(' #%d %s:%d', l:stack.nr, l:stack.filename, l:stack.lnum)) + else + call append('$', printf(' #%d %s', l:stack.nr, l:stack.filename)) + endif + call append('$', printf(' In %s', l:stack.function)) + if !empty(l:stack.text) + call append('$', printf(' %s', l:stack.text)) + endif + endfor + for l:msg in l:entry.msg + call append('$', printf(' %s', l:msg)) + endfor + call append('$', '') + endfor +endfunction + +" }}}1 +function! s:logger.syntax() abort dict " {{{1 + syntax match VimtexInfoOther /.*/ + + syntax include @VIM syntax/vim.vim + syntax match VimtexInfoVimCode /^ .*/ transparent contains=@VIM + + syntax match VimtexInfoKey /^\S*:/ nextgroup=VimtexInfoValue + syntax match VimtexInfoKey /^ #\d\+/ nextgroup=VimtexInfoValue + syntax match VimtexInfoKey /^ In/ nextgroup=VimtexInfoValue + syntax match VimtexInfoValue /.*/ contained +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/matchparen.vim b/autoload/vimtex/matchparen.vim new file mode 100644 index 00000000..80442cd9 --- /dev/null +++ b/autoload/vimtex/matchparen.vim @@ -0,0 +1,111 @@ +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#matchparen#init_buffer() abort " {{{1 + if !g:vimtex_matchparen_enabled | return | endif + + call vimtex#matchparen#enable() +endfunction + +" }}}1 + +function! vimtex#matchparen#enable() abort " {{{1 + call s:matchparen.enable() +endfunction + +" }}}1 +function! vimtex#matchparen#disable() abort " {{{1 + call s:matchparen.disable() +endfunction + +" }}}1 +function! vimtex#matchparen#popup_check(...) abort " {{{1 + if pumvisible() + call s:matchparen.highlight() + endif +endfunction + +" }}}1 + +let s:matchparen = {} + +function! s:matchparen.enable() abort dict " {{{1 + " vint: -ProhibitAutocmdWithNoGroup + + execute 'augroup vimtex_matchparen' . bufnr('%') + autocmd! + autocmd CursorMoved <buffer> call s:matchparen.highlight() + autocmd CursorMovedI <buffer> call s:matchparen.highlight() + try + autocmd TextChangedP <buffer> call s:matchparen.highlight() + catch /E216/ + silent! let self.timer = + \ timer_start(50, 'vimtex#matchparen#popup_check', {'repeat' : -1}) + endtry + augroup END + + call self.highlight() + + " vint: +ProhibitAutocmdWithNoGroup +endfunction + +" }}}1 +function! s:matchparen.disable() abort dict " {{{1 + call self.clear() + execute 'autocmd! vimtex_matchparen' . bufnr('%') + silent! call timer_stop(self.timer) +endfunction + +" }}}1 +function! s:matchparen.clear() abort dict " {{{1 + silent! call matchdelete(w:vimtex_match_id1) + silent! call matchdelete(w:vimtex_match_id2) + unlet! w:vimtex_match_id1 + unlet! w:vimtex_match_id2 +endfunction +function! s:matchparen.highlight() abort dict " {{{1 + call self.clear() + + if vimtex#util#in_comment() | return | endif + + " This is a hack to ensure that $ in visual block mode adhers to the rule + " specified in :help v_$ + if mode() ==# "\<c-v>" + let l:pos = vimtex#pos#get_cursor() + if len(l:pos) == 5 && l:pos[-1] == 2147483647 + call feedkeys('$', 'in') + endif + endif + + let l:current = vimtex#delim#get_current('all', 'both') + if empty(l:current) | return | endif + + let l:corresponding = vimtex#delim#get_matching(l:current) + if empty(l:corresponding) | return | endif + if empty(l:corresponding.match) | return | endif + + let [l:open, l:close] = l:current.is_open + \ ? [l:current, l:corresponding] + \ : [l:corresponding, l:current] + + if exists('*matchaddpos') + let w:vimtex_match_id1 = matchaddpos('MatchParen', + \ [[l:open.lnum, l:open.cnum, strlen(l:open.match)]]) + let w:vimtex_match_id2 = matchaddpos('MatchParen', + \ [[l:close.lnum, l:close.cnum, strlen(l:close.match)]]) + else + let w:vimtex_match_id1 = matchadd('MatchParen', + \ '\%' . l:open.lnum . 'l\%' . l:open.cnum . 'c' . l:open.re.this) + let w:vimtex_match_id2 = matchadd('MatchParen', + \ '\%' . l:close.lnum . 'l\%' . l:close.cnum . 'c' . l:close.re.this) + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/misc.vim b/autoload/vimtex/misc.vim new file mode 100644 index 00000000..1b39a645 --- /dev/null +++ b/autoload/vimtex/misc.vim @@ -0,0 +1,158 @@ +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#misc#init_buffer() abort " {{{1 + command! -buffer VimtexReload call vimtex#misc#reload() + command! -buffer -bang -range=% VimtexCountWords + \ call vimtex#misc#wordcount_display({ + \ 'range' : [<line1>, <line2>], + \ 'detailed' : <q-bang> == '!', + \ 'count_letters' : 0, + \ }) + command! -buffer -bang -range=% VimtexCountLetters + \ call vimtex#misc#wordcount_display({ + \ 'range' : [<line1>, <line2>], + \ 'detailed' : <q-bang> == '!', + \ 'count_letters' : 1, + \ }) + + nnoremap <buffer> <plug>(vimtex-reload) :VimtexReload<cr> +endfunction + +" }}}1 + +function! vimtex#misc#get_graphicspath(fname) abort " {{{1 + for l:root in b:vimtex.graphicspath + ['.'] + let l:candidate = simplify(b:vimtex.root . '/' . l:root . '/' . a:fname) + for l:suffix in ['', '.jpg', '.png', '.pdf'] + if filereadable(l:candidate . l:suffix) + return l:candidate . l:suffix + endif + endfor + endfor + + return a:fname +endfunction + +" }}}1 +function! vimtex#misc#wordcount(...) abort " {{{1 + let l:opts = a:0 > 0 ? a:1 : {} + + let l:range = get(l:opts, 'range', [1, line('$')]) + if l:range == [1, line('$')] + let l:file = b:vimtex + else + let l:file = vimtex#parser#selection_to_texfile('arg', l:range) + endif + + let cmd = 'cd ' . vimtex#util#shellescape(l:file.root) + let cmd .= has('win32') ? '& ' : '; ' + let cmd .= 'texcount -nosub -sum ' + let cmd .= get(l:opts, 'count_letters') ? '-letter ' : '' + let cmd .= get(l:opts, 'detailed') ? '-inc ' : '-q -1 -merge ' + let cmd .= g:vimtex_texcount_custom_arg . ' ' + let cmd .= vimtex#util#shellescape(l:file.base) + let lines = vimtex#process#capture(cmd) + + if l:file.base !=# b:vimtex.base + call delete(l:file.tex) + endif + + if get(l:opts, 'detailed') + return lines + else + call filter(lines, 'v:val !~# ''ERROR\|^\s*$''') + return join(lines, '') + endif +endfunction + +" }}}1 +function! vimtex#misc#wordcount_display(opts) abort " {{{1 + let output = vimtex#misc#wordcount(a:opts) + + if !get(a:opts, 'detailed') + call vimtex#log#info('Counted ' + \ . (get(a:opts, 'count_letters') ? 'letters: ' : 'words: ') + \ . output) + return + endif + + " Create wordcount window + if bufnr('TeXcount') >= 0 + bwipeout TeXcount + endif + split TeXcount + + " Add lines to buffer + for line in output + call append('$', printf('%s', line)) + endfor + 0delete _ + + " Set mappings + nnoremap <buffer><nowait><silent> q :bwipeout<cr> + + " Set buffer options + setlocal bufhidden=wipe + setlocal buftype=nofile + setlocal cursorline + setlocal nobuflisted + setlocal nolist + setlocal nospell + setlocal noswapfile + setlocal nowrap + setlocal tabstop=8 + setlocal nomodifiable + + " Set highlighting + syntax match TexcountText /^.*:.*/ contains=TexcountValue + syntax match TexcountValue /.*:\zs.*/ + highlight link TexcountText VimtexMsg + highlight link TexcountValue Constant +endfunction + +" }}}1 +" {{{1 function! vimtex#misc#reload() +if get(s:, 'reload_guard', 1) + function! vimtex#misc#reload() abort + let s:reload_guard = 0 + + for l:file in glob(fnamemodify(s:file, ':h') . '/../**/*.vim', 0, 1) + execute 'source' l:file + endfor + + " Temporarily unset b:current_syntax (if active) + let l:reload_syntax = get(b:, 'current_syntax', '') ==# 'tex' + if l:reload_syntax + unlet b:current_syntax + endif + + call vimtex#init() + + " Reload syntax + if l:reload_syntax + runtime! syntax/tex.vim + endif + + " Reload indent file + if exists('b:did_vimtex_indent') + unlet b:did_indent + runtime indent/tex.vim + endif + + call vimtex#log#info('The plugin has been reloaded!') + unlet s:reload_guard + endfunction +endif + +" }}}1 + + +let s:file = expand('<sfile>') + +endif diff --git a/autoload/vimtex/motion.vim b/autoload/vimtex/motion.vim new file mode 100644 index 00000000..86b42055 --- /dev/null +++ b/autoload/vimtex/motion.vim @@ -0,0 +1,207 @@ +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#motion#init_buffer() abort " {{{1 + if !g:vimtex_motion_enabled | return | endif + + " Utility map to avoid conflict with "normal" command + nnoremap <buffer> <sid>(v) v + nnoremap <buffer> <sid>(V) V + + " Matching pairs + nnoremap <silent><buffer> <plug>(vimtex-%) :call vimtex#motion#find_matching_pair()<cr> + xnoremap <silent><buffer> <sid>(vimtex-%) :<c-u>call vimtex#motion#find_matching_pair(1)<cr> + xmap <silent><buffer> <plug>(vimtex-%) <sid>(vimtex-%) + onoremap <silent><buffer> <plug>(vimtex-%) :execute "normal \<sid>(v)\<sid>(vimtex-%)"<cr> + + " Sections + nnoremap <silent><buffer> <plug>(vimtex-]]) :<c-u>call vimtex#motion#section(0,0,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-][) :<c-u>call vimtex#motion#section(1,0,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-[]) :<c-u>call vimtex#motion#section(1,1,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-[[) :<c-u>call vimtex#motion#section(0,1,0)<cr> + xnoremap <silent><buffer> <sid>(vimtex-]]) :<c-u>call vimtex#motion#section(0,0,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-][) :<c-u>call vimtex#motion#section(1,0,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-[]) :<c-u>call vimtex#motion#section(1,1,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-[[) :<c-u>call vimtex#motion#section(0,1,1)<cr> + xmap <silent><buffer> <plug>(vimtex-]]) <sid>(vimtex-]]) + xmap <silent><buffer> <plug>(vimtex-][) <sid>(vimtex-][) + xmap <silent><buffer> <plug>(vimtex-[]) <sid>(vimtex-[]) + xmap <silent><buffer> <plug>(vimtex-[[) <sid>(vimtex-[[) + onoremap <silent><buffer> <plug>(vimtex-]]) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-]])"<cr> + onoremap <silent><buffer> <plug>(vimtex-][) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-][)"<cr> + onoremap <silent><buffer> <plug>(vimtex-[]) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-[])"<cr> + onoremap <silent><buffer> <plug>(vimtex-[[) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-[[)"<cr> + + " Environments + nnoremap <silent><buffer> <plug>(vimtex-]m) :<c-u>call vimtex#motion#environment(1,0,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-]M) :<c-u>call vimtex#motion#environment(0,0,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-[m) :<c-u>call vimtex#motion#environment(1,1,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-[M) :<c-u>call vimtex#motion#environment(0,1,0)<cr> + xnoremap <silent><buffer> <sid>(vimtex-]m) :<c-u>call vimtex#motion#environment(1,0,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-]M) :<c-u>call vimtex#motion#environment(0,0,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-[m) :<c-u>call vimtex#motion#environment(1,1,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-[M) :<c-u>call vimtex#motion#environment(0,1,1)<cr> + xmap <silent><buffer> <plug>(vimtex-]m) <sid>(vimtex-]m) + xmap <silent><buffer> <plug>(vimtex-]M) <sid>(vimtex-]M) + xmap <silent><buffer> <plug>(vimtex-[m) <sid>(vimtex-[m) + xmap <silent><buffer> <plug>(vimtex-[M) <sid>(vimtex-[M) + onoremap <silent><buffer> <plug>(vimtex-]m) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-]m)"<cr> + onoremap <silent><buffer> <plug>(vimtex-]M) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-]M)"<cr> + onoremap <silent><buffer> <plug>(vimtex-[m) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-[m)"<cr> + onoremap <silent><buffer> <plug>(vimtex-[M) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-[M)"<cr> + + " Comments + nnoremap <silent><buffer> <plug>(vimtex-]/) :<c-u>call vimtex#motion#comment(1,0,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-]*) :<c-u>call vimtex#motion#comment(0,0,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-[/) :<c-u>call vimtex#motion#comment(1,1,0)<cr> + nnoremap <silent><buffer> <plug>(vimtex-[*) :<c-u>call vimtex#motion#comment(0,1,0)<cr> + xnoremap <silent><buffer> <sid>(vimtex-]/) :<c-u>call vimtex#motion#comment(1,0,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-]*) :<c-u>call vimtex#motion#comment(0,0,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-[/) :<c-u>call vimtex#motion#comment(1,1,1)<cr> + xnoremap <silent><buffer> <sid>(vimtex-[*) :<c-u>call vimtex#motion#comment(0,1,1)<cr> + xmap <silent><buffer> <plug>(vimtex-]/) <sid>(vimtex-]/) + xmap <silent><buffer> <plug>(vimtex-]*) <sid>(vimtex-]*) + xmap <silent><buffer> <plug>(vimtex-[/) <sid>(vimtex-[/) + xmap <silent><buffer> <plug>(vimtex-[*) <sid>(vimtex-[*) + onoremap <silent><buffer> <plug>(vimtex-]/) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-]/)"<cr> + onoremap <silent><buffer> <plug>(vimtex-]*) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-]*)"<cr> + onoremap <silent><buffer> <plug>(vimtex-[/) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-[/)"<cr> + onoremap <silent><buffer> <plug>(vimtex-[*) + \ :execute "normal \<sid>(V)" . v:count1 . "\<sid>(vimtex-[*)"<cr> +endfunction + +" }}}1 + +function! vimtex#motion#find_matching_pair(...) abort " {{{1 + if a:0 > 0 + normal! gv + endif + + let delim = vimtex#delim#get_current('all', 'both') + if empty(delim) + let delim = vimtex#delim#get_next('all', 'both') + if empty(delim) | return | endif + endif + + let delim = vimtex#delim#get_matching(delim) + if empty(delim) | return | endif + if empty(delim.match) | return | endif + + normal! m` + call vimtex#pos#set_cursor(delim.lnum, + \ (delim.is_open + \ ? delim.cnum + \ : delim.cnum + strlen(delim.match) - 1)) +endfunction + +" }}}1 +function! vimtex#motion#section(type, backwards, visual) abort " {{{1 + let l:count = v:count1 + if a:visual + normal! gv + endif + + " Check trivial cases + let l:top = search(s:re_sec, 'nbW') == 0 + let l:bottom = search(a:type == 1 ? s:re_sec_t2 : s:re_sec, 'nW') == 0 + if a:backwards && l:top + return vimtex#pos#set_cursor([1, 1]) + elseif !a:backwards && l:bottom + return vimtex#pos#set_cursor([line('$'), 1]) + endif + + " Define search pattern and search flag + let l:re = a:type == 0 ? s:re_sec : s:re_sec_t1 + let l:flags = 'W' + if a:backwards + let l:flags .= 'b' + endif + + for l:_ in range(l:count) + let l:save_pos = vimtex#pos#get_cursor() + + if a:type == 1 + call search('\S', 'W') + endif + + let l:bottom = search(s:re_sec_t2, 'nW') == 0 + if a:type == 1 && !a:backwards && l:bottom + return vimtex#pos#set_cursor([line('$'), 1]) + endif + + let l:top = search(s:re_sec, 'ncbW') == 0 + let l:lnum = search(l:re, l:flags) + + if l:top && l:lnum > 0 && a:type == 1 && !a:backwards + let l:lnum = search(l:re, l:flags) + endif + + if a:type == 1 + call search('\S\s*\n\zs', 'Wb') + + " Move to start of file if cursor was moved to top part of document + if search(s:re_sec, 'ncbW') == 0 + call vimtex#pos#set_cursor([1, 1]) + endif + endif + endfor +endfunction + +" }}}1 +function! vimtex#motion#environment(begin, backwards, visual) abort " {{{1 + let l:count = v:count1 + if a:visual + normal! gv + endif + + let l:re = g:vimtex#re#not_comment . (a:begin ? '\\begin\s*\{' : '\\end\s*\{') + let l:flags = 'W' . (a:backwards ? 'b' : '') + + for l:_ in range(l:count) + call search(l:re, l:flags) + endfor +endfunction + +" }}}1 +function! vimtex#motion#comment(begin, backwards, visual) abort " {{{1 + let l:count = v:count1 + if a:visual + normal! gv + endif + + let l:re = a:begin + \ ? '\v%(^\s*\%.*\n)@<!\s*\%' + \ : '\v^\s*\%.*\n%(^\s*\%)@!' + let l:flags = 'W' . (a:backwards ? 'b' : '') + + for l:_ in range(l:count) + call search(l:re, l:flags) + endfor +endfunction + +" }}}1 + + +" Patterns to match section/chapter/... +let s:re_sec = '\v^\s*\\%(%(sub)?paragraph|%(sub)*section|chapter|part|' + \ . 'appendi%(x|ces)|%(front|back|main)matter)>' +let s:re_sec_t1 = '\v%(' . s:re_sec . '|^\s*%(\\end\{document\}|%$))' +let s:re_sec_t2 = '\v%(' . s:re_sec . '|^\s*\\end\{document\})' + +endif diff --git a/autoload/vimtex/parser.vim b/autoload/vimtex/parser.vim new file mode 100644 index 00000000..e1a7970f --- /dev/null +++ b/autoload/vimtex/parser.vim @@ -0,0 +1,144 @@ +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#parser#tex(file, ...) abort " {{{1 + return vimtex#parser#tex#parse(a:file, a:0 > 0 ? a:1 : {}) +endfunction + +" }}}1 +function! vimtex#parser#preamble(file, ...) abort " {{{1 + return vimtex#parser#tex#parse_preamble(a:file, a:0 > 0 ? a:1 : {}) +endfunction + +" }}}1 +function! vimtex#parser#auxiliary(file) abort " {{{1 + return vimtex#parser#auxiliary#parse(a:file) +endfunction + +" }}}1 +function! vimtex#parser#fls(file) abort " {{{1 + return vimtex#parser#fls#parse(a:file) +endfunction + +" }}}1 +function! vimtex#parser#toc(...) abort " {{{1 + let l:vimtex = a:0 > 0 ? a:1 : b:vimtex + + let l:cache = vimtex#cache#open('parsertoc', { + \ 'persistent': 0, + \ 'default': {'entries': [], 'ftime': -1}, + \}) + let l:current = l:cache.get(l:vimtex.tex) + + " Update cache if relevant + let l:ftime = l:vimtex.getftime() + if l:ftime > l:current.ftime + let l:cache.modified = 1 + let l:current.ftime = l:ftime + let l:current.entries = vimtex#parser#toc#parse(l:vimtex.tex) + endif + + return deepcopy(l:current.entries) +endfunction + +" }}}1 +function! vimtex#parser#bib(file, ...) abort " {{{1 + return vimtex#parser#bib#parse(a:file, a:0 > 0 ? a:1 : {}) +endfunction + +" }}}1 + +function! vimtex#parser#get_externalfiles() abort " {{{1 + let l:preamble = vimtex#parser#preamble(b:vimtex.tex) + + let l:result = [] + for l:line in filter(l:preamble, 'v:val =~# ''\\externaldocument''') + let l:name = matchstr(l:line, '{\zs[^}]*\ze}') + call add(l:result, { + \ 'tex' : l:name . '.tex', + \ 'aux' : l:name . '.aux', + \ 'opt' : matchstr(l:line, '\[\zs[^]]*\ze\]'), + \ }) + endfor + + return l:result +endfunction + +" }}}1 +function! vimtex#parser#selection_to_texfile(type, ...) range abort " {{{1 + " + " Get selected lines. Method depends on type of selection, which may be + " either of + " + " 1. range from argument + " 2. Command range + " 3. Visual mapping + " 4. Operator mapping + " + if a:type ==# 'arg' + let l:lines = getline(a:1[0], a:1[1]) + elseif a:type ==# 'cmd' + let l:lines = getline(a:firstline, a:lastline) + elseif a:type ==# 'visual' + let l:lines = getline(line("'<"), line("'>")) + else + let l:lines = getline(line("'["), line("']")) + endif + + " + " Use only the part of the selection that is within the + " + " \begin{document} ... \end{document} + " + " environment. + " + let l:start = 0 + let l:end = len(l:lines) + for l:n in range(len(l:lines)) + if l:lines[l:n] =~# '\\begin\s*{document}' + let l:start = l:n + 1 + elseif l:lines[l:n] =~# '\\end\s*{document}' + let l:end = l:n - 1 + break + endif + endfor + + " + " Check if the selection has any real content + " + if l:start >= len(l:lines) + \ || l:end < 0 + \ || empty(substitute(join(l:lines[l:start : l:end], ''), '\s*', '', '')) + return {} + endif + + " + " Define the set of lines to compile + " + let l:lines = vimtex#parser#preamble(b:vimtex.tex) + \ + ['\begin{document}'] + \ + l:lines[l:start : l:end] + \ + ['\end{document}'] + + " + " Write content to temporary file + " + let l:file = {} + let l:file.root = b:vimtex.root + let l:file.base = b:vimtex.name . '_vimtex_selected.tex' + let l:file.tex = l:file.root . '/' . l:file.base + let l:file.pdf = fnamemodify(l:file.tex, ':r') . '.pdf' + let l:file.log = fnamemodify(l:file.tex, ':r') . '.log' + call writefile(l:lines, l:file.tex) + + return l:file +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/auxiliary.vim b/autoload/vimtex/parser/auxiliary.vim new file mode 100644 index 00000000..b8805ebd --- /dev/null +++ b/autoload/vimtex/parser/auxiliary.vim @@ -0,0 +1,58 @@ +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#parser#auxiliary#parse(file) abort " {{{1 + return s:parse_recurse(a:file, []) +endfunction + +" }}}1 + +function! s:parse_recurse(file, parsed) abort " {{{1 + if !filereadable(a:file) || index(a:parsed, a:file) >= 0 + return [] + endif + call add(a:parsed, a:file) + + let l:lines = [] + for l:line in readfile(a:file) + call add(l:lines, l:line) + + if l:line =~# '\\@input{' + let l:file = s:input_line_parser(l:line, a:file) + call extend(l:lines, s:parse_recurse(l:file, a:parsed)) + endif + endfor + + return l:lines +endfunction + +" }}}1 + +function! s:input_line_parser(line, file) abort " {{{1 + let l:file = matchstr(a:line, '\\@input{\zs[^}]\+\ze}') + + " Remove extension to simplify the parsing (e.g. for "my file name".aux) + let l:file = substitute(l:file, '\.aux', '', '') + + " Trim whitespaces and quotes from beginning/end of string, append extension + let l:file = substitute(l:file, '^\(\s\|"\)*', '', '') + let l:file = substitute(l:file, '\(\s\|"\)*$', '', '') + let l:file .= '.aux' + + " Use absolute paths + if l:file !~# '\v^(\/|[A-Z]:)' + let l:file = fnamemodify(a:file, ':p:h') . '/' . l:file + endif + + " Only return filename if it is readable + return filereadable(l:file) ? l:file : '' +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/bib.vim b/autoload/vimtex/parser/bib.vim new file mode 100644 index 00000000..7ea2c238 --- /dev/null +++ b/autoload/vimtex/parser/bib.vim @@ -0,0 +1,370 @@ +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#parser#bib#parse(file, opts) abort " {{{1 + if !filereadable(a:file) | return [] | endif + + let l:backend = get(a:opts, 'backend', g:vimtex_parser_bib_backend) + + if l:backend ==# 'bibtex' + if !executable('bibtex') | let l:backend = 'vim' | endif + elseif l:backend ==# 'bibparse' + if !executable('bibparse') | let l:backend = 'vim' | endif + else + let l:backend = 'vim' + endif + + return s:parse_with_{l:backend}(a:file) +endfunction + +" }}}1 + + +function! s:parse_with_bibtex(file) abort " {{{1 + call s:parse_with_bibtex_init() + if s:bibtex_not_executable | return [] | endif + + " Define temporary files + let tmp = { + \ 'aux' : 'tmpfile.aux', + \ 'bbl' : 'tmpfile.bbl', + \ 'blg' : 'tmpfile.blg', + \ } + + " Write temporary aux file + call writefile([ + \ '\citation{*}', + \ '\bibstyle{' . s:bibtex_bstfile . '}', + \ '\bibdata{' . fnamemodify(a:file, ':r') . '}', + \ ], tmp.aux) + + " Create the temporary bbl file + call vimtex#process#run('bibtex -terse ' . fnameescape(tmp.aux), { + \ 'background' : 0, + \ 'silent' : 1, + \}) + + " Parse temporary bbl file + let lines = join(readfile(tmp.bbl), "\n") + let lines = substitute(lines, '\n\n\@!\(\s\=\)\s*\|{\|}', '\1', 'g') + let lines = vimtex#util#tex2unicode(lines) + let lines = split(lines, "\n") + + let l:entries = [] + for line in lines + let matches = split(line, '||') + if empty(matches) || empty(matches[0]) | continue | endif + + let l:entry = { + \ 'key': matches[0], + \ 'type': matches[1], + \} + + if !empty(matches[2]) + let l:entry.author = matches[2] + endif + if !empty(matches[3]) + let l:entry.year = matches[3] + endif + if !empty(get(matches, 4, '')) + let l:entry.title = get(matches, 4, '') + endif + + call add(l:entries, l:entry) + endfor + + " Clean up + call delete(tmp.aux) + call delete(tmp.bbl) + call delete(tmp.blg) + + return l:entries +endfunction + +" }}}1 +function! s:parse_with_bibtex_init() abort " {{{1 + if exists('s:bibtex_init_done') | return | endif + + " Check if bibtex is executable + let s:bibtex_not_executable = !executable('bibtex') + if s:bibtex_not_executable + call vimtex#log#warning( + \ 'bibtex is not executable and may not be used to parse bib files!') + endif + + " Check if bstfile contains whitespace (not handled by vimtex) + if stridx(s:bibtex_bstfile, ' ') >= 0 + let l:oldbst = s:bibtex_bstfile . '.bst' + let s:bibtex_bstfile = tempname() + call writefile(readfile(l:oldbst), s:bibtex_bstfile . '.bst') + endif + + let s:bibtex_init_done = 1 +endfunction + +let s:bibtex_bstfile = expand('<sfile>:p:h') . '/vimcomplete' + +" }}}1 + +function! s:parse_with_bibparse(file) abort " {{{1 + call s:parse_with_bibparse_init() + if s:bibparse_not_executable | return [] | endif + + call vimtex#process#run('bibparse ' . fnameescape(a:file) + \ . ' >_vimtex_bibparsed.log', {'background' : 0, 'silent' : 1}) + let l:lines = readfile('_vimtex_bibparsed.log') + call delete('_vimtex_bibparsed.log') + + let l:current = {} + let l:entries = [] + for l:line in l:lines + if l:line[0] ==# '@' + if !empty(l:current) + call add(l:entries, l:current) + let l:current = {} + endif + + let l:index = stridx(l:line, ' ') + if l:index > 0 + let l:type = l:line[1:l:index-1] + let l:current.type = l:type + let l:current.key = l:line[l:index+1:] + endif + elseif !empty(l:current) + let l:index = stridx(l:line, '=') + if l:index < 0 | continue | endif + + let l:key = l:line[:l:index-1] + let l:value = l:line[l:index+1:] + let l:current[tolower(l:key)] = l:value + endif + endfor + + if !empty(l:current) + call add(l:entries, l:current) + endif + + return l:entries +endfunction + +" }}}1 +function! s:parse_with_bibparse_init() abort " {{{1 + if exists('s:bibparse_init_done') | return | endif + + " Check if bibtex is executable + let s:bibparse_not_executable = !executable('bibparse') + if s:bibparse_not_executable + call vimtex#log#warning( + \ 'bibparse is not executable and may not be used to parse bib files!') + endif + + let s:bibparse_init_done = 1 +endfunction + +" }}}1 + +function! s:parse_with_vim(file) abort " {{{1 + " Adheres to the format description found here: + " http://www.bibtex.org/Format/ + + if !filereadable(a:file) + return [] + endif + + let l:current = {} + let l:strings = {} + let l:entries = [] + for l:line in filter(readfile(a:file), 'v:val !~# ''^\s*\%(%\|$\)''') + if empty(l:current) + if s:parse_type(l:line, l:current, l:strings) + let l:current = {} + endif + continue + endif + + if l:current.type ==# 'string' + if s:parse_string(l:line, l:current, l:strings) + let l:current = {} + endif + else + if s:parse_entry(l:line, l:current, l:entries) + let l:current = {} + endif + endif + endfor + + return map(l:entries, 's:parse_entry_body(v:val, l:strings)') +endfunction + +" }}}1 + +function! s:parse_type(line, current, strings) abort " {{{1 + let l:matches = matchlist(a:line, '\v^\@(\w+)\s*\{\s*(.*)') + if empty(l:matches) | return 0 | endif + + let l:type = tolower(l:matches[1]) + if index(['preamble', 'comment'], l:type) >= 0 | return 0 | endif + + let a:current.level = 1 + let a:current.body = '' + + if l:type ==# 'string' + return s:parse_string(l:matches[2], a:current, a:strings) + else + let a:current.type = l:type + let a:current.key = matchstr(l:matches[2], '.*\ze,\s*') + return 0 + endif +endfunction + +" }}}1 +function! s:parse_string(line, string, strings) abort " {{{1 + let a:string.level += s:count(a:line, '{') - s:count(a:line, '}') + if a:string.level > 0 + let a:string.body .= a:line + return 0 + endif + + let a:string.body .= matchstr(a:line, '.*\ze}') + + let l:matches = matchlist(a:string.body, '\v^\s*(\w+)\s*\=\s*"(.*)"\s*$') + if !empty(l:matches) && !empty(l:matches[1]) + let a:strings[l:matches[1]] = l:matches[2] + endif + + return 1 +endfunction + +" }}}1 +function! s:parse_entry(line, entry, entries) abort " {{{1 + let a:entry.level += s:count(a:line, '{') - s:count(a:line, '}') + if a:entry.level > 0 + let a:entry.body .= a:line + return 0 + endif + + let a:entry.body .= matchstr(a:line, '.*\ze}') + + call add(a:entries, a:entry) + return 1 +endfunction + +" }}}1 + +function! s:parse_entry_body(entry, strings) abort " {{{1 + unlet a:entry.level + + let l:key = '' + let l:pos = matchend(a:entry.body, '^\s*') + while l:pos >= 0 + if empty(l:key) + let [l:key, l:pos] = s:get_key(a:entry.body, l:pos) + else + let [l:value, l:pos] = s:get_value(a:entry.body, l:pos, a:strings) + let a:entry[l:key] = l:value + let l:key = '' + endif + endwhile + + unlet a:entry.body + return a:entry +endfunction + +" }}}1 +function! s:get_key(body, head) abort " {{{1 + " Parse the key part of a bib entry tag. + " Assumption: a:body is left trimmed and either empty or starts with a key. + " Returns: The key and the remaining part of the entry body. + + let l:matches = matchlist(a:body, '^\v(\w+)\s*\=\s*', a:head) + return empty(l:matches) + \ ? ['', -1] + \ : [tolower(l:matches[1]), a:head + strlen(l:matches[0])] +endfunction + +" }}}1 +function! s:get_value(body, head, strings) abort " {{{1 + " Parse the value part of a bib entry tag, until separating comma or end. + " Assumption: a:body is left trimmed and either empty or starts with a value. + " Returns: The value and the remaining part of the entry body. + " + " A bib entry value is either + " 1. A number. + " 2. A concatenation (with #s) of double quoted strings, curlied strings, + " and/or bibvariables, + " + if a:body[a:head] =~# '\d' + let l:value = matchstr(a:body, '^\d\+', a:head) + let l:head = matchend(a:body, '^\s*,\s*', a:head + len(l:value)) + return [l:value, l:head] + else + return s:get_value_string(a:body, a:head, a:strings) + endif + + return ['s:get_value failed', -1] +endfunction + +" }}}1 +function! s:get_value_string(body, head, strings) abort " {{{1 + if a:body[a:head] ==# '{' + let l:sum = 1 + let l:i1 = a:head + 1 + let l:i0 = l:i1 + + while l:sum > 0 + let [l:match, l:_, l:i1] = matchstrpos(a:body, '[{}]', l:i1) + if l:i1 < 0 | break | endif + + let l:i0 = l:i1 + let l:sum += l:match ==# '{' ? 1 : -1 + endwhile + + let l:value = a:body[a:head+1:l:i0-2] + let l:head = matchend(a:body, '^\s*', l:i0) + elseif a:body[a:head] ==# '"' + let l:index = match(a:body, '\\\@<!"', a:head+1) + if l:index < 0 + return ['s:get_value_string failed', ''] + endif + + let l:value = a:body[a:head+1:l:index-1] + let l:head = matchend(a:body, '^\s*', l:index+1) + return [l:value, l:head] + elseif a:body[a:head:] =~# '^\w' + let l:value = matchstr(a:body, '^\w\+', a:head) + let l:head = matchend(a:body, '^\s*', a:head + strlen(l:value)) + let l:value = get(a:strings, l:value, '@(' . l:value . ')') + else + let l:head = a:head + endif + + if a:body[l:head] ==# '#' + let l:head = matchend(a:body, '^\s*', l:head + 1) + let [l:vadd, l:head] = s:get_value_string(a:body, l:head, a:strings) + let l:value .= l:vadd + endif + + return [l:value, matchend(a:body, '^,\s*', l:head)] +endfunction + +" }}}1 + +function! s:count(container, item) abort " {{{1 + " Necessary because in old Vim versions, count() does not work for strings + try + let l:count = count(a:container, a:item) + catch /E712/ + let l:count = count(split(a:container, '\zs'), a:item) + endtry + + return l:count +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/fls.vim b/autoload/vimtex/parser/fls.vim new file mode 100644 index 00000000..46b0db2f --- /dev/null +++ b/autoload/vimtex/parser/fls.vim @@ -0,0 +1,19 @@ +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#parser#fls#parse(file) abort " {{{1 + if !filereadable(a:file) + return [] + endif + + return readfile(a:file) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/tex.vim b/autoload/vimtex/parser/tex.vim new file mode 100644 index 00000000..6259b5fa --- /dev/null +++ b/autoload/vimtex/parser/tex.vim @@ -0,0 +1,205 @@ +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#parser#tex#parse(file, opts) abort " {{{1 + let l:opts = extend({ + \ 'detailed': 1, + \ 'root' : exists('b:vimtex.root') ? b:vimtex.root : '', + \}, a:opts) + + let l:cache = vimtex#cache#open('texparser', { + \ 'local': 1, + \ 'persistent': 0, + \ 'default': {'ftime': -2}, + \}) + + let l:parsed = s:parse(a:file, l:opts, l:cache) + + if !l:opts.detailed + call map(l:parsed, 'v:val[2]') + endif + + return l:parsed +endfunction + +" }}}1 +function! vimtex#parser#tex#parse_files(file, opts) abort " {{{1 + let l:opts = extend({ + \ 'root' : exists('b:vimtex.root') ? b:vimtex.root : '', + \}, a:opts) + + let l:cache = vimtex#cache#open('texparser', { + \ 'local': 1, + \ 'persistent': 0, + \ 'default': {'ftime': -2}, + \}) + + return vimtex#util#uniq_unsorted( + \ s:parse_files(a:file, l:opts, l:cache)) +endfunction + +" }}}1 +function! vimtex#parser#tex#parse_preamble(file, opts) abort " {{{1 + let l:opts = extend({ + \ 'inclusive' : 0, + \ 'root' : exists('b:vimtex.root') ? b:vimtex.root : '', + \}, a:opts) + + return s:parse_preamble(a:file, l:opts, []) +endfunction + +" }}}1 + +function! s:parse(file, opts, cache) abort " {{{1 + let l:current = a:cache.get(a:file) + let l:ftime = getftime(a:file) + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + call s:parse_current(a:file, a:opts, l:current) + endif + + let l:parsed = [] + + for l:val in l:current.lines + if type(l:val) == type([]) + call add(l:parsed, l:val) + else + call extend(l:parsed, s:parse(l:val, a:opts, a:cache)) + endif + endfor + + return l:parsed +endfunction + +" }}}1 +function! s:parse_files(file, opts, cache) abort " {{{1 + let l:current = a:cache.get(a:file) + let l:ftime = getftime(a:file) + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + call s:parse_current(a:file, a:opts, l:current) + endif + + " Only include existing files + if !filereadable(a:file) | return [] | endif + + let l:files = [a:file] + for l:file in l:current.includes + let l:files += s:parse_files(l:file, a:opts, a:cache) + endfor + + return l:files +endfunction + +" }}}1 +function! s:parse_current(file, opts, current) abort " {{{1 + let a:current.lines = [] + let a:current.includes = [] + + " Also load includes from glsentries + let l:re_input = g:vimtex#re#tex_input . '|^\s*\\loadglsentries' + + if filereadable(a:file) + let l:lnum = 0 + for l:line in readfile(a:file) + let l:lnum += 1 + call add(a:current.lines, [a:file, l:lnum, l:line]) + + " Minor optimization: Avoid complex regex on "simple" lines + if stridx(l:line, '\') < 0 | continue | endif + + if l:line =~# l:re_input + let l:file = s:input_parser(l:line, a:file, a:opts.root) + call add(a:current.lines, l:file) + call add(a:current.includes, l:file) + endif + endfor + endif +endfunction + +" }}}1 +function! s:parse_preamble(file, opts, parsed_files) abort " {{{1 + if !filereadable(a:file) || index(a:parsed_files, a:file) >= 0 + return [] + endif + call add(a:parsed_files, a:file) + + let l:lines = [] + for l:line in readfile(a:file) + if l:line =~# '\\begin\s*{document}' + if a:opts.inclusive + call add(l:lines, l:line) + endif + break + endif + + call add(l:lines, l:line) + + if l:line =~# g:vimtex#re#tex_input + let l:file = s:input_parser(l:line, a:file, a:opts.root) + call extend(l:lines, s:parse_preamble(l:file, a:opts, a:parsed_files)) + endif + endfor + + return l:lines +endfunction + +" }}}1 + +function! s:input_parser(line, current_file, root) abort " {{{1 + " Handle \space commands + let l:file = substitute(a:line, '\\space\s*', ' ', 'g') + + " Handle import package commands + if l:file =~# g:vimtex#re#tex_input_import + let l:root = l:file =~# '\\sub' + \ ? fnamemodify(a:current_file, ':p:h') + \ : a:root + + let l:candidate = s:input_to_filename( + \ substitute(copy(l:file), '}\s*{', '', 'g'), l:root) + if !empty(l:candidate) + return l:candidate + else + return s:input_to_filename( + \ substitute(copy(l:file), '{.{-}}', '', ''), l:root) + endif + else + return s:input_to_filename(l:file, a:root) + endif +endfunction + +" }}}1 +function! s:input_to_filename(input, root) abort " {{{1 + let l:file = matchstr(a:input, '\zs[^{}]\+\ze}\s*\%(%\|$\)') + + " Trim whitespaces and quotes from beginning/end of string + let l:file = substitute(l:file, '^\(\s\|"\)*', '', '') + let l:file = substitute(l:file, '\(\s\|"\)*$', '', '') + + " Ensure that the file name has extension + if empty(fnamemodify(l:file, ':e')) + let l:file .= '.tex' + endif + + if vimtex#paths#is_abs(l:file) + return l:file + endif + + let l:candidate = a:root . '/' . l:file + if filereadable(l:candidate) + return l:candidate + endif + + let l:candidate = vimtex#kpsewhich#find(l:file) + return filereadable(l:candidate) ? l:candidate : l:file +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/toc.vim b/autoload/vimtex/parser/toc.vim new file mode 100644 index 00000000..517a25be --- /dev/null +++ b/autoload/vimtex/parser/toc.vim @@ -0,0 +1,778 @@ +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 +" +" +" Parses tex project for ToC-like entries. Each entry is a dictionary +" similar to the following: +" +" entry = { +" title : "Some title", +" number : "3.1.2", +" file : /path/to/file.tex, +" line : 142, +" rank : cumulative line number, +" level : 2, +" type : [content | label | todo | include], +" link : [0 | 1], +" } +" + +function! vimtex#parser#toc#parse(file) abort " {{{1 + let l:entries = [] + let l:content = vimtex#parser#tex(a:file) + + let l:max_level = 0 + for [l:file, l:lnum, l:line] in l:content + if l:line =~# s:matcher_sections.re + let l:max_level = max([ + \ l:max_level, + \ s:sec_to_value[matchstr(l:line, s:matcher_sections.re_level)] + \]) + endif + endfor + + call s:level.reset('preamble', l:max_level) + + " No more parsing if there is no content + if empty(l:content) | return l:entries | endif + + " + " Begin parsing LaTeX files + " + let l:lnum_total = 0 + let l:matchers = s:matchers_preamble + for [l:file, l:lnum, l:line] in l:content + let l:lnum_total += 1 + let l:context = { + \ 'file' : l:file, + \ 'line' : l:line, + \ 'lnum' : l:lnum, + \ 'lnum_total' : l:lnum_total, + \ 'level' : s:level, + \ 'max_level' : l:max_level, + \ 'entry' : get(l:entries, -1, {}), + \ 'num_entries' : len(l:entries), + \} + + " Detect end of preamble + if s:level.preamble && l:line =~# '\v^\s*\\begin\{document\}' + let s:level.preamble = 0 + let l:matchers = s:matchers_content + continue + endif + + " Handle multi-line entries + if exists('s:matcher_continue') + call s:matcher_continue.continue(l:context) + continue + endif + + " Apply prefilter - this gives considerable speedup for large documents + if l:line !~# s:re_prefilter | continue | endif + + " Apply the matchers + for l:matcher in l:matchers + if l:line =~# l:matcher.re + let l:entry = l:matcher.get_entry(l:context) + if type(l:entry) == type([]) + call extend(l:entries, l:entry) + elseif !empty(l:entry) + call add(l:entries, l:entry) + endif + endif + endfor + endfor + + for l:matcher in s:matchers + try + call l:matcher.filter(l:entries) + catch /E716/ + endtry + endfor + + return l:entries +endfunction + +" }}}1 +function! vimtex#parser#toc#get_topmatters() abort " {{{1 + let l:topmatters = s:level.frontmatter + let l:topmatters += s:level.mainmatter + let l:topmatters += s:level.appendix + let l:topmatters += s:level.backmatter + + for l:level in get(s:level, 'old', []) + let l:topmatters += l:level.frontmatter + let l:topmatters += l:level.mainmatter + let l:topmatters += l:level.appendix + let l:topmatters += l:level.backmatter + endfor + + return l:topmatters +endfunction + +" }}}1 +function! vimtex#parser#toc#get_entry_general(context) abort dict " {{{1 + return { + \ 'title' : self.title, + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'rank' : a:context.lnum_total, + \ 'level' : 0, + \ 'type' : 'content', + \} +endfunction + +" }}}1 + +" IMPORTANT: The following defines a prefilter for optimizing the toc parser. +" Any line that should be parsed has to be matched by this regexp! +" {{{1 let s:re_prefilter = ... +let s:re_prefilter = '\v%(\\' . join([ + \ '%(front|main|back)matter', + \ 'add%(global|section)?bib', + \ 'appendix', + \ 'begin', + \ 'bibliography', + \ 'chapter', + \ 'documentclass', + \ 'import', + \ 'include', + \ 'includegraphics', + \ 'input', + \ 'label', + \ 'part', + \ 'printbib', + \ 'printindex', + \ 'paragraph', + \ 'section', + \ 'subfile', + \ 'tableofcontents', + \ 'todo', + \], '|') . ')' + \ . '|\%\s*%(' . join(g:vimtex_toc_todo_keywords, '|') . ')' + \ . '|\%\s*vimtex-include' +for s:m in g:vimtex_toc_custom_matchers + if has_key(s:m, 'prefilter') + let s:re_prefilter .= '|' . s:m.prefilter + endif +endfor + +" }}}1 + +" Adds entries for included files +let s:matcher_include = { + \ 're' : vimtex#re#tex_input . '\zs\f{-}\s*\ze\}', + \ 'in_preamble' : 1, + \ 'priority' : 0, + \} +function! s:matcher_include.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + if !vimtex#paths#is_abs(l:file[0]) + let l:file = b:vimtex.root . '/' . l:file + endif + let l:file = fnamemodify(l:file, ':~:.') + if !filereadable(l:file) + let l:file .= '.tex' + endif + return { + \ 'title' : 'tex incl: ' . (strlen(l:file) < 70 + \ ? l:file + \ : l:file[0:30] . '...' . l:file[-36:]), + \ 'number' : '', + \ 'file' : l:file, + \ 'line' : 1, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ } +endfunction + +" }}}1 + +" Adds entries for included graphics files (filetype tikz, tex) +let s:matcher_include_graphics = { + \ 're' : '\v^\s*\\includegraphics\*?%(\s*\[[^]]*\]){0,2}\s*\{\zs[^}]*', + \ 'priority' : 1, + \} +function! s:matcher_include_graphics.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + if !vimtex#paths#is_abs(l:file) + let l:file = vimtex#misc#get_graphicspath(l:file) + endif + let l:file = fnamemodify(l:file, ':~:.') + let l:ext = fnamemodify(l:file, ':e') + + return !filereadable(l:file) || index(['asy', 'tikz'], l:ext) < 0 + \ ? {} + \ : { + \ 'title' : 'fig incl: ' . (strlen(l:file) < 70 + \ ? l:file + \ : l:file[0:30] . '...' . l:file[-36:]), + \ 'number' : '', + \ 'file' : l:file, + \ 'line' : 1, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \ } +endfunction + +" }}}1 + +" Adds entries for included files through vimtex specific syntax (this allows +" to add entries for any filetype or file) +let s:matcher_include_vimtex = { + \ 're' : '^\s*%\s*vimtex-include:\?\s\+\zs\f\+', + \ 'in_preamble' : 1, + \ 'priority' : 1, + \} +function! s:matcher_include_vimtex.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + if !vimtex#paths#is_abs(l:file) + let l:file = b:vimtex.root . '/' . l:file + endif + let l:file = fnamemodify(l:file, ':~:.') + return { + \ 'title' : 'vtx incl: ' . (strlen(l:file) < 70 + \ ? l:file + \ : l:file[0:30] . '...' . l:file[-36:]), + \ 'number' : '', + \ 'file' : l:file, + \ 'line' : 1, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \ } +endfunction + +" }}}1 + +let s:matcher_include_bibtex = { + \ 're' : '\v^\s*\\bibliography\s*\{\zs[^}]+\ze\}', + \ 'in_preamble' : 1, + \ 'priority' : 0, + \} +function! s:matcher_include_bibtex.get_entry(context) abort dict " {{{1 + let l:entries = [] + + for l:file in split(matchstr(a:context.line, self.re), ',') + " Ensure that the file name has extension + if l:file !~# '\.bib$' + let l:file .= '.bib' + endif + + call add(l:entries, { + \ 'title' : printf('bib incl: %-.67s', fnamemodify(l:file, ':t')), + \ 'number' : '', + \ 'file' : vimtex#kpsewhich#find(l:file), + \ 'line' : 1, + \ 'level' : 0, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \}) + endfor + + return l:entries +endfunction + +" }}}1 + +let s:matcher_include_biblatex = { + \ 're' : '\v^\s*\\add(bibresource|globalbib|sectionbib)\s*\{\zs[^}]+\ze\}', + \ 'in_preamble' : 1, + \ 'in_content' : 0, + \ 'priority' : 0, + \} +function! s:matcher_include_biblatex.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + + return { + \ 'title' : printf('bib incl: %-.67s', fnamemodify(l:file, ':t')), + \ 'number' : '', + \ 'file' : vimtex#kpsewhich#find(l:file), + \ 'line' : 1, + \ 'level' : 0, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \} +endfunction + +" }}}1 + +let s:matcher_preamble = { + \ 're' : '\v^\s*\\documentclass', + \ 'in_preamble' : 1, + \ 'in_content' : 0, + \ 'priority' : 0, + \} +function! s:matcher_preamble.get_entry(context) abort " {{{1 + return g:vimtex_toc_show_preamble + \ ? { + \ 'title' : 'Preamble', + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : 0, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'content', + \ } + \ : {} +endfunction + +" }}}1 + +let s:matcher_parts = { + \ 're' : '\v^\s*\\\zs((front|main|back)matter|appendix)>', + \ 'priority' : 0, + \} +function! s:matcher_parts.get_entry(context) abort dict " {{{1 + call a:context.level.reset( + \ matchstr(a:context.line, self.re), + \ a:context.max_level) + return {} +endfunction + +" }}}1 + +let s:matcher_sections = { + \ 're' : '\v^\s*\\%(part|chapter|%(sub)*section|%(sub)?paragraph)\*?\s*(\[|\{)', + \ 're_starred' : '\v^\s*\\%(part|chapter|%(sub)*section)\*', + \ 're_level' : '\v^\s*\\\zs%(part|chapter|%(sub)*section|%(sub)?paragraph)', + \ 'priority' : 0, + \} +let s:matcher_sections.re_title = s:matcher_sections.re . '\zs.{-}\ze\%?\s*$' +function! s:matcher_sections.get_entry(context) abort dict " {{{1 + let level = matchstr(a:context.line, self.re_level) + let type = matchlist(a:context.line, self.re)[1] + let title = matchstr(a:context.line, self.re_title) + let number = '' + + let [l:end, l:count] = s:find_closing(0, title, 1, type) + if l:count == 0 + let title = self.parse_title(strpart(title, 0, l:end+1)) + else + let self.type = type + let self.count = l:count + let s:matcher_continue = deepcopy(self) + endif + + if a:context.line !~# self.re_starred + call a:context.level.increment(level) + if a:context.line !~# '\v^\s*\\%(sub)?paragraph' + let number = deepcopy(a:context.level) + endif + endif + + return { + \ 'title' : title, + \ 'number' : number, + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'content', + \ } +endfunction + +" }}}1 +function! s:matcher_sections.parse_title(title) abort dict " {{{1 + let l:title = substitute(a:title, '\v%(\]|\})\s*$', '', '') + return s:clear_texorpdfstring(l:title) +endfunction + +" }}}1 +function! s:matcher_sections.continue(context) abort dict " {{{1 + let [l:end, l:count] = s:find_closing(0, a:context.line, self.count, self.type) + if l:count == 0 + let a:context.entry.title = self.parse_title(a:context.entry.title . strpart(a:context.line, 0, l:end+1)) + unlet! s:matcher_continue + else + let a:context.entry.title .= a:context.line + let self.count = l:count + endif +endfunction + +" }}}1 + +let s:matcher_table_of_contents = { + \ 'title' : 'Table of contents', + \ 're' : '\v^\s*\\tableofcontents', + \ 'priority' : 0, + \} + +let s:matcher_index = { + \ 'title' : 'Alphabetical index', + \ 're' : '\v^\s*\\printindex\[?', + \ 'priority' : 0, + \} + +let s:matcher_titlepage = { + \ 'title' : 'Titlepage', + \ 're' : '\v^\s*\\begin\{titlepage\}', + \ 'priority' : 0, + \} + +let s:matcher_bibliography = { + \ 'title' : 'Bibliography', + \ 're' : '\v^\s*\\%(' + \ . 'printbib%(liography|heading)\s*(\{|\[)?' + \ . '|begin\s*\{\s*thebibliography\s*\}' + \ . '|bibliography\s*\{)', + \ 're_biblatex' : '\v^\s*\\printbib%(liography|heading)', + \ 'priority' : 0, + \} +function! s:matcher_bibliography.get_entry(context) abort dict " {{{1 + let l:entry = call('vimtex#parser#toc#get_entry_general', [a:context], self) + + if a:context.line !~# self.re_biblatex + return l:entry + endif + + let self.options = matchstr(a:context.line, self.re_biblatex . '\s*\[\zs.*') + + let [l:end, l:count] = s:find_closing( + \ 0, self.options, !empty(self.options), '[') + if l:count == 0 + let self.options = strpart(self.options, 0, l:end) + call self.parse_options(a:context, l:entry) + else + let self.count = l:count + let s:matcher_continue = deepcopy(self) + endif + + return l:entry +endfunction + +" }}}1 +function! s:matcher_bibliography.continue(context) abort dict " {{{1 + let [l:end, l:count] = s:find_closing(0, a:context.line, self.count, '[') + if l:count == 0 + let self.options .= strpart(a:context.line, 0, l:end) + unlet! s:matcher_continue + call self.parse_options(a:context, a:context.entry) + else + let self.options .= a:context.line + let self.count = l:count + endif +endfunction + +" }}}1 +function! s:matcher_bibliography.parse_options(context, entry) abort dict " {{{1 + " Parse the options + let l:opt_pairs = map(split(self.options, ','), 'split(v:val, ''='')') + let l:opts = {} + for [l:key, l:val] in l:opt_pairs + let l:key = substitute(l:key, '^\s*\|\s*$', '', 'g') + let l:val = substitute(l:val, '^\s*\|\s*$', '', 'g') + let l:val = substitute(l:val, '{\|}', '', 'g') + let l:opts[l:key] = l:val + endfor + + " Check if entry should appear in the TOC + let l:heading = get(l:opts, 'heading') + let a:entry.added_to_toc = l:heading =~# 'intoc\|numbered' + + " Check if entry should be numbered + if l:heading =~# '\v%(sub)?bibnumbered' + if a:context.level.chapter > 0 + let l:levels = ['chapter', 'section'] + else + let l:levels = ['section', 'subsection'] + endif + call a:context.level.increment(l:levels[l:heading =~# '^sub']) + let a:entry.level = a:context.max_level - a:context.level.current + let a:entry.number = deepcopy(a:context.level) + endif + + " Parse title + try + let a:entry.title = remove(l:opts, 'title') + catch /E716/ + let a:entry.title = l:heading =~# '^sub' ? 'References' : 'Bibliography' + endtry +endfunction + +" }}}1 +function! s:matcher_bibliography.filter(entries) abort dict " {{{1 + if !empty( + \ filter(deepcopy(a:entries), 'get(v:val, "added_to_toc")')) + call filter(a:entries, 'get(v:val, "added_to_toc", 1)') + endif +endfunction + +" }}}1 + +let s:matcher_todos = { + \ 're' : g:vimtex#re#not_bslash . '\%\s+(' + \ . join(g:vimtex_toc_todo_keywords, '|') . ')[ :]+\s*(.*)', + \ 'in_preamble' : 1, + \ 'priority' : 2, + \} +function! s:matcher_todos.get_entry(context) abort dict " {{{1 + let [l:type, l:text] = matchlist(a:context.line, self.re)[1:2] + return { + \ 'title' : toupper(l:type) . ': ' . l:text, + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'todo', + \ } +endfunction + +" }}}1 + +let s:matcher_todonotes = { + \ 're' : g:vimtex#re#not_comment . '\\\w*todo\w*%(\[[^]]*\])?\{\zs.*', + \ 'priority' : 2, + \} +function! s:matcher_todonotes.get_entry(context) abort dict " {{{1 + let title = matchstr(a:context.line, self.re) + + let [l:end, l:count] = s:find_closing(0, title, 1, '{') + if l:count == 0 + let title = strpart(title, 0, l:end) + else + let self.count = l:count + let s:matcher_continue = deepcopy(self) + endif + + return { + \ 'title' : 'TODO: ' . title, + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'todo', + \ } +endfunction + +" }}}1 +function! s:matcher_todonotes.continue(context) abort dict " {{{1 + let [l:end, l:count] = s:find_closing(0, a:context.line, self.count, '{') + if l:count == 0 + let a:context.entry.title .= strpart(a:context.line, 0, l:end) + unlet! s:matcher_continue + else + let a:context.entry.title .= a:context.line + let self.count = l:count + endif +endfunction + +" }}}1 + +let s:matcher_labels = { + \ 're' : g:vimtex#re#not_comment . '\\label\{\zs.{-}\ze\}', + \ 'priority' : 1, + \} +function! s:matcher_labels.get_entry(context) abort dict " {{{1 + return { + \ 'title' : matchstr(a:context.line, self.re), + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'label', + \ } +endfunction +" }}}1 + +let s:matcher_beamer_frame = { + \ 're' : '^\s*\\begin{frame}', + \ 'priority' : 0, + \} +function! s:matcher_beamer_frame.get_entry(context) abort dict " {{{1 + let l:title = vimtex#util#trim( + \ matchstr(a:context.line, self.re . '\s*{\zs.*\ze}\s*$')) + + return { + \ 'title' : 'Frame' . (empty(l:title) ? '' : ': ' . l:title), + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'content', + \ } +endfunction +" }}}1 + +" +" Utility functions +" +function! s:clear_texorpdfstring(title) abort " {{{1 + let l:i1 = match(a:title, '\\texorpdfstring') + if l:i1 < 0 | return a:title | endif + + " Find start of included part + let [l:i2, l:dummy] = s:find_closing( + \ match(a:title, '{', l:i1+1), a:title, 1, '{') + let l:i2 = match(a:title, '{', l:i2+1) + if l:i2 < 0 | return a:title | endif + + " Find end of included part + let [l:i3, l:dummy] = s:find_closing(l:i2, a:title, 1, '{') + if l:i3 < 0 | return a:title | endif + + return strpart(a:title, 0, l:i1) + \ . strpart(a:title, l:i2+1, l:i3-l:i2-1) + \ . s:clear_texorpdfstring(strpart(a:title, l:i3+1)) +endfunction + +" }}}1 +function! s:find_closing(start, string, count, type) abort " {{{1 + if a:type ==# '{' + let l:re = '{\|}' + let l:open = '{' + else + let l:re = '\[\|\]' + let l:open = '[' + endif + let l:i2 = a:start-1 + let l:count = a:count + while l:count > 0 + let l:i2 = match(a:string, l:re, l:i2+1) + if l:i2 < 0 | break | endif + + if a:string[l:i2] ==# l:open + let l:count += 1 + else + let l:count -= 1 + endif + endwhile + + return [l:i2, l:count] +endfunction + +" }}}1 +function! s:sort_by_priority(d1, d2) abort " {{{1 + let l:p1 = get(a:d1, 'priority') + let l:p2 = get(a:d2, 'priority') + return l:p1 >= l:p2 ? l:p1 > l:p2 : -1 +endfunction + +" }}}1 + +" +" Section level counter +" +let s:level = {} +function! s:level.reset(part, level) abort dict " {{{1 + if a:part ==# 'preamble' + let self.old = [] + else + let self.old += [copy(self)] + endif + + let self.preamble = 0 + let self.frontmatter = 0 + let self.mainmatter = 0 + let self.appendix = 0 + let self.backmatter = 0 + let self.part = 0 + let self.chapter = 0 + let self.section = 0 + let self.subsection = 0 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + let self.current = a:level + let self[a:part] = 1 +endfunction + +" }}}1 +function! s:level.increment(level) abort dict " {{{1 + let self.current = s:sec_to_value[a:level] + + let self.part_toggle = 0 + + if a:level ==# 'part' + let self.part += 1 + let self.part_toggle = 1 + elseif a:level ==# 'chapter' + let self.chapter += 1 + let self.section = 0 + let self.subsection = 0 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'section' + let self.section += 1 + let self.subsection = 0 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'subsection' + let self.subsection += 1 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'subsubsection' + let self.subsubsection += 1 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'subsubsubsection' + let self.subsubsubsection += 1 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'paragraph' + let self.paragraph += 1 + let self.subparagraph = 0 + elseif a:level ==# 'subparagraph' + let self.subparagraph += 1 + endif +endfunction + +" }}}1 + +let s:sec_to_value = { + \ '_' : 0, + \ 'subparagraph' : 1, + \ 'paragraph' : 2, + \ 'subsubsubsection' : 3, + \ 'subsubsection' : 4, + \ 'subsection' : 5, + \ 'section' : 6, + \ 'chapter' : 7, + \ 'part' : 8, + \ } + +" +" Create the lists of matchers +" +let s:matchers = map( + \ filter(items(s:), 'v:val[0] =~# ''^matcher_'''), + \ 'v:val[1]') + \ + g:vimtex_toc_custom_matchers +call sort(s:matchers, function('s:sort_by_priority')) + +for s:m in s:matchers + if !has_key(s:m, 'get_entry') + let s:m.get_entry = function('vimtex#parser#toc#get_entry_general') + endif +endfor +unlet! s:m + +let s:matchers_preamble = filter( + \ deepcopy(s:matchers), "get(v:val, 'in_preamble')") +let s:matchers_content = filter( + \ deepcopy(s:matchers), "get(v:val, 'in_content', 1)") + +endif diff --git a/autoload/vimtex/paths.vim b/autoload/vimtex/paths.vim new file mode 100644 index 00000000..0d308ed8 --- /dev/null +++ b/autoload/vimtex/paths.vim @@ -0,0 +1,91 @@ +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#paths#pushd(path) abort " {{{1 + if empty(a:path) || getcwd() ==# fnamemodify(a:path, ':p') + let s:qpath += [''] + else + let s:qpath += [getcwd()] + execute s:cd fnameescape(a:path) + endif +endfunction + +" }}}1 +function! vimtex#paths#popd() abort " {{{1 + let l:path = remove(s:qpath, -1) + if !empty(l:path) + execute s:cd fnameescape(l:path) + endif +endfunction + +" }}}1 + +function! vimtex#paths#is_abs(path) abort " {{{1 + return a:path =~# s:re_abs +endfunction + +" }}}1 + +function! vimtex#paths#shorten_relative(path) abort " {{{1 + " Input: An absolute path + " Output: Relative path with respect to the vimtex root, path relative to + " vimtex root (unless absolute path is shorter) + + let l:relative = vimtex#paths#relative(a:path, b:vimtex.root) + return strlen(l:relative) < strlen(a:path) + \ ? l:relative : a:path +endfunction + +" }}}1 +function! vimtex#paths#relative(path, current) abort " {{{1 + " Note: This algorithm is based on the one presented by @Offirmo at SO, + " http://stackoverflow.com/a/12498485/51634 + + let l:target = simplify(substitute(a:path, '\\', '/', 'g')) + let l:common = simplify(substitute(a:current, '\\', '/', 'g')) + + " This only works on absolute paths + if !vimtex#paths#is_abs(l:target) + return substitute(a:path, '^\.\/', '', '') + endif + + let l:tries = 50 + let l:result = '' + while stridx(l:target, l:common) != 0 && l:tries > 0 + let l:common = fnamemodify(l:common, ':h') + let l:result = empty(l:result) ? '..' : '../' . l:result + let l:tries -= 1 + endwhile + + if l:tries == 0 | return a:path | endif + + if l:common ==# '/' + let l:result .= '/' + endif + + let l:forward = strpart(l:target, strlen(l:common)) + if !empty(l:forward) + let l:result = empty(l:result) + \ ? l:forward[1:] + \ : l:result . l:forward + endif + + return l:result +endfunction + +" }}}1 + + +let s:cd = exists('*haslocaldir') && haslocaldir() + \ ? 'lcd' + \ : exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd' +let s:qpath = get(s:, 'qpath', []) + +let s:re_abs = has('win32') ? '^[A-Z]:[\\/]' : '^/' + +endif diff --git a/autoload/vimtex/pos.vim b/autoload/vimtex/pos.vim new file mode 100644 index 00000000..127fcd46 --- /dev/null +++ b/autoload/vimtex/pos.vim @@ -0,0 +1,97 @@ +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#pos#set_cursor(...) abort " {{{1 + call cursor(s:parse_args(a:000)) +endfunction + +" }}}1 +function! vimtex#pos#get_cursor() abort " {{{1 + return exists('*getcurpos') ? getcurpos() : getpos('.') +endfunction + +" }}}1 +function! vimtex#pos#get_cursor_line() abort " {{{1 + let l:pos = vimtex#pos#get_cursor() + return l:pos[1] +endfunction + +" }}}1 + +function! vimtex#pos#val(...) abort " {{{1 + let [l:lnum, l:cnum; l:rest] = s:parse_args(a:000) + + return 100000*l:lnum + min([l:cnum, 90000]) +endfunction + +" }}}1 +function! vimtex#pos#next(...) abort " {{{1 + let [l:lnum, l:cnum; l:rest] = s:parse_args(a:000) + + return l:cnum < strlen(getline(l:lnum)) + \ ? [0, l:lnum, l:cnum+1, 0] + \ : [0, l:lnum+1, 1, 0] +endfunction + +" }}}1 +function! vimtex#pos#prev(...) abort " {{{1 + let [l:lnum, l:cnum; l:rest] = s:parse_args(a:000) + + return l:cnum > 1 + \ ? [0, l:lnum, l:cnum-1, 0] + \ : [0, max([l:lnum-1, 1]), strlen(getline(l:lnum-1)), 0] +endfunction + +" }}}1 +function! vimtex#pos#larger(pos1, pos2) abort " {{{1 + return vimtex#pos#val(a:pos1) > vimtex#pos#val(a:pos2) +endfunction + +" }}}1 +function! vimtex#pos#equal(p1, p2) abort " {{{1 + let l:pos1 = s:parse_args(a:p1) + let l:pos2 = s:parse_args(a:p2) + return l:pos1[:1] == l:pos2[:1] +endfunction + +" }}}1 +function! vimtex#pos#smaller(pos1, pos2) abort " {{{1 + return vimtex#pos#val(a:pos1) < vimtex#pos#val(a:pos2) +endfunction + +" }}}1 + +function! s:parse_args(args) abort " {{{1 + " + " The arguments should be in one of the following forms (when unpacked): + " + " [lnum, cnum] + " [bufnum, lnum, cnum, ...] + " {'lnum' : lnum, 'cnum' : cnum} + " + + if len(a:args) > 1 + return s:parse_args([a:args]) + elseif len(a:args) == 1 + if type(a:args[0]) == type({}) + return [get(a:args[0], 'lnum'), get(a:args[0], 'cnum')] + else + if len(a:args[0]) == 2 + return a:args[0] + else + return a:args[0][1:] + endif + endif + else + return a:args + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/process.vim b/autoload/vimtex/process.vim new file mode 100644 index 00000000..a22709c3 --- /dev/null +++ b/autoload/vimtex/process.vim @@ -0,0 +1,233 @@ +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#process#new(...) abort " {{{1 + let l:opts = a:0 > 0 ? a:1 : {} + return extend(deepcopy(s:process), l:opts) +endfunction + +" }}}1 +function! vimtex#process#run(cmd, ...) abort " {{{1 + let l:opts = a:0 > 0 ? a:1 : {} + let l:opts.cmd = a:cmd + let l:process = vimtex#process#new(l:opts) + + return l:process.run() +endfunction + +" }}}1 +function! vimtex#process#capture(cmd) abort " {{{1 + return vimtex#process#run(a:cmd, {'capture': 1}) +endfunction + +" }}}1 +function! vimtex#process#start(cmd, ...) abort " {{{1 + let l:opts = a:0 > 0 ? a:1 : {} + let l:opts.continuous = 1 + return vimtex#process#run(a:cmd, l:opts) +endfunction + +" }}}1 + +let s:process = { + \ 'cmd' : '', + \ 'pid' : 0, + \ 'background' : 1, + \ 'continuous' : 0, + \ 'output' : '', + \ 'workdir' : '', + \ 'silent' : 1, + \ 'capture' : 0, + \ 'result' : '', + \} + +function! s:process.run() abort dict " {{{1 + if self._do_not_run() | return | endif + + call self._pre_run() + call self._prepare() + call self._execute() + call self._restore() + call self._post_run() + + return self.capture ? self.result : self +endfunction + +" }}}1 +function! s:process.stop() abort dict " {{{1 + if !self.pid | return | endif + + let l:cmd = has('win32') + \ ? 'taskkill /PID ' . self.pid . ' /T /F' + \ : 'kill ' . self.pid + call vimtex#process#run(l:cmd, {'background': 0}) + + let self.pid = 0 +endfunction + +" }}}1 +function! s:process.pprint_items() abort dict " {{{1 + let l:list = [ + \ ['pid', self.pid ? self.pid : '-'], + \ ['cmd', get(self, 'prepared_cmd', self.cmd)], + \] + + return l:list +endfunction + +" }}}1 + +function! s:process._do_not_run() abort dict " {{{1 + if empty(self.cmd) + call vimtex#log#warning('Can''t run empty command') + return 1 + endif + if self.pid + call vimtex#log#warning('Process already running!') + return 1 + endif + + return 0 +endfunction + +" }}}1 +function! s:process._pre_run() abort dict " {{{1 + if self.capture + let self.silent = 0 + let self.background = 0 + elseif empty(self.output) && self.background + let self.output = 'null' + endif + + call vimtex#paths#pushd(self.workdir) +endfunction + +" }}}1 +function! s:process._execute() abort dict " {{{1 + if self.capture + let self.result = split(system(self.prepared_cmd), '\n') + elseif self.silent + silent call system(self.prepared_cmd) + elseif self.background + silent execute '!' . self.prepared_cmd + if !has('gui_running') + redraw! + endif + else + execute '!' . self.prepared_cmd + endif + + " Capture the pid if relevant + if has_key(self, 'set_pid') && self.continuous + call self.set_pid() + endif +endfunction + +" }}}1 +function! s:process._post_run() abort dict " {{{1 + call vimtex#paths#popd() +endfunction + +" }}}1 + +if has('win32') + function! s:process._prepare() abort dict " {{{1 + if &shell !~? 'cmd' + let self.win32_restore_shell = 1 + let self.win32_saved_shell = [ + \ &shell, + \ &shellcmdflag, + \ &shellxquote, + \ &shellxescape, + \ &shellquote, + \ &shellpipe, + \ &shellredir, + \ &shellslash + \] + set shell& shellcmdflag& shellxquote& shellxescape& + set shellquote& shellpipe& shellredir& shellslash& + else + let self.win32_restore_shell = 0 + endif + + let l:cmd = self.cmd + + if self.background + if !empty(self.output) + let l:cmd .= self.output ==# 'null' + \ ? ' >nul' + \ : ' >' . self.output + let l:cmd = 'cmd /s /c "' . l:cmd . '"' + else + let l:cmd = 'cmd /c "' . l:cmd . '"' + endif + let l:cmd = 'start /b ' . cmd + endif + + if self.silent && self.output ==# 'null' + let self.prepared_cmd = '"' . l:cmd . '"' + else + let self.prepared_cmd = l:cmd + endif + endfunction + + " }}}1 + function! s:process._restore() abort dict " {{{1 + if self.win32_restore_shell + let [ &shell, + \ &shellcmdflag, + \ &shellxquote, + \ &shellxescape, + \ &shellquote, + \ &shellpipe, + \ &shellredir, + \ &shellslash] = self.win32_saved_shell + endif + endfunction + + " }}}1 + function! s:process.get_pid() abort dict " {{{1 + let self.pid = 0 + endfunction + + " }}}1 +else + function! s:process._prepare() abort dict " {{{1 + let l:cmd = self.cmd + + if self.background + if !empty(self.output) + let l:cmd .= ' >' + \ . (self.output ==# 'null' + \ ? '/dev/null' + \ : shellescape(self.output)) + \ . ' 2>&1' + endif + let l:cmd .= ' &' + endif + + if !self.silent + let l:cmd = escape(l:cmd, '%#') + endif + + let self.prepared_cmd = l:cmd + endfunction + + " }}}1 + function! s:process._restore() abort dict " {{{1 + endfunction + + " }}}1 + function! s:process.get_pid() abort dict " {{{1 + let self.pid = 0 + endfunction + + " }}}1 +endif + +endif diff --git a/autoload/vimtex/profile.vim b/autoload/vimtex/profile.vim new file mode 100644 index 00000000..bb1db390 --- /dev/null +++ b/autoload/vimtex/profile.vim @@ -0,0 +1,125 @@ +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#profile#start() abort " {{{1 + profile start prof.log + profile func * +endfunction + +" }}}1 +function! vimtex#profile#stop() abort " {{{1 + profile stop + call s:fix_sids() +endfunction + +" }}}1 +" +function! vimtex#profile#open() abort " {{{1 + source ~/.vim/vimrc + silent edit prof.log +endfunction + +" }}}1 +function! vimtex#profile#print() abort " {{{1 + for l:line in readfile('prof.log') + echo l:line + endfor + echo '' + quit! +endfunction + +" }}}1 + +function! vimtex#profile#file(filename) abort " {{{1 + call vimtex#profile#start() + + execute 'silent edit' a:filename + + call vimtex#profile#stop() +endfunction + +" }}}1 +function! vimtex#profile#command(cmd) abort " {{{1 + call vimtex#profile#start() + + execute a:cmd + + call vimtex#profile#stop() +endfunction + +" }}}1 + +function! vimtex#profile#filter(sections) abort " {{{1 + let l:lines = readfile('prof.log') + " call filter(l:lines, 'v:val !~# ''FTtex''') + " call filter(l:lines, 'v:val !~# ''LoadFTPlugin''') + + let l:new = [] + for l:sec in a:sections + call extend(l:new, s:get_section(l:sec, l:lines)) + endfor + + call writefile(l:new, 'prof.log') +endfunction + +" }}}1 + +function! s:fix_sids() abort " {{{1 + let l:lines = readfile('prof.log') + let l:new = [] + for l:line in l:lines + let l:sid = matchstr(l:line, '\v\<SNR\>\zs\d+\ze_') + if !empty(l:sid) + let l:filename = map( + \ vimtex#util#command('scriptnames'), + \ 'split(v:val, "\\v:=\\s+")[1]')[l:sid-1] + if l:filename =~# 'vimtex' + let l:filename = substitute(l:filename, '^.*autoload\/', '', '') + let l:filename = substitute(l:filename, '\.vim$', '#s:', '') + let l:filename = substitute(l:filename, '\/', '#', 'g') + else + let l:filename .= ':' + endif + call add(l:new, substitute(l:line, '\v\<SNR\>\d+_', l:filename, 'g')) + else + call add(l:new, substitute(l:line, '\s\+$', '', '')) + endif + endfor + call writefile(l:new, 'prof.log') +endfunction + +" }}}1 +function! s:get_section(name, lines) abort " {{{1 + let l:active = 0 + let l:section = [] + for l:line in a:lines + if l:active + if l:line =~# '^FUNCTION' && l:line !~# a:name + let l:active = 0 + else + call add(l:section, l:line) + endif + continue + endif + + if l:line =~# a:name + call add(l:section, l:line) + let l:active = 1 + endif + endfor + + if l:active + call add(l:section, ' ') + endif + + return l:section +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/qf.vim b/autoload/vimtex/qf.vim new file mode 100644 index 00000000..6cc530e6 --- /dev/null +++ b/autoload/vimtex/qf.vim @@ -0,0 +1,245 @@ +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#qf#init_buffer() abort " {{{1 + if !g:vimtex_quickfix_enabled | return | endif + + command! -buffer VimtexErrors call vimtex#qf#toggle() + + nnoremap <buffer> <plug>(vimtex-errors) :call vimtex#qf#toggle()<cr> +endfunction + +" }}}1 +function! vimtex#qf#init_state(state) abort " {{{1 + if !g:vimtex_quickfix_enabled | return | endif + + try + let l:qf = vimtex#qf#{g:vimtex_quickfix_method}#new() + call l:qf.init(a:state) + unlet l:qf.init + let a:state.qf = l:qf + catch /vimtex: Requirements not met/ + call vimtex#log#warning( + \ 'Quickfix state not initialized!', + \ 'Please see :help g:vimtex_quickfix_method') + endtry +endfunction + +" }}}1 + +function! vimtex#qf#toggle() abort " {{{1 + if vimtex#qf#is_open() + cclose + else + call vimtex#qf#open(1) + endif +endfunction + +" }}}1 +function! vimtex#qf#open(force) abort " {{{1 + if !exists('b:vimtex.qf.addqflist') | return | endif + + try + call vimtex#qf#setqflist() + catch /Vimtex: No log file found/ + if a:force + call vimtex#log#warning('No log file found') + endif + if g:vimtex_quickfix_mode > 0 + cclose + endif + return + catch + call vimtex#log#error('Something went wrong when parsing log files!') + if g:vimtex_quickfix_mode > 0 + cclose + endif + return + endtry + + if empty(getqflist()) + if a:force + call vimtex#log#info('No errors!') + endif + if g:vimtex_quickfix_mode > 0 + cclose + endif + return + endif + + " + " There are two options that determine when to open the quickfix window. If + " forced, the quickfix window is always opened when there are errors or + " warnings (forced typically imply that the functions is called from the + " normal mode mapping). Else the behaviour is based on the settings. + " + let l:errors_or_warnings = s:qf_has_errors() + \ || g:vimtex_quickfix_open_on_warning + + if a:force || (g:vimtex_quickfix_mode > 0 && l:errors_or_warnings) + call s:window_save() + botright cwindow + if g:vimtex_quickfix_mode == 2 + call s:window_restore() + endif + if g:vimtex_quickfix_autoclose_after_keystrokes > 0 + augroup vimtex_qf_autoclose + autocmd! + autocmd CursorMoved,CursorMovedI * call s:qf_autoclose_check() + augroup END + endif + redraw + endif +endfunction + +" }}}1 +function! vimtex#qf#setqflist(...) abort " {{{1 + if !exists('b:vimtex.qf.addqflist') | return | endif + + if a:0 > 0 + let l:tex = a:1 + let l:log = fnamemodify(l:tex, ':r') . '.log' + let l:blg = fnamemodify(l:tex, ':r') . '.blg' + let l:jump = 0 + else + let l:tex = b:vimtex.tex + let l:log = b:vimtex.log() + let l:blg = b:vimtex.ext('blg') + let l:jump = g:vimtex_quickfix_autojump + endif + + try + " Initialize the quickfix list + " Note: Only create new list if the current list is not a vimtex qf list + if get(getqflist({'title': 1}), 'title') =~# 'Vimtex' + call setqflist([], 'r') + else + call setqflist([]) + endif + + " Parse LaTeX errors + call b:vimtex.qf.addqflist(l:tex, l:log) + + " Parse bibliography errors + if has_key(b:vimtex.packages, 'biblatex') + call vimtex#qf#biblatex#addqflist(l:blg) + else + call vimtex#qf#bibtex#addqflist(l:blg) + endif + + " Ignore entries if desired + if !empty(g:vimtex_quickfix_ignore_filters) + let l:qflist = getqflist() + for l:re in g:vimtex_quickfix_ignore_filters + call filter(l:qflist, 'v:val.text !~# l:re') + endfor + call setqflist(l:qflist, 'r') + endif + + " Set title if supported + try + call setqflist([], 'r', {'title': 'Vimtex errors (' . b:vimtex.qf.name . ')'}) + catch + endtry + + " Jump to first error if wanted + if l:jump + cfirst + endif + catch /Vimtex: No log file found/ + throw 'Vimtex: No log file found' + endtry +endfunction + +" }}}1 +function! vimtex#qf#inquire(file) abort " {{{1 + try + call vimtex#qf#setqflist(a:file) + return s:qf_has_errors() + catch + return 0 + endtry +endfunction + +" }}}1 + +function! vimtex#qf#is_open() abort " {{{1 + redir => l:bufstring + silent! ls! + redir END + + let l:buflist = filter(split(l:bufstring, '\n'), 'v:val =~# ''Quickfix''') + + for l:line in l:buflist + let l:bufnr = str2nr(matchstr(l:line, '^\s*\zs\d\+')) + if bufwinnr(l:bufnr) >= 0 + \ && getbufvar(l:bufnr, '&buftype', '') ==# 'quickfix' + return 1 + endif + endfor + + return 0 +endfunction + +" }}}1 + +function! s:window_save() abort " {{{1 + if exists('*win_gotoid') + let s:previous_window = win_getid() + else + let w:vimtex_remember_window = 1 + endif +endfunction + +" }}}1 +function! s:window_restore() abort " {{{1 + if exists('*win_gotoid') + call win_gotoid(s:previous_window) + else + for l:winnr in range(1, winnr('$')) + if getwinvar(l:winnr, 'vimtex_remember_window') + execute l:winnr . 'wincmd p' + unlet! w:vimtex_remember_window + endif + endfor + endif +endfunction + +" }}}1 + +function! s:qf_has_errors() abort " {{{1 + return len(filter(getqflist(), 'v:val.type ==# ''E''')) > 0 +endfunction + +" }}}1 +" +function! s:qf_autoclose_check() abort " {{{1 + if get(s:, 'keystroke_counter') == 0 + let s:keystroke_counter = g:vimtex_quickfix_autoclose_after_keystrokes + endif + + redir => l:bufstring + silent! ls! + redir END + + if empty(filter(split(l:bufstring, '\n'), 'v:val =~# ''%a- .*Quickfix''')) + let s:keystroke_counter -= 1 + else + let s:keystroke_counter = g:vimtex_quickfix_autoclose_after_keystrokes + 1 + endif + + if s:keystroke_counter == 0 + cclose + autocmd! vimtex_qf_autoclose + augroup! vimtex_qf_autoclose + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/qf/biblatex.vim b/autoload/vimtex/qf/biblatex.vim new file mode 100644 index 00000000..2e5e08a5 --- /dev/null +++ b/autoload/vimtex/qf/biblatex.vim @@ -0,0 +1,250 @@ +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#qf#biblatex#addqflist(blg) abort " {{{1 + if get(g:vimtex_quickfix_blgparser, 'disable') | return | endif + + try + call s:biblatex.addqflist(a:blg) + catch /biblatex Aborted/ + endtry +endfunction + +" }}}1 + +let s:biblatex = { + \ 'file' : '', + \ 'types' : [], + \ 'db_files' : [], + \} +function! s:biblatex.addqflist(blg) abort " {{{1 + let self.file = a:blg + let self.root = fnamemodify(a:blg, ':h') + if empty(self.file) | throw 'biblatex Aborted' | endif + + let self.types = map( + \ filter(items(s:), 'v:val[0] =~# ''^type_'''), + \ 'v:val[1]') + let self.db_files = [] + + let self.errorformat_saved = &l:errorformat + setlocal errorformat=%+E%.%#\>\ ERROR%m + setlocal errorformat+=%+W%.%#\>\ WARN\ -\ Duplicate\ entry%m + setlocal errorformat+=%+W%.%#\>\ WARN\ -\ The\ entry%.%#cannot\ be\ encoded%m + setlocal errorformat+=%-G%.%# + execute 'caddfile' fnameescape(self.file) + let &l:errorformat = self.errorformat_saved + + call self.fix_paths() +endfunction + +" }}}1 +function! s:biblatex.fix_paths() abort " {{{1 + let l:qflist = getqflist() + try + let l:title = getqflist({'title': 1}) + catch /E118/ + let l:title = 'Vimtex errors' + endtry + + for l:qf in l:qflist + for l:type in self.types + if l:type.fix(self, l:qf) | break | endif + endfor + endfor + + call setqflist(l:qflist, 'r') + + " Set title if supported + try + call setqflist([], 'r', l:title) + catch + endtry +endfunction + +" }}}1 +function! s:biblatex.get_db_files() abort " {{{1 + if empty(self.db_files) + let l:preamble = vimtex#parser#preamble(b:vimtex.tex, { + \ 'root' : b:vimtex.root, + \}) + let l:files = map( + \ filter(l:preamble, 'v:val =~# ''\\addbibresource'''), + \ 'matchstr(v:val, ''{\zs.*\ze}'')') + let self.db_files = [] + for l:file in l:files + if filereadable(l:file) + let self.db_files += [l:file] + elseif filereadable(expand(l:file)) + let self.db_files += [expand(l:file)] + else + let l:cand = vimtex#kpsewhich#run(l:file) + if len(l:cand) == 1 + let self.db_files += [l:cand[0]] + endif + endif + endfor + endif + + return self.db_files +endfunction + +" }}}1 +function! s:biblatex.get_filename(name) abort " {{{1 + if !filereadable(a:name) + for l:root in [self.root, b:vimtex.root] + let l:candidate = fnamemodify(simplify(l:root . '/' . a:name), ':.') + if filereadable(l:candidate) + return l:candidate + endif + endfor + endif + + return a:name +endfunction + +" }}}1 +function! s:biblatex.get_key_pos(key) abort " {{{1 + for l:file in self.get_db_files() + let l:lnum = self.get_key_lnum(a:key, l:file) + if l:lnum > 0 + return [l:file, l:lnum] + endif + endfor + + return [] +endfunction + +" }}}1 +function! s:biblatex.get_key_lnum(key, filename) abort " {{{1 + if !filereadable(a:filename) | return 0 | endif + + let l:lines = readfile(a:filename) + let l:lnums = range(len(l:lines)) + let l:annotated_lines = map(l:lnums, '[v:val, l:lines[v:val]]') + let l:matches = filter(l:annotated_lines, 'v:val[1] =~# ''^\s*@\w*{\s*\V' . a:key . '''') + + return len(l:matches) > 0 ? l:matches[-1][0]+1 : 0 +endfunction + +" }}}1 +function! s:biblatex.get_entry_key(filename, lnum) abort " {{{1 + for l:file in self.get_db_files() + if fnamemodify(l:file, ':t') !=# a:filename | continue | endif + + let l:entry = get(filter(readfile(l:file, 0, a:lnum), 'v:val =~# ''^@'''), -1) + if empty(l:entry) | continue | endif + + return matchstr(l:entry, '{\v\zs.{-}\ze(,|$)') + endfor + + return '' +endfunction + +" }}}1 + +" +" Parsers for the various warning types +" + +let s:type_parse_error = {} +function! s:type_parse_error.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# 'ERROR - BibTeX subsystem.*expected end of entry' + let l:matches = matchlist(a:entry.text, '\v(\S*\.bib).*line (\d+)') + let a:entry.filename = a:ctx.get_filename(fnamemodify(l:matches[1], ':t')) + let a:entry.lnum = l:matches[2] + + " Use filename and line number to get entry name + let l:key = a:ctx.get_entry_key(a:entry.filename, a:entry.lnum) + if !empty(l:key) + let a:entry.text = 'biblatex: Error parsing entry with key "' . l:key . '"' + endif + return 1 + endif +endfunction + +" }}}1 + +let s:type_duplicate = {} +function! s:type_duplicate.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# 'WARN - Duplicate entry' + let l:matches = matchlist(a:entry.text, '\v: ''(\S*)'' in file ''(.{-})''') + let l:key = l:matches[1] + let a:entry.filename = a:ctx.get_filename(l:matches[2]) + let a:entry.lnum = a:ctx.get_key_lnum(l:key, a:entry.filename) + let a:entry.text = 'biblatex: Duplicate entry key "' . l:key . '"' + return 1 + endif +endfunction + +" }}}1 + +let s:type_no_driver = {} +function! s:type_no_driver.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# 'No driver for entry type' + let l:key = matchstr(a:entry.text, 'entry type ''\v\zs.{-}\ze''') + let a:entry.text = 'biblatex: Using fallback driver for ''' . l:key . '''' + + let l:pos = a:ctx.get_key_pos(l:key) + if !empty(l:pos) + let a:entry.filename = a:ctx.get_filename(l:pos[0]) + let a:entry.lnum = l:pos[1] + if has_key(a:entry, 'bufnr') + unlet a:entry.bufnr + endif + endif + + return 1 + endif +endfunction + +" }}}1 + +let s:type_not_found = {} +function! s:type_not_found.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# 'The following entry could not be found' + let l:key = split(a:entry.text, ' ')[-1] + let a:entry.text = 'biblatex: Entry with key ''' . l:key . ''' not found' + + for [l:file, l:lnum, l:line] in vimtex#parser#tex(b:vimtex.tex) + if l:line =~# g:vimtex#re#not_comment . '\\\S*\V' . l:key + let a:entry.lnum = l:lnum + let a:entry.filename = l:file + unlet a:entry.bufnr + break + endif + endfor + + return 1 + endif +endfunction + +" }}}1 + +let s:type_encoding = {} +function! s:type_encoding.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# 'The entry .* has characters which cannot' + let l:key = matchstr(a:entry.text, 'The entry ''\v\zs.{-}\ze''') + let a:entry.text = 'biblatex: Entry with key ''' . l:key . ''' has non-ascii characters' + + let l:pos = a:ctx.get_key_pos(l:key) + if !empty(l:pos) + let a:entry.filename = a:ctx.get_filename(l:pos[0]) + let a:entry.lnum = l:pos[1] + if has_key(a:entry, 'bufnr') + unlet a:entry.bufnr + endif + endif + + return 1 + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/qf/bibtex.vim b/autoload/vimtex/qf/bibtex.vim new file mode 100644 index 00000000..94bdcafa --- /dev/null +++ b/autoload/vimtex/qf/bibtex.vim @@ -0,0 +1,187 @@ +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#qf#bibtex#addqflist(blg) abort " {{{1 + if get(g:vimtex_quickfix_blgparser, 'disable') | return | endif + + try + call s:bibtex.addqflist(a:blg) + catch /BibTeX Aborted/ + endtry +endfunction + +" }}}1 + +let s:bibtex = { + \ 'file' : '', + \ 'types' : [], + \ 'db_files' : [], + \} +function! s:bibtex.addqflist(blg) abort " {{{1 + let self.file = a:blg + if empty(self.file) || !filereadable(self.file) | throw 'BibTeX Aborted' | endif + + let self.types = map( + \ filter(items(s:), 'v:val[0] =~# ''^type_'''), + \ 'v:val[1]') + let self.db_files = [] + + let self.errorformat_saved = &l:errorformat + setlocal errorformat=%+E%.%#---line\ %l\ of\ file\ %f + setlocal errorformat+=%+EI\ found\ %.%#---while\ reading\ file\ %f + setlocal errorformat+=%+WWarning--empty\ %.%#\ in\ %.%m + setlocal errorformat+=%+WWarning--entry\ type\ for%m + setlocal errorformat+=%-C--line\ %l\ of\ file\ %f + setlocal errorformat+=%-G%.%# + execute 'caddfile' fnameescape(self.file) + let &l:errorformat = self.errorformat_saved + + call self.fix_paths() +endfunction + +" }}}1 +function! s:bibtex.fix_paths() abort " {{{1 + let l:qflist = getqflist() + try + let l:title = getqflist({'title': 1}) + catch /E118/ + let l:title = 'Vimtex errors' + endtry + + for l:qf in l:qflist + for l:type in self.types + if l:type.fix(self, l:qf) | break | endif + endfor + endfor + + call setqflist(l:qflist, 'r') + + " Set title if supported + try + call setqflist([], 'r', l:title) + catch + endtry +endfunction + +" }}}1 +function! s:bibtex.get_db_files() abort " {{{1 + if empty(self.db_files) + let l:build_dir = fnamemodify(b:vimtex.ext('log'), ':.:h') . '/' + for l:file in map( + \ filter(readfile(self.file), 'v:val =~# ''Database file #\d:'''), + \ 'matchstr(v:val, '': \zs.*'')') + if filereadable(l:file) + call add(self.db_files, l:file) + elseif filereadable(l:build_dir . l:file) + call add(self.db_files, l:build_dir . l:file) + endif + endfor + endif + + return self.db_files +endfunction + +" }}}1 +function! s:bibtex.get_key_loc(key) abort " {{{1 + for l:file in self.get_db_files() + let l:lines = readfile(l:file) + let l:lnum = 0 + for l:line in l:lines + let l:lnum += 1 + if l:line =~# '^\s*@\w*{\s*\V' . a:key + return [l:file, l:lnum] + endif + endfor + endfor + + return [] +endfunction + +" }}}1 + +" +" Parsers for the various warning types +" + +let s:type_syn_error = {} +function! s:type_syn_error.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# '---line \d\+ of file' + let a:entry.text = split(a:entry.text, '---')[0] + return 1 + endif +endfunction + +" }}}1 + +let s:type_empty = { + \ 're' : '\vWarning--empty (.*) in (\S*)', + \} +function! s:type_empty.fix(ctx, entry) abort " {{{1 + let l:matches = matchlist(a:entry.text, self.re) + if empty(l:matches) | return 0 | endif + + let l:type = l:matches[1] + let l:key = l:matches[2] + + unlet a:entry.bufnr + let a:entry.text = printf('Missing "%s" in "%s"', l:type, l:key) + + let l:loc = a:ctx.get_key_loc(l:key) + if !empty(l:loc) + let a:entry.filename = l:loc[0] + let a:entry.lnum = l:loc[1] + endif + + return 1 +endfunction + +" }}}1 + +let s:type_style_file_defined = { + \ 're' : '\vWarning--entry type for "(\w+)"', + \} +function! s:type_style_file_defined.fix(ctx, entry) abort " {{{1 + let l:matches = matchlist(a:entry.text, self.re) + if empty(l:matches) | return 0 | endif + + let l:key = l:matches[1] + + unlet a:entry.bufnr + let a:entry.text = 'Entry type for "' . l:key . '" isn''t style-file defined' + + let l:loc = a:ctx.get_key_loc(l:key) + if !empty(l:loc) + let a:entry.filename = l:loc[0] + let a:entry.lnum = l:loc[1] + endif + + return 1 +endfunction + +" }}}1 + +let s:type_no_bibstyle = {} +function! s:type_no_bibstyle.fix(ctx, entry) abort " {{{1 + if a:entry.text =~# 'I found no \\bibstyle' + let a:entry.text = 'BibTeX found no \bibstyle command (missing \bibliographystyle?)' + let a:entry.filename = b:vimtex.tex + unlet a:entry.bufnr + for [l:file, l:lnum, l:line] in vimtex#parser#tex(b:vimtex.tex) + if l:line =~# g:vimtex#re#not_comment . '\\bibliography' + let a:entry.lnum = l:lnum + let a:entry.filename = l:file + break + endif + endfor + return 1 + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/qf/latexlog.vim b/autoload/vimtex/qf/latexlog.vim new file mode 100644 index 00000000..0046543e --- /dev/null +++ b/autoload/vimtex/qf/latexlog.vim @@ -0,0 +1,209 @@ +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#qf#latexlog#new() abort " {{{1 + return deepcopy(s:qf) +endfunction + +" }}}1 + + +let s:qf = { + \ 'name' : 'LaTeX logfile', + \} + +function! s:qf.init(state) abort dict "{{{1 + let self.config = get(g:, 'vimtex_quickfix_latexlog', {}) + let self.config.default = get(self.config, 'default', 1) + let self.config.packages = get(self.config, 'packages', {}) + let self.config.packages.default = get(self.config.packages, 'default', + \ self.config.default) + + let self.types = map( + \ filter(items(s:), 'v:val[0] =~# ''^type_'''), + \ 'v:val[1]') +endfunction + +" }}}1 +function! s:qf.set_errorformat() abort dict "{{{1 + " + " Note: The errorformat assumes we're using the -file-line-error with + " [pdf]latex. For more info, see |errorformat-LaTeX|. + " + + " Push file to file stack + setlocal errorformat=%-P**%f + setlocal errorformat+=%-P**\"%f\" + + " Match errors + setlocal errorformat+=%E!\ LaTeX\ %trror:\ %m + setlocal errorformat+=%E%f:%l:\ %m + setlocal errorformat+=%E!\ %m + + " More info for undefined control sequences + setlocal errorformat+=%Z<argument>\ %m + + " More info for some errors + setlocal errorformat+=%Cl.%l\ %m + + " + " Define general warnings + " + let l:default = self.config.default + if get(self.config, 'font', l:default) + setlocal errorformat+=%+WLaTeX\ Font\ Warning:\ %.%#line\ %l%.%# + setlocal errorformat+=%-CLaTeX\ Font\ Warning:\ %m + setlocal errorformat+=%-C(Font)%m + else + setlocal errorformat+=%-WLaTeX\ Font\ Warning:\ %m + endif + + if !get(self.config, 'references', l:default) + setlocal errorformat+=%-WLaTeX\ %.%#Warning:\ %.%#eference%.%#undefined%.%#line\ %l%.%# + setlocal errorformat+=%-WLaTeX\ %.%#Warning:\ %.%#undefined\ references. + endif + + if get(self.config, 'general', l:default) + setlocal errorformat+=%+WLaTeX\ %.%#Warning:\ %.%#line\ %l%.%# + setlocal errorformat+=%+WLaTeX\ %.%#Warning:\ %m + endif + + if get(self.config, 'overfull', l:default) + setlocal errorformat+=%+WOverfull\ %\\%\\hbox%.%#\ at\ lines\ %l--%*\\d + setlocal errorformat+=%+WOverfull\ %\\%\\hbox%.%#\ at\ line\ %l + setlocal errorformat+=%+WOverfull\ %\\%\\vbox%.%#\ at\ line\ %l + endif + + if get(self.config, 'underfull', l:default) + setlocal errorformat+=%+WUnderfull\ %\\%\\hbox%.%#\ at\ lines\ %l--%*\\d + setlocal errorformat+=%+WUnderfull\ %\\%\\vbox%.%#\ at\ line\ %l + endif + + " + " Define package related warnings + " + let l:default = self.config.packages.default + if get(self.config.packages, 'natbib', l:default) + setlocal errorformat+=%+WPackage\ natbib\ Warning:\ %m\ on\ input\ line\ %l. + else + setlocal errorformat+=%-WPackage\ natbib\ Warning:\ %m\ on\ input\ line\ %l. + endif + + if get(self.config.packages, 'biblatex', l:default) + setlocal errorformat+=%+WPackage\ biblatex\ Warning:\ %m + setlocal errorformat+=%-C(biblatex)%.%#in\ t%.%# + setlocal errorformat+=%-C(biblatex)%.%#Please\ v%.%# + setlocal errorformat+=%-C(biblatex)%.%#LaTeX\ a%.%# + setlocal errorformat+=%-C(biblatex)%m + else + setlocal errorformat+=%-WPackage\ biblatex\ Warning:\ %m + endif + + if get(self.config.packages, 'babel', l:default) + setlocal errorformat+=%+WPackage\ babel\ Warning:\ %m + setlocal errorformat+=%-Z(babel)%.%#input\ line\ %l. + setlocal errorformat+=%-C(babel)%m + else + setlocal errorformat+=%-WPackage\ babel\ Warning:\ %m + endif + + if get(self.config.packages, 'hyperref', l:default) + setlocal errorformat+=%+WPackage\ hyperref\ Warning:\ %m + setlocal errorformat+=%-C(hyperref)%m\ on\ input\ line\ %l. + setlocal errorformat+=%-C(hyperref)%m + else + setlocal errorformat+=%-WPackage\ hyperref\ Warning:\ %m + endif + + if get(self.config.packages, 'scrreprt', l:default) + setlocal errorformat+=%+WPackage\ scrreprt\ Warning:\ %m + setlocal errorformat+=%-C(scrreprt)%m + else + setlocal errorformat+=%-WPackage\ scrreprt\ Warning:\ %m + endif + + if get(self.config.packages, 'fixltx2e', l:default) + setlocal errorformat+=%+WPackage\ fixltx2e\ Warning:\ %m + setlocal errorformat+=%-C(fixltx2e)%m + else + setlocal errorformat+=%-WPackage\ fixltx2e\ Warning:\ %m + endif + + if get(self.config.packages, 'titlesec', l:default) + setlocal errorformat+=%+WPackage\ titlesec\ Warning:\ %m + setlocal errorformat+=%-C(titlesec)%m + else + setlocal errorformat+=%-WPackage\ titlesec\ Warning:\ %m + endif + + if get(self.config.packages, 'general', l:default) + setlocal errorformat+=%+WPackage\ %.%#\ Warning:\ %m\ on\ input\ line\ %l. + setlocal errorformat+=%+WPackage\ %.%#\ Warning:\ %m + setlocal errorformat+=%-Z(%.%#)\ %m\ on\ input\ line\ %l. + setlocal errorformat+=%-C(%.%#)\ %m + endif + + " Ignore unmatched lines + setlocal errorformat+=%-G%.%# +endfunction + +" }}}1 +function! s:qf.addqflist(tex, log) abort dict "{{{1 + if empty(a:log) || !filereadable(a:log) + throw 'Vimtex: No log file found' + endif + + let self.errorformat_saved = &l:errorformat + call self.set_errorformat() + execute 'caddfile' fnameescape(a:log) + let &l:errorformat = self.errorformat_saved + + " Apply some post processing of the quickfix list + let self.main = a:tex + let self.root = b:vimtex.root + call self.fix_paths() +endfunction + +" }}}1 +function! s:qf.pprint_items() abort dict " {{{1 + return [[ 'config', self.config ]] +endfunction + +" }}}1 +function! s:qf.fix_paths() abort dict " {{{1 + let l:qflist = getqflist() + + for l:qf in l:qflist + " For errors and warnings that don't supply a file, the basename of the + " main file is used. However, if the working directory is not the root of + " the LaTeX project, than this results in bufnr = 0. + if l:qf.bufnr == 0 + let l:qf.bufnr = bufnr(self.main) + continue + endif + + " The buffer names of all file:line type errors are relative to the root of + " the main LaTeX file. + let l:file = fnamemodify( + \ simplify(self.root . '/' . bufname(l:qf.bufnr)), ':.') + if !filereadable(l:file) | continue | endif + + if !bufexists(l:file) + execute 'badd' l:file + endif + + let l:qf.filename = l:file + let l:qf.bufnr = bufnr(l:file) + endfor + + call setqflist(l:qflist, 'r') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/qf/pplatex.vim b/autoload/vimtex/qf/pplatex.vim new file mode 100644 index 00000000..39e9a03e --- /dev/null +++ b/autoload/vimtex/qf/pplatex.vim @@ -0,0 +1,98 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" CreatedBy: Johannes Wienke (languitar@semipol.de) +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#qf#pplatex#new() abort " {{{1 + return deepcopy(s:qf) +endfunction + +" }}}1 + + +let s:qf = { + \ 'name' : 'LaTeX logfile using pplatex', + \} + +function! s:qf.init(state) abort dict "{{{1 + if !executable('pplatex') + call vimtex#log#error('pplatex is not executable!') + throw 'vimtex: Requirements not met' + endif + + " Automatically remove the -file-line-error option if we use the latexmk + " backend (for convenience) + if a:state.compiler.name ==# 'latexmk' + let l:index = index(a:state.compiler.options, '-file-line-error') + if l:index >= 0 + call remove(a:state.compiler.options, l:index) + endif + endif +endfunction + +function! s:qf.set_errorformat() abort dict "{{{1 + " Each new item starts with two asterics followed by the file, potentially + " a line number and sometimes even the message itself is on the same line. + " Please note that the trailing whitspaces in the error formats are + " intentional as pplatex produces these. + + " Start of new items with file and line number, message on next line(s). + setlocal errorformat=%E**\ Error\ \ \ in\ %f\\,\ Line\ %l:%m + setlocal errorformat+=%W**\ Warning\ in\ %f\\,\ Line\ %l:%m + setlocal errorformat+=%I**\ BadBox\ \ in\ %f\\,\ Line\ %l:%m + + " Start of items with with file, line and message on the same line. There are + " no BadBoxes reported this way. + setlocal errorformat+=%E**\ Error\ \ \ in\ %f\\,\ Line\ %l:%m + setlocal errorformat+=%W**\ Warning\ in\ %f\\,\ Line\ %l:%m + + " Start of new items with only a file. + setlocal errorformat+=%E**\ Error\ \ \ in\ %f:%m + setlocal errorformat+=%W**\ Warning\ in\ %f:%m + setlocal errorformat+=%I**\ BadBox\ \ in\ %f:%m + + " Start of items with with file and message on the same line. There are + " no BadBoxes reported this way. + setlocal errorformat+=%E**\ Error\ in\ %f:%m + setlocal errorformat+=%W**\ Warning\ in\ %f:%m + + " Some errors are difficult even for pplatex + setlocal errorformat+=%E**\ Error\ \ :%m + + " Anything that starts with three spaces is part of the message from a + " previously started multiline error item. + setlocal errorformat+=%C\ \ \ %m\ on\ input\ line\ %l. + setlocal errorformat+=%C\ \ \ %m + + " Items are terminated with two newlines. + setlocal errorformat+=%-Z + + " Skip statistical results at the bottom of the output. + setlocal errorformat+=%-GResult%.%# + setlocal errorformat+=%-G +endfunction + +" }}}1 +function! s:qf.addqflist(tex, log) abort dict " {{{1 + if empty(a:log) || !filereadable(a:log) + throw 'Vimtex: No log file found' + endif + + let l:tmp = fnameescape(fnamemodify(a:log, ':r') . '.pplatex') + let l:log = fnameescape(a:log) + + silent call system(printf('pplatex -i %s >%s', l:log, l:tmp)) + let self.errorformat_saved = &l:errorformat + call self.set_errorformat() + execute 'caddfile' l:tmp + let &l:errorformat = self.errorformat_saved + silent call system('rm ' . l:tmp) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/qf/pulp.vim b/autoload/vimtex/qf/pulp.vim new file mode 100644 index 00000000..0f0b73ef --- /dev/null +++ b/autoload/vimtex/qf/pulp.vim @@ -0,0 +1,67 @@ +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#qf#pulp#new() abort " {{{1 + return deepcopy(s:qf) +endfunction + +" }}}1 + + +let s:qf = { + \ 'name' : 'LaTeX logfile using pulp', + \} + +function! s:qf.init(state) abort dict "{{{1 + if !executable('pulp') + call vimtex#log#error('pulp is not executable!') + throw 'vimtex: Requirements not met' + endif + + " Automatically remove the -file-line-error option if we use the latexmk + " backend (for convenience) + if a:state.compiler.name ==# 'latexmk' + let l:index = index(a:state.compiler.options, '-file-line-error') + if l:index >= 0 + call remove(a:state.compiler.options, l:index) + endif + endif +endfunction + +function! s:qf.set_errorformat() abort dict "{{{1 + setlocal errorformat= + setlocal errorformat+=%-G%*[^\ ])\ %.%# + setlocal errorformat+=%-G%.%#For\ some\ reason%.%# + setlocal errorformat+=%W%f:%l-%*[0-9?]:\ %*[^\ ]\ warning:\ %m + setlocal errorformat+=%E%f:%l-%*[0-9?]:\ %*[^\ ]\ error:\ %m + setlocal errorformat+=%W%f:%l-%*[0-9?]:\ %m + setlocal errorformat+=%W%l-%*[0-9?]:\ %m + setlocal errorformat+=%-G%.%# +endfunction + +" }}}1 +function! s:qf.addqflist(tex, log) abort dict " {{{1 + if empty(a:log) || !filereadable(a:log) + call setqflist([]) + throw 'Vimtex: No log file found' + endif + + let l:tmp = fnameescape(fnamemodify(a:log, ':r') . '.pulp') + let l:log = fnameescape(a:log) + + silent call system(printf('pulp %s >%s', l:log, l:tmp)) + let self.errorformat_saved = &l:errorformat + call self.set_errorformat() + execute 'caddfile' l:tmp + let &l:errorformat = self.errorformat_saved + silent call system('rm ' . l:tmp) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/re.vim b/autoload/vimtex/re.vim new file mode 100644 index 00000000..0b3aab58 --- /dev/null +++ b/autoload/vimtex/re.vim @@ -0,0 +1,110 @@ +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 +" + +let g:vimtex#re#not_bslash = '\v%(\\@<!%(\\\\)*)@<=' +let g:vimtex#re#not_comment = '\v%(' . g:vimtex#re#not_bslash . '\%.*)@<!' + +let g:vimtex#re#tex_input_root = + \ '\v^\s*\%\s*!?\s*[tT][eE][xX]\s+[rR][oO][oO][tT]\s*\=\s*\zs.*\ze\s*$' +let g:vimtex#re#tex_input_latex = '\v\\%(' + \ . join(get(g:, 'vimtex_include_indicators', + \ ['input', 'include', 'subfile', 'subfileinclude']), + \ '|') . ')\s*\{' +let g:vimtex#re#tex_input_import = + \ '\v\\%(sub)?%(import|%(input|include)from)\*?\{[^\}]*\}\{' +let g:vimtex#re#tex_input_package = + \ '\v\\%(usepackage|RequirePackage)%(\s*\[[^]]*\])?\s*\{\zs[^}]*\ze\}' + +let g:vimtex#re#tex_input = '\v^\s*%(' . join([ + \ g:vimtex#re#tex_input_latex, + \ g:vimtex#re#tex_input_import, + \ ], '|') . ')' + +let g:vimtex#re#bib_input = '\v\\%(addbibresource|bibliography)>' + +let g:vimtex#re#tex_include = g:vimtex#re#tex_input_root + \ . '|' . g:vimtex#re#tex_input . '\zs[^\}]*\ze\}?' + \ . '|' . g:vimtex#re#tex_input_package + +" {{{1 Completion regexes +let g:vimtex#re#neocomplete = + \ '\v\\%(' + \ . '\a*cite\a*%(\s*\[[^]]*\]){0,2}\s*\{[^}]*' + \ . '|%(text|block)cquote\*?%(\s*\[[^]]*\]){0,2}\s*\{[^}]*' + \ . '|%(for|hy)\w*cquote\*?\{[^}]*}%(\s*\[[^]]*\]){0,2}\s*\{[^}]*' + \ . '|\a*ref%(\s*\{[^}]*|range\s*\{[^,}]*%(}\{)?)' + \ . '|hyperref\s*\[[^]]*' + \ . '|includegraphics\*?%(\s*\[[^]]*\]){0,2}\s*\{[^}]*' + \ . '|%(include%(only)?|input|subfile)\s*\{[^}]*' + \ . '|([cpdr]?(gls|Gls|GLS)|acr|Acr|ACR)\a*\s*\{[^}]*' + \ . '|(ac|Ac|AC)\s*\{[^}]*' + \ . '|includepdf%(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|includestandalone%(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|%(usepackage|RequirePackage|PassOptionsToPackage)%(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|documentclass%(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|begin%(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|end%(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|\a*' + \ . ')' + +let g:vimtex#re#deoplete = '\\(?:' + \ . '\w*cite\w*(?:\s*\[[^]]*\]){0,2}\s*{[^}]*' + \ . '|(text|block)cquote\*?(?:\s*\[[^]]*\]){0,2}\s*{[^}]*' + \ . '|(for|hy)\w*cquote\*?{[^}]*}(?:\s*\[[^]]*\]){0,2}\s*{[^}]*' + \ . '|\w*ref(?:\s*\{[^}]*|range\s*\{[^,}]*(?:}{)?)' + \ . '|hyperref\s*\[[^]]*' + \ . '|includegraphics\*?(?:\s*\[[^]]*\]){0,2}\s*\{[^}]*' + \ . '|(?:include(?:only)?|input|subfile)\s*\{[^}]*' + \ . '|([cpdr]?(gls|Gls|GLS)|acr|Acr|ACR)[a-zA-Z]*\s*\{[^}]*' + \ . '|(ac|Ac|AC)\s*\{[^}]*' + \ . '|includepdf(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|includestandalone(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|(usepackage|RequirePackage|PassOptionsToPackage)(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|documentclass(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|begin(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|end(\s*\[[^]]*\])?\s*\{[^}]*' + \ . '|\w*' + \ .')' + +let g:vimtex#re#ncm2#cmds = [ + \ '\\[A-Za-z]+', + \ '\\(usepackage|RequirePackage|PassOptionsToPackage)(\s*\[[^]]*\])?\s*\{[^}]*', + \ '\\documentclass(\s*\[[^]]*\])?\s*\{[^}]*', + \ '\\begin(\s*\[[^]]*\])?\s*\{[^}]*', + \ '\\end(\s*\[[^]]*\])?\s*\{[^}]*', + \] +let g:vimtex#re#ncm2#bibtex = [ + \ '\\[A-Za-z]*cite[A-Za-z]*(\[[^]]*\]){0,2}{[^}]*', + \ '\\(text|block)cquote\*?(\[[^]]*\]){0,2}{[^}]*', + \ '\\(for|hy)[A-Za-z]*cquote\*?{[^}]*}(\[[^]]*\]){0,2}{[^}]*', + \] +let g:vimtex#re#ncm2#labels = [ + \ '\\[A-Za-z]*ref({[^}]*|range{([^,{}]*(}{)?))', + \ '\\hyperref\[[^]]*', + \ '\\([cpdr]?(gls|Gls|GLS)|acr|Acr|ACR)[a-zA-Z]*\s*\{[^}]*', + \ '\\(ac|Ac|AC)\s*\{[^}]*', + \] +let g:vimtex#re#ncm2#files = [ + \ '\\includegraphics\*?(\[[^]]*\]){0,2}{[^}]*', + \ '\\(include(only)?|input|subfile){[^}]*', + \ '\\includepdf(\s*\[[^]]*\])?\s*\{[^}]*', + \ '\\includestandalone(\s*\[[^]]*\])?\s*\{[^}]*', + \] + +let g:vimtex#re#ncm2 = g:vimtex#re#ncm2#cmds + + \ g:vimtex#re#ncm2#bibtex + + \ g:vimtex#re#ncm2#labels + + \ g:vimtex#re#ncm2#files + +let g:vimtex#re#ncm = copy(g:vimtex#re#ncm2) + +let g:vimtex#re#youcompleteme = map(copy(g:vimtex#re#ncm), "'re!' . v:val") + +" }}}1 + +endif diff --git a/autoload/vimtex/scratch.vim b/autoload/vimtex/scratch.vim new file mode 100644 index 00000000..299cc0ee --- /dev/null +++ b/autoload/vimtex/scratch.vim @@ -0,0 +1,72 @@ +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#scratch#new(opts) abort " {{{1 + let l:buf = extend(deepcopy(s:scratch), a:opts) + call l:buf.open() +endfunction + +" }}}1 + + +let s:scratch = { + \ 'name' : 'VimtexScratch' + \} +function! s:scratch.open() abort dict " {{{1 + let l:bufnr = bufnr('') + let l:vimtex = get(b:, 'vimtex', {}) + + silent execute 'keepalt edit' escape(self.name, ' ') + + let self.prev_bufnr = l:bufnr + let b:scratch = self + let b:vimtex = l:vimtex + + setlocal bufhidden=wipe + setlocal buftype=nofile + setlocal concealcursor=nvic + setlocal conceallevel=0 + setlocal nobuflisted + setlocal nolist + setlocal nospell + setlocal noswapfile + setlocal nowrap + setlocal tabstop=8 + + nnoremap <silent><nowait><buffer> q :call b:scratch.close()<cr> + nnoremap <silent><nowait><buffer> <esc> :call b:scratch.close()<cr> + nnoremap <silent><nowait><buffer> <c-6> :call b:scratch.close()<cr> + nnoremap <silent><nowait><buffer> <c-^> :call b:scratch.close()<cr> + nnoremap <silent><nowait><buffer> <c-e> :call b:scratch.close()<cr> + + if has_key(self, 'syntax') + call self.syntax() + endif + + call self.fill() +endfunction + +" }}}1 +function! s:scratch.close() abort dict " {{{1 + silent execute 'keepalt buffer' self.prev_bufnr +endfunction + +" }}}1 +function! s:scratch.fill() abort dict " {{{1 + setlocal modifiable + %delete + + call self.print_content() + + 0delete _ + setlocal nomodifiable +endfunction + +" }}}1 + +endif 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 diff --git a/autoload/vimtex/syntax.vim b/autoload/vimtex/syntax.vim new file mode 100644 index 00000000..428ccaaa --- /dev/null +++ b/autoload/vimtex/syntax.vim @@ -0,0 +1,66 @@ +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#syntax#init() abort " {{{1 + if !get(g:, 'vimtex_syntax_enabled', 1) | return | endif + + " The following ensures that syntax addons are not loaded until after the + " filetype plugin has been sourced. See e.g. #1428 for more info. + if exists('b:vimtex') + call vimtex#syntax#load() + else + augroup vimtex_syntax + autocmd! + autocmd User VimtexEventInitPost call vimtex#syntax#load() + augroup END + endif +endfunction + +" }}}1 +function! vimtex#syntax#load() abort " {{{1 + if s:is_loaded() | return | endif + + " Initialize project cache (used e.g. for the minted package) + if !has_key(b:vimtex, 'syntax') + let b:vimtex.syntax = {} + endif + + " Initialize b:vimtex_syntax + let b:vimtex_syntax = {} + + " Reset included syntaxes (necessary e.g. when doing :e) + call vimtex#syntax#misc#include_reset() + + " Set some better defaults + syntax spell toplevel + syntax sync maxlines=500 + + " Load some general syntax improvements + call vimtex#syntax#load#general() + + " Load syntax for documentclass and packages + call vimtex#syntax#load#packages() + + " Hack to make it possible to determine if vimtex syntax was loaded + syntax match texVimtexLoaded 'dummyVimtexLoadedText' contained +endfunction + +" }}}1 + +function! s:is_loaded() abort " {{{1 + if exists('*execute') + let l:result = split(execute('syntax'), "\n") + return !empty(filter(l:result, 'v:val =~# "texVimtexLoaded"')) + else + return 0 + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/load.vim b/autoload/vimtex/syntax/load.vim new file mode 100644 index 00000000..d1b8b09b --- /dev/null +++ b/autoload/vimtex/syntax/load.vim @@ -0,0 +1,82 @@ +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#syntax#load#general() abort " {{{1 + " I don't see why we can't match Math zones in the MatchNMGroup + if !exists('g:tex_no_math') + syntax cluster texMatchNMGroup add=@texMathZones + endif + + " Todo elements + syntax match texStatement '\\todo\w*' contains=texTodo + syntax match texTodo '\\todo\w*' + + " Fix strange mistake in main syntax file where \usepackage is added to the + " texInputFile group + syntax match texDocType /\\usepackage\>/ + \ nextgroup=texBeginEndName,texDocTypeArgs + + " Improve support for italic font, bold font and some conceals + if get(g:, 'tex_fast', 'b') =~# 'b' + let s:conceal = (has('conceal') && get(g:, 'tex_conceal', 'b') =~# 'b') + \ ? 'concealends' : '' + + for [s:style, s:group, s:commands] in [ + \ ['texItalStyle', 'texItalGroup', ['emph', 'textit']], + \ ['texBoldStyle', 'texBoldGroup', ['textbf']], + \] + for s:cmd in s:commands + execute 'syntax region' s:style 'matchgroup=texTypeStyle' + \ 'start="\\' . s:cmd . '\s*{" end="}"' + \ 'contains=@Spell,@' . s:group + \ s:conceal + endfor + execute 'syntax cluster texMatchGroup add=' . s:style + endfor + endif + + " Allow arguments in newenvironments + syntax region texEnvName contained matchgroup=Delimiter + \ start="{"rs=s+1 end="}" + \ nextgroup=texEnvBgn,texEnvArgs contained skipwhite skipnl + syntax region texEnvArgs contained matchgroup=Delimiter + \ start="\["rs=s+1 end="]" + \ nextgroup=texEnvBgn,texEnvArgs skipwhite skipnl + syntax cluster texEnvGroup add=texDefParm,texNewEnv,texComment + + " Add support for \renewenvironment and \renewcommand + syntax match texNewEnv "\\renewenvironment\>" + \ nextgroup=texEnvName skipwhite skipnl + syntax match texNewCmd "\\renewcommand\>" + \ nextgroup=texCmdName skipwhite skipnl + + " Match nested DefParms + syntax match texDefParmNested contained "##\+\d\+" + highlight def link texDefParmNested Identifier + syntax cluster texEnvGroup add=texDefParmNested + syntax cluster texCmdGroup add=texDefParmNested +endfunction + +" }}}1 +function! vimtex#syntax#load#packages() abort " {{{1 + try + call vimtex#syntax#p#{b:vimtex.documentclass}#load() + catch /E117:/ + endtry + + for l:pkg in map(keys(b:vimtex.packages), "substitute(v:val, '-', '_', 'g')") + try + call vimtex#syntax#p#{l:pkg}#load() + catch /E117:/ + endtry + endfor +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/misc.vim b/autoload/vimtex/syntax/misc.vim new file mode 100644 index 00000000..633a1d75 --- /dev/null +++ b/autoload/vimtex/syntax/misc.vim @@ -0,0 +1,92 @@ +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#syntax#misc#add_to_section_clusters(group) abort " {{{1 + for l:cluster in [ + \ 'texPartGroup', + \ 'texChapterGroup', + \ 'texSectionGroup', + \ 'texSubSectionGroup', + \ 'texSubSubSectionGroup', + \ 'texParaGroup', + \] + execute printf('syntax cluster %s add=%s', l:cluster, a:group) + endfor + + execute printf('syntax cluster texVimtexGlobal add=%s', a:group) +endfunction + +" }}}1 +function! vimtex#syntax#misc#include(name) abort " {{{1 + let l:inc_name = 'vimtex_nested_' . a:name + + if !has_key(s:included, l:inc_name) + let s:included[l:inc_name] = s:include(l:inc_name, a:name) + endif + + return s:included[l:inc_name] ? l:inc_name : '' +endfunction + +" }}}1 +function! vimtex#syntax#misc#include_reset() abort " {{{1 + let s:included = {'vimtex_nested_tex': 0} +endfunction + +let s:included = {'vimtex_nested_tex': 0} + +" }}}1 +function! vimtex#syntax#misc#new_math_zone(sfx, mathzone, starred) abort " {{{1 + " This function is based on Charles E. Campbell's amsmath.vba file 2018-06-29 + + if get(g:, 'tex_fast', 'M') !~# 'M' | return | endif + + let foldcmd = get(g:, 'tex_fold_enabled') ? ' fold' : '' + + let grp = 'texMathZone' . a:sfx + execute 'syntax cluster texMathZones add=' . grp + execute 'syntax region ' . grp + \ . ' start=''\\begin\s*{\s*' . a:mathzone . '\s*}''' + \ . ' end=''\\end\s*{\s*' . a:mathzone . '\s*}''' + \ . foldcmd . ' keepend contains=@texMathZoneGroup' + execute 'highlight def link '.grp.' texMath' + + if a:starred + let grp .= 'S' + execute 'syntax cluster texMathZones add=' . grp + execute 'syntax region ' . grp + \ . ' start=''\\begin\s*{\s*' . a:mathzone . '\*\s*}''' + \ . ' end=''\\end\s*{\s*' . a:mathzone . '\*\s*}''' + \ . foldcmd . ' keepend contains=@texMathZoneGroup' + execute 'highlight def link '.grp.' texMath' + endif + + execute 'syntax match texBadMath ''\\end\s*{\s*' . a:mathzone . '\*\=\s*}''' +endfunction + +" }}}1 + +function! s:include(cluster, name) abort " {{{1 + let l:name = get(g:vimtex_syntax_nested.aliases, a:name, a:name) + let l:path = 'syntax/' . l:name . '.vim' + + if empty(globpath(&runtimepath, l:path)) | return 0 | endif + + unlet b:current_syntax + execute 'syntax include @' . a:cluster l:path + let b:current_syntax = 'tex' + + for l:ignored_group in get(g:vimtex_syntax_nested.ignored, l:name, []) + execute 'syntax cluster' a:cluster 'remove=' . l:ignored_group + endfor + + return 1 +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/amsmath.vim b/autoload/vimtex/syntax/p/amsmath.vim new file mode 100644 index 00000000..d202d140 --- /dev/null +++ b/autoload/vimtex/syntax/p/amsmath.vim @@ -0,0 +1,47 @@ +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 +" + +scriptencoding utf-8 + +function! vimtex#syntax#p#amsmath#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'amsmath') | return | endif + let b:vimtex_syntax.amsmath = 1 + + " Allow subequations (fixes #1019) + " - This should be temporary, as it seems subequations is erroneously part of + " texBadMath from Charles Campbell's syntax plugin. + syntax match texBeginEnd + \ "\(\\begin\>\|\\end\>\)\ze{subequations}" + \ nextgroup=texBeginEndName + + call vimtex#syntax#misc#new_math_zone('AmsA', 'align', 1) + call vimtex#syntax#misc#new_math_zone('AmsB', 'alignat', 1) + call vimtex#syntax#misc#new_math_zone('AmsD', 'flalign', 1) + call vimtex#syntax#misc#new_math_zone('AmsC', 'gather', 1) + call vimtex#syntax#misc#new_math_zone('AmsD', 'multline', 1) + call vimtex#syntax#misc#new_math_zone('AmsE', 'xalignat', 1) + call vimtex#syntax#misc#new_math_zone('AmsF', 'xxalignat', 0) + call vimtex#syntax#misc#new_math_zone('AmsG', 'mathpar', 1) + + " Amsmath [lr][vV]ert (Holger Mitschke) + if has('conceal') && &enc ==# 'utf-8' && get(g:, 'tex_conceal', 'd') =~# 'd' + for l:texmath in [ + \ ['\\lvert', '|'] , + \ ['\\rvert', '|'] , + \ ['\\lVert', '‖'] , + \ ['\\rVert', '‖'] , + \ ] + execute "syntax match texMathDelim '\\\\[bB]igg\\=[lr]\\=" + \ . l:texmath[0] . "' contained conceal cchar=" . l:texmath[1] + endfor + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/array.vim b/autoload/vimtex/syntax/p/array.vim new file mode 100644 index 00000000..bc45e79b --- /dev/null +++ b/autoload/vimtex/syntax/p/array.vim @@ -0,0 +1,35 @@ +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#syntax#p#array#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'array') | return | endif + let b:vimtex_syntax.array = 1 + + call vimtex#syntax#p#tabularx#load() + if !get(g:, 'tex_fast', 'M') =~# 'M' | return | endif + + " + " The following code changes inline math so as to support the column + " specifiers [0], e.g. + " + " \begin{tabular}{*{3}{>{$}c<{$}}} + " + " [0]: https://en.wikibooks.org/wiki/LaTeX/Tables#Column_specification_using_.3E.7B.5Ccmd.7D_and_.3C.7B.5Ccmd.7D + " + + syntax clear texMathZoneX + if has('conceal') && &enc ==# 'utf-8' && get(g:, 'tex_conceal', 'd') =~# 'd' + syntax region texMathZoneX matchgroup=Delimiter start="\([<>]{\)\@<!\$" skip="\%(\\\\\)*\\\$" matchgroup=Delimiter end="\$" end="%stopzone\>" concealends contains=@texMathZoneGroup + else + syntax region texMathZoneX matchgroup=Delimiter start="\([<>]{\)\@<!\$" skip="\%(\\\\\)*\\\$" matchgroup=Delimiter end="\$" end="%stopzone\>" contains=@texMathZoneGroup + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/asymptote.vim b/autoload/vimtex/syntax/p/asymptote.vim new file mode 100644 index 00000000..137c3890 --- /dev/null +++ b/autoload/vimtex/syntax/p/asymptote.vim @@ -0,0 +1,34 @@ +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#syntax#p#asymptote#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'asymptote') | return | endif + let b:vimtex_syntax.asymptote = 1 + + call vimtex#syntax#misc#add_to_section_clusters('texZoneAsymptote') + + if !empty(vimtex#syntax#misc#include('asy')) + syntax region texZoneAsymptote + \ start='\\begin{asy\z(def\)\?}'rs=s + \ end='\\end{asy\z1}'re=e + \ keepend + \ transparent + \ contains=texBeginEnd,@vimtex_nested_asy + else + syntax region texZoneAsymptote + \ start='\\begin{asy\z(def\)\?}'rs=s + \ end='\\end{asy\z1}'re=e + \ keepend + \ contains=texBeginEnd + highlight def link texZoneAsymptote texZone + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/beamer.vim b/autoload/vimtex/syntax/p/beamer.vim new file mode 100644 index 00000000..341c229c --- /dev/null +++ b/autoload/vimtex/syntax/p/beamer.vim @@ -0,0 +1,32 @@ +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#syntax#p#beamer#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'beamer') | return | endif + let b:vimtex_syntax.beamer = 1 + + syntax match texBeamerDelimiter '<\|>' contained + syntax match texBeamerOpt '<[^>]*>' contained contains=texBeamerDelimiter + + syntax match texStatementBeamer '\\only\(<[^>]*>\)\?' contains=texBeamerOpt + syntax match texStatementBeamer '\\item<[^>]*>' contains=texBeamerOpt + + syntax match texInputFile + \ '\\includegraphics<[^>]*>\(\[.\{-}\]\)\=\s*{.\{-}}' + \ contains=texStatement,texBeamerOpt,texInputCurlies,texInputFileOpt + + call vimtex#syntax#misc#add_to_section_clusters('texStatementBeamer') + + highlight link texStatementBeamer texStatement + highlight link texBeamerOpt Identifier + highlight link texBeamerDelimiter Delimiter +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/biblatex.vim b/autoload/vimtex/syntax/p/biblatex.vim new file mode 100644 index 00000000..1c620d6c --- /dev/null +++ b/autoload/vimtex/syntax/p/biblatex.vim @@ -0,0 +1,84 @@ +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#syntax#p#biblatex#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'biblatex') | return | endif + let b:vimtex_syntax.biblatex = 1 + + if get(g:, 'tex_fast', 'r') !~# 'r' | return | endif + + for l:pattern in [ + \ 'bibentry', + \ 'cite[pt]?\*?', + \ 'citeal[tp]\*?', + \ 'cite(num|text|url)', + \ '[Cc]ite%(title|author|year(par)?|date)\*?', + \ '[Pp]arencite\*?', + \ 'foot%(full)?cite%(text)?', + \ 'fullcite', + \ '[Tt]extcite', + \ '[Ss]martcite', + \ 'supercite', + \ '[Aa]utocite\*?', + \ '[Ppf]?[Nn]otecite', + \ '%(text|block)cquote\*?', + \] + execute 'syntax match texStatement' + \ '/\v\\' . l:pattern . '\ze\s*%(\[|\{)/' + \ 'nextgroup=texRefOption,texCite' + endfor + + for l:pattern in [ + \ '[Cc]ites', + \ '[Pp]arencites', + \ 'footcite%(s|texts)', + \ '[Tt]extcites', + \ '[Ss]martcites', + \ 'supercites', + \ '[Aa]utocites', + \ '[pPfFsStTaA]?[Vv]olcites?', + \ 'cite%(field|list|name)', + \] + execute 'syntax match texStatement' + \ '/\v\\' . l:pattern . '\ze\s*%(\[|\{)/' + \ 'nextgroup=texRefOptions,texCites' + endfor + + for l:pattern in [ + \ '%(foreign|hyphen)textcquote\*?', + \ '%(foreign|hyphen)blockcquote', + \ 'hybridblockcquote', + \] + execute 'syntax match texStatement' + \ '/\v\\' . l:pattern . '\ze\s*%(\[|\{)/' + \ 'nextgroup=texQuoteLang' + endfor + + syntax region texRefOptions contained matchgroup=Delimiter + \ start='\[' end=']' + \ contains=@texRefGroup,texRefZone + \ nextgroup=texRefOptions,texCites + + syntax region texCites contained matchgroup=Delimiter + \ start='{' end='}' + \ contains=@texRefGroup,texRefZone,texCites + \ nextgroup=texRefOptions,texCites + + syntax region texQuoteLang contained matchgroup=Delimiter + \ start='{' end='}' + \ transparent + \ contains=@texMatchGroup + \ nextgroup=texRefOption,texCite + + highlight def link texRefOptions texRefOption + highlight def link texCites texCite +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/breqn.vim b/autoload/vimtex/syntax/p/breqn.vim new file mode 100644 index 00000000..ab81db2f --- /dev/null +++ b/autoload/vimtex/syntax/p/breqn.vim @@ -0,0 +1,23 @@ +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 +" + +scriptencoding utf-8 + +function! vimtex#syntax#p#breqn#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'breqn') | return | endif + let b:vimtex_syntax.breqn = 1 + + call vimtex#syntax#misc#new_math_zone('BreqnA', 'dmath', 1) + call vimtex#syntax#misc#new_math_zone('BreqnB', 'dseries', 1) + call vimtex#syntax#misc#new_math_zone('BreqnC', 'dgroup', 1) + call vimtex#syntax#misc#new_math_zone('BreqnD', 'darray', 1) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/cases.vim b/autoload/vimtex/syntax/p/cases.vim new file mode 100644 index 00000000..383cd8a7 --- /dev/null +++ b/autoload/vimtex/syntax/p/cases.vim @@ -0,0 +1,20 @@ +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 +" + +scriptencoding utf-8 + +function! vimtex#syntax#p#cases#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'cases') | return | endif + let b:vimtex_syntax.cases = 1 + + call VimtexNewMathZone('E', '\(sub\)\?numcases', 0) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/cleveref.vim b/autoload/vimtex/syntax/p/cleveref.vim new file mode 100644 index 00000000..1066e4ab --- /dev/null +++ b/autoload/vimtex/syntax/p/cleveref.vim @@ -0,0 +1,44 @@ +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#syntax#p#cleveref#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'cleveref') | return | endif + let b:vimtex_syntax.cleveref = 1 + if get(g:, 'tex_fast', 'r') !~# 'r' | return | endif + + syntax match texStatement '\\\(\(label\)\?c\(page\)\?\|C\|auto\)ref\>' + \ nextgroup=texCRefZone + + " \crefrange, \cpagerefrange (these commands expect two arguments) + syntax match texStatement '\\c\(page\)\?refrange\>' + \ nextgroup=texCRefZoneRange skipwhite skipnl + + " \label[xxx]{asd} + syntax match texStatement '\\label\[.\{-}\]' + \ nextgroup=texCRefZone skipwhite skipnl + \ contains=texCRefLabelOpts + + syntax region texCRefZone contained matchgroup=Delimiter + \ start="{" end="}" + \ contains=@texRefGroup,texRefZone + syntax region texCRefZoneRange contained matchgroup=Delimiter + \ start="{" end="}" + \ contains=@texRefGroup,texRefZone + \ nextgroup=texCRefZone skipwhite skipnl + syntax region texCRefLabelOpts contained matchgroup=Delimiter + \ start='\[' end=']' + \ contains=@texRefGroup,texRefZone + + highlight link texCRefZone texRefZone + highlight link texCRefZoneRange texRefZone + highlight link texCRefLabelOpts texCmdArgs +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/csquotes.vim b/autoload/vimtex/syntax/p/csquotes.vim new file mode 100644 index 00000000..1bcfb740 --- /dev/null +++ b/autoload/vimtex/syntax/p/csquotes.vim @@ -0,0 +1,18 @@ +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#syntax#p#csquotes#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'csquotes') | return | endif + let b:vimtex_syntax.csquotes = 1 + + call vimtex#syntax#p#biblatex#load() +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/dot2texi.vim b/autoload/vimtex/syntax/p/dot2texi.vim new file mode 100644 index 00000000..881c2397 --- /dev/null +++ b/autoload/vimtex/syntax/p/dot2texi.vim @@ -0,0 +1,25 @@ +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#syntax#p#dot2texi#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'dot2texi') | return | endif + let b:vimtex_syntax.dot2texi = 1 + + call vimtex#syntax#misc#include('dot') + call vimtex#syntax#misc#add_to_section_clusters('texZoneDot') + syntax region texZoneDot + \ start="\\begin{dot2tex}"rs=s + \ end="\\end{dot2tex}"re=e + \ keepend + \ transparent + \ contains=texBeginEnd,@vimtex_nested_dot +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/glossaries.vim b/autoload/vimtex/syntax/p/glossaries.vim new file mode 100644 index 00000000..259919cb --- /dev/null +++ b/autoload/vimtex/syntax/p/glossaries.vim @@ -0,0 +1,20 @@ +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 +" + +scriptencoding utf-8 + +function! vimtex#syntax#p#glossaries#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'glossaries') | return | endif + let b:vimtex_syntax.glossaries = 1 + + " Currently nothing here +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/glossaries_extra.vim b/autoload/vimtex/syntax/p/glossaries_extra.vim new file mode 100644 index 00000000..4a6d29f2 --- /dev/null +++ b/autoload/vimtex/syntax/p/glossaries_extra.vim @@ -0,0 +1,21 @@ +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 +" + +scriptencoding utf-8 + +function! vimtex#syntax#p#glossaries_extra#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'glossaries_extra') | return | endif + let b:vimtex_syntax.glossaries_extra = 1 + + " Load amsmath + call vimtex#syntax#p#glossaries#load() +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/gnuplottex.vim b/autoload/vimtex/syntax/p/gnuplottex.vim new file mode 100644 index 00000000..7ecaee54 --- /dev/null +++ b/autoload/vimtex/syntax/p/gnuplottex.vim @@ -0,0 +1,25 @@ +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#syntax#p#gnuplottex#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'gnuplottex') | return | endif + let b:vimtex_syntax.gnuplottex = 1 + + call vimtex#syntax#misc#include('gnuplot') + call vimtex#syntax#misc#add_to_section_clusters('texZoneGnuplot') + syntax region texZoneGnuplot + \ start='\\begin{gnuplot}\(\_s*\[\_[\]]\{-}\]\)\?'rs=s + \ end='\\end{gnuplot}'re=e + \ keepend + \ transparent + \ contains=texBeginEnd,texBeginEndModifier,@vimtex_nested_gnuplot +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/hyperref.vim b/autoload/vimtex/syntax/p/hyperref.vim new file mode 100644 index 00000000..2eb1cf19 --- /dev/null +++ b/autoload/vimtex/syntax/p/hyperref.vim @@ -0,0 +1,35 @@ +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#syntax#p#hyperref#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'hyperref') | return | endif + let b:vimtex_syntax.hyperref = 1 + + syntax match texStatement '\\url\ze[^\ta-zA-Z]' nextgroup=texUrlVerb + syntax region texUrlVerb matchgroup=Delimiter + \ start='\z([^\ta-zA-Z]\)' end='\z1' contained + + syntax match texStatement '\\url\ze\s*{' nextgroup=texUrl + syntax region texUrl matchgroup=Delimiter start='{' end='}' contained + + syntax match texStatement '\\href' nextgroup=texHref + syntax region texHref matchgroup=Delimiter start='{' end='}' contained + \ nextgroup=texMatcher + + syntax match texStatement '\\hyperref' nextgroup=texHyperref + syntax region texHyperref matchgroup=Delimiter start='\[' end='\]' contained + + highlight link texUrl Function + highlight link texUrlVerb texUrl + highlight link texHref texUrl + highlight link texHyperref texRefZone +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/listings.vim b/autoload/vimtex/syntax/p/listings.vim new file mode 100644 index 00000000..81c7da24 --- /dev/null +++ b/autoload/vimtex/syntax/p/listings.vim @@ -0,0 +1,75 @@ +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#syntax#p#listings#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'listings') | return | endif + let b:vimtex_syntax.listings = s:get_nested_languages() + + " First some general support + syntax match texInputFile + \ "\\lstinputlisting\s*\(\[.\{-}\]\)\={.\{-}}" + \ contains=texStatement,texInputCurlies,texInputFileOpt + syntax match texZone "\\lstinline\s*\(\[.\{-}\]\)\={.\{-}}" + + " Set all listings environments to listings + syntax cluster texFoldGroup add=texZoneListings + syntax region texZoneListings + \ start="\\begin{lstlisting}\(\_s*\[\_[^\]]\{-}\]\)\?"rs=s + \ end="\\end{lstlisting}\|%stopzone\>"re=e + \ keepend + \ contains=texBeginEnd + + " Next add nested syntax support for desired languages + for l:nested in b:vimtex_syntax.listings + let l:cluster = vimtex#syntax#misc#include(l:nested) + if empty(l:cluster) | continue | endif + + let l:group_main = 'texZoneListings' . toupper(l:nested[0]) . l:nested[1:] + let l:group_lstset = l:group_main . 'Lstset' + let l:group_contained = l:group_main . 'Contained' + execute 'syntax cluster texFoldGroup add=' . l:group_main + execute 'syntax cluster texFoldGroup add=' . l:group_lstset + + execute 'syntax region' l:group_main + \ 'start="\c\\begin{lstlisting}\s*' + \ . '\[\_[^\]]\{-}language=' . l:nested . '\%(\s*,\_[^\]]\{-}\)\?\]"rs=s' + \ 'end="\\end{lstlisting}"re=e' + \ 'keepend' + \ 'transparent' + \ 'contains=texBeginEnd,@' . l:cluster + + execute 'syntax match' l:group_lstset + \ '"\c\\lstset{.*language=' . l:nested . '\%(\s*,\|}\)"' + \ 'transparent' + \ 'contains=texStatement,texMatcher' + \ 'skipwhite skipempty' + \ 'nextgroup=' . l:group_contained + + execute 'syntax region' l:group_contained + \ 'start="\\begin{lstlisting}"rs=s' + \ 'end="\\end{lstlisting}"re=e' + \ 'keepend' + \ 'transparent' + \ 'containedin=' . l:group_lstset + \ 'contains=texStatement,texBeginEnd,@' . l:cluster + endfor + + highlight link texZoneListings texZone +endfunction + +" }}}1 + +function! s:get_nested_languages() abort " {{{1 + return map( + \ filter(getline(1, '$'), "v:val =~# 'language='"), + \ 'matchstr(v:val, ''language=\zs\w\+'')') +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/luacode.vim b/autoload/vimtex/syntax/p/luacode.vim new file mode 100644 index 00000000..5e00c690 --- /dev/null +++ b/autoload/vimtex/syntax/p/luacode.vim @@ -0,0 +1,31 @@ +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#syntax#p#luacode#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'luacode') | return | endif + let b:vimtex_syntax.luacode = 1 + + call vimtex#syntax#misc#include('lua') + call vimtex#syntax#misc#add_to_section_clusters('texZoneLua') + syntax region texZoneLua + \ start='\\begin{luacode\*\?}'rs=s + \ end='\\end{luacode\*\?}'re=e + \ keepend + \ transparent + \ contains=texBeginEnd,@vimtex_nested_lua + syntax match texStatement '\\\(directlua\|luadirect\)' nextgroup=texZoneLuaArg + syntax region texZoneLuaArg matchgroup=Delimiter + \ start='{' + \ end='}' + \ contained + \ contains=@vimtex_nested_lua +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/markdown.vim b/autoload/vimtex/syntax/p/markdown.vim new file mode 100644 index 00000000..6b7a6ad6 --- /dev/null +++ b/autoload/vimtex/syntax/p/markdown.vim @@ -0,0 +1,43 @@ +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#syntax#p#markdown#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'markdown') | return | endif + let b:vimtex_syntax.markdown = 1 + + call vimtex#syntax#misc#add_to_section_clusters('texZoneMarkdown') + call vimtex#syntax#misc#include('markdown') + + " Don't quite know why this is necessary, but it is + syntax match texBeginEnd + \ '\(\\begin\>\|\\end\>\)\ze{markdown}' + \ nextgroup=texBeginEndName + + syntax region texZoneMarkdown + \ start='\\begin{markdown}'rs=s + \ end='\\end{markdown}'re=e + \ keepend + \ transparent + \ contains=@texFoldGroup,@texDocGroup,@vimtex_nested_markdown + + " Input files + syntax match texInputFile /\\markdownInput\>/ + \ contains=texStatement + \ nextgroup=texInputFileArg + syntax region texInputFileArg + \ matchgroup=texInputCurlies + \ start="{" end="}" + \ contained + \ contains=texComment + + highlight default link texInputFileArg texInputFile +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/mathtools.vim b/autoload/vimtex/syntax/p/mathtools.vim new file mode 100644 index 00000000..24f7080a --- /dev/null +++ b/autoload/vimtex/syntax/p/mathtools.vim @@ -0,0 +1,21 @@ +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 +" + +scriptencoding utf-8 + +function! vimtex#syntax#p#mathtools#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'mathtools') | return | endif + let b:vimtex_syntax.mathtools = 1 + + " Load amsmath + call vimtex#syntax#p#amsmath#load() +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/minted.vim b/autoload/vimtex/syntax/p/minted.vim new file mode 100644 index 00000000..02793c34 --- /dev/null +++ b/autoload/vimtex/syntax/p/minted.vim @@ -0,0 +1,256 @@ +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#syntax#p#minted#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'minted') | return | endif + let b:vimtex_syntax.minted = 1 + + " Parse minted macros in the current project + call s:parse_minted_constructs() + + " Match minted language names + syntax region texMintedName matchgroup=Delimiter start="{" end="}" contained + syntax region texMintedNameOpt matchgroup=Delimiter start="\[" end="\]" contained + + " Match boundaries of minted environments + syntax match texMintedBounds '\\end{minted}' + \ contained + \ contains=texBeginEnd + syntax match texMintedBounds '\\begin{minted}' + \ contained + \ contains=texBeginEnd + \ nextgroup=texMintedBoundsOpts,texMintedName + syntax region texMintedBoundsOpts matchgroup=Delimiter + \ start="\[" end="\]" + \ contained + \ nextgroup=texMintedName + + " Match starred custom minted environments with options + syntax match texMintedStarred "\\begin{\w\+\*}" + \ contained + \ contains=texBeginEnd + \ nextgroup=texMintedStarredOpts + syntax region texMintedStarredOpts matchgroup=Delimiter + \ start='{' + \ end='}' + \ contained + \ containedin=texMintedStarred + + " Match \newminted type macros + syntax match texStatement '\\newmint\%(ed\|inline\)\?' nextgroup=texMintedName,texMintedNameOpt + + " Match "unknown" environments + call vimtex#syntax#misc#add_to_section_clusters('texZoneMinted') + syntax region texZoneMinted + \ start="\\begin{minted}\%(\_s*\[\_[^\]]\{-}\]\)\?\_s*{\w\+}"rs=s + \ end="\\end{minted}"re=e + \ keepend + \ contains=texMintedBounds.* + + " Match "unknown" commands + syntax match texArgMinted "{\w\+}" + \ contained + \ contains=texMintedName + \ nextgroup=texZoneMintedCmd + syntax region texZoneMintedCmd matchgroup=Delimiter + \ start='\z([|+/]\)' + \ end='\z1' + \ contained + syntax region texZoneMintedCmd matchgroup=Delimiter + \ start='{' + \ end='}' + \ contained + + " Next add nested syntax support for desired languages + for [l:nested, l:config] in items(b:vimtex.syntax.minted) + let l:cluster = vimtex#syntax#misc#include(l:nested) + + let l:name = 'Minted' . toupper(l:nested[0]) . l:nested[1:] + let l:group_main = 'texZone' . l:name + let l:group_arg = 'texArg' . l:name + let l:group_arg_zone = 'texArgZone' . l:name + call vimtex#syntax#misc#add_to_section_clusters(l:group_main) + + if empty(l:cluster) + let l:transparent = '' + let l:contains_env = '' + let l:contains_macro = '' + execute 'highlight link' l:group_main 'texZoneMinted' + execute 'highlight link' l:group_arg_zone 'texZoneMinted' + else + let l:transparent = 'transparent' + let l:contains_env = ',@' . l:cluster + let l:contains_macro = 'contains=@' . l:cluster + endif + + " Match minted environment + execute 'syntax region' l:group_main + \ 'start="\\begin{minted}\%(\_s*\[\_[^\]]\{-}\]\)\?\_s*{' . l:nested . '}"rs=s' + \ 'end="\\end{minted}"re=e' + \ 'keepend' + \ l:transparent + \ 'contains=texMintedBounds.*' . l:contains_env + + " Match custom environment names + for l:env in get(l:config, 'environments', []) + execute 'syntax region' l:group_main + \ 'start="\\begin{\z(' . l:env . '\*\?\)}"rs=s' + \ 'end="\\end{\z1}"re=e' + \ 'keepend' + \ l:transparent + \ 'contains=texMintedStarred,texBeginEnd' . l:contains_env + endfor + + " Match minted macros + " - \mint[]{lang}|...| + " - \mint[]{lang}{...} + " - \mintinline[]{lang}|...| + " - \mintinline[]{lang}{...} + execute 'syntax match' l:group_arg '''{' . l:nested . '}''' + \ 'contained' + \ 'contains=texMintedName' + \ 'nextgroup=' . l:group_arg_zone + execute 'syntax region' l:group_arg_zone + \ 'matchgroup=Delimiter' + \ 'start=''\z([|+/]\)''' + \ 'end=''\z1''' + \ 'contained' + \ l:contains_macro + execute 'syntax region' l:group_arg_zone + \ 'matchgroup=Delimiter' + \ 'start=''{''' + \ 'end=''}''' + \ 'contained' + \ l:contains_macro + + " Match minted custom macros + for l:cmd in sort(get(l:config, 'commands', [])) + execute printf('syntax match texStatement ''\\%s'' nextgroup=%s', + \ l:cmd, l:group_arg_zone) + endfor + endfor + + " Main matcher for the minted statements/commands + " - Note: This comes last to allow the nextgroup pattern + syntax match texStatement '\\mint\(inline\)\?' nextgroup=texArgOptMinted,texArgMinted.* + syntax region texArgOptMinted matchgroup=Delimiter + \ start='\[' + \ end='\]' + \ contained + \ nextgroup=texArgMinted.* + + highlight link texZoneMinted texZone + highlight link texZoneMintedCmd texZone + highlight link texMintedName texInputFileOpt + highlight link texMintedNameOpt texMintedName +endfunction + +" }}}1 + +function! s:parse_minted_constructs() abort " {{{1 + if has_key(b:vimtex.syntax, 'minted') | return | endif + + let l:db = deepcopy(s:db) + let b:vimtex.syntax.minted = l:db.data + + let l:in_multi = 0 + for l:line in vimtex#parser#tex(b:vimtex.tex, {'detailed': 0}) + " Multiline minted environments + if l:in_multi + let l:lang = matchstr(l:line, '\]\s*{\zs\w\+\ze}') + if !empty(l:lang) + call l:db.register(l:lang) + let l:in_multi = 0 + endif + continue + endif + if l:line =~# '\\begin{minted}\s*\[[^\]]*$' + let l:in_multi = 1 + continue + endif + + " Single line minted environments + let l:lang = matchstr(l:line, '\\begin{minted}\%(\s*\[\[^\]]*\]\)\?\s*{\zs\w\+\ze}') + if !empty(l:lang) + call l:db.register(l:lang) + continue + endif + + " Simple minted commands + let l:lang = matchstr(l:line, '\\mint\%(\s*\[[^\]]*\]\)\?\s*{\zs\w\+\ze}') + if !empty(l:lang) + call l:db.register(l:lang) + continue + endif + + " Custom environments: + " - \newminted{lang}{opts} -> langcode + " - \newminted[envname]{lang}{opts} -> envname + let l:matches = matchlist(l:line, + \ '\\newminted\%(\s*\[\([^\]]*\)\]\)\?\s*{\([a-zA-Z-]\+\)}') + if !empty(l:matches) + call l:db.register(l:matches[2]) + call l:db.add_environment(!empty(l:matches[1]) + \ ? l:matches[1] + \ : l:matches[2] . 'code') + continue + endif + + " Custom macros: + " - \newmint(inline){lang}{opts} -> \lang(inline) + " - \newmint(inline)[macroname]{lang}{opts} -> \macroname + let l:matches = matchlist(l:line, + \ '\\newmint\(inline\)\?\%(\s*\[\([^\]]*\)\]\)\?\s*{\([a-zA-Z-]\+\)}') + if !empty(l:matches) + call l:db.register(l:matches[3]) + call l:db.add_macro(!empty(l:matches[2]) + \ ? l:matches[2] + \ : l:matches[3] . l:matches[1]) + continue + endif + endfor +endfunction + +" }}}1 + + +let s:db = { + \ 'data' : {}, + \} + +function! s:db.register(lang) abort dict " {{{1 + " Avoid dashes in langnames + let l:lang = substitute(a:lang, '-', '', 'g') + + if !has_key(self.data, l:lang) + let self.data[l:lang] = { + \ 'environments' : [], + \ 'commands' : [], + \} + endif + + let self.cur = self.data[l:lang] +endfunction + +" }}}1 +function! s:db.add_environment(envname) abort dict " {{{1 + if index(self.cur.environments, a:envname) < 0 + let self.cur.environments += [a:envname] + endif +endfunction + +" }}}1 +function! s:db.add_macro(macroname) abort dict " {{{1 + if index(self.cur.commands, a:macroname) < 0 + let self.cur.commands += [a:macroname] + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/moreverb.vim b/autoload/vimtex/syntax/p/moreverb.vim new file mode 100644 index 00000000..f6bb8f8c --- /dev/null +++ b/autoload/vimtex/syntax/p/moreverb.vim @@ -0,0 +1,26 @@ +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#syntax#p#moreverb#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'moreverb') | return | endif + let b:vimtex_syntax.moreverb = 1 + + if exists('g:tex_verbspell') + syntax region texZone start="\\begin{verbatimtab}" end="\\end{verbatimtab}\|%stopzone\>" contains=@Spell + syntax region texZone start="\\begin{verbatimwrite}" end="\\end{verbatimwrite}\|%stopzone\>" contains=@Spell + syntax region texZone start="\\begin{boxedverbatim}" end="\\end{boxedverbatim}\|%stopzone\>" contains=@Spell + else + syntax region texZone start="\\begin{verbatimtab}" end="\\end{verbatimtab}\|%stopzone\>" + syntax region texZone start="\\begin{verbatimwrite}" end="\\end{verbatimwrite}\|%stopzone\>" + syntax region texZone start="\\begin{boxedverbatim}" end="\\end{boxedverbatim}\|%stopzone\>" + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/natbib.vim b/autoload/vimtex/syntax/p/natbib.vim new file mode 100644 index 00000000..f28e2a94 --- /dev/null +++ b/autoload/vimtex/syntax/p/natbib.vim @@ -0,0 +1,18 @@ +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#syntax#p#natbib#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'natbib') | return | endif + let b:vimtex_syntax.natbib = 1 + + call vimtex#syntax#p#biblatex#load() +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/pdfpages.vim b/autoload/vimtex/syntax/p/pdfpages.vim new file mode 100644 index 00000000..ddd390d5 --- /dev/null +++ b/autoload/vimtex/syntax/p/pdfpages.vim @@ -0,0 +1,33 @@ +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#syntax#p#pdfpages#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'pdfpages') | return | endif + let b:vimtex_syntax.pdfpages = 1 + + syntax match texInputFile /\\includepdf\>/ + \ contains=texStatement + \ nextgroup=texInputFileOpt,texInputFileArg + syntax region texInputFileOpt + \ matchgroup=Delimiter + \ start="\[" end="\]" + \ contained + \ contains=texComment,@NoSpell + \ nextgroup=texInputFileArg + syntax region texInputFileArg + \ matchgroup=texInputCurlies + \ start="{" end="}" + \ contained + \ contains=texComment + + highlight default link texInputFileArg texInputFile +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/pgfplots.vim b/autoload/vimtex/syntax/p/pgfplots.vim new file mode 100644 index 00000000..15b0fe4a --- /dev/null +++ b/autoload/vimtex/syntax/p/pgfplots.vim @@ -0,0 +1,38 @@ +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#syntax#p#pgfplots#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'pgfplots') | return | endif + let b:vimtex_syntax.pgfplots = 1 + + " Load Tikz first + call vimtex#syntax#p#tikz#load() + + " Add texAxisStatement to Tikz cluster + syntax cluster texTikz add=texAxisStatement + + " Match pgfplotsset and axis environments + syntax match texTikzSet /\\pgfplotsset\>/ + \ contains=texStatement skipwhite nextgroup=texTikzOptsCurly + syntax match texTikzEnv /\v\\begin\{%(log)*axis}/ + \ contains=texBeginEnd nextgroup=texTikzOpts skipwhite + syntax match texTikzEnv /\v\\begin\{groupplot}/ + \ contains=texBeginEnd nextgroup=texTikzOpts skipwhite + + " Match some custom pgfplots macros + syntax match texAxisStatement /\\addplot3\>/ + \ contained skipwhite nextgroup=texTikzOpts + syntax match texAxisStatement /\\nextgroupplot\>/ + \ contained skipwhite nextgroup=texTikzOpts + + highlight def link texAxisStatement texStatement +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/pythontex.vim b/autoload/vimtex/syntax/p/pythontex.vim new file mode 100644 index 00000000..e58c3747 --- /dev/null +++ b/autoload/vimtex/syntax/p/pythontex.vim @@ -0,0 +1,40 @@ +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#syntax#p#pythontex#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'pythontex') | return | endif + let b:vimtex_syntax.pythontex = 1 + + call vimtex#syntax#misc#include('python') + + syntax match texStatement /\\py[bsc]\?/ contained nextgroup=texPythontexArg + syntax region texPythontexArg matchgroup=Delimiter + \ start='{' end='}' + \ contained contains=@vimtex_nested_python + syntax region texPythontexArg matchgroup=Delimiter + \ start='\z([#@]\)' end='\z1' + \ contained contains=@vimtex_nested_python + + call vimtex#syntax#misc#add_to_section_clusters('texZonePythontex') + syntax region texZonePythontex + \ start='\\begin{pyblock}'rs=s + \ end='\\end{pyblock}'re=e + \ keepend + \ transparent + \ contains=texBeginEnd,@vimtex_nested_python + syntax region texZonePythontex + \ start='\\begin{pycode}'rs=s + \ end='\\end{pycode}'re=e + \ keepend + \ transparent + \ contains=texBeginEnd,@vimtex_nested_python +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/subfile.vim b/autoload/vimtex/syntax/p/subfile.vim new file mode 100644 index 00000000..9192f09e --- /dev/null +++ b/autoload/vimtex/syntax/p/subfile.vim @@ -0,0 +1,19 @@ +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#syntax#p#subfile#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'subfile') | return | endif + let b:vimtex_syntax.subfile = 1 + + syntax match texInputFile /\\subfile\s*\%(\[.\{-}\]\)\=\s*{.\{-}}/ + \ contains=texStatement,texInputCurlies,texInputFileOpt +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/tabularx.vim b/autoload/vimtex/syntax/p/tabularx.vim new file mode 100644 index 00000000..0ff623c2 --- /dev/null +++ b/autoload/vimtex/syntax/p/tabularx.vim @@ -0,0 +1,77 @@ +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#syntax#p#tabularx#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'tabularx') | return | endif + let b:vimtex_syntax.tabularx = 1 + + call vimtex#syntax#misc#add_to_section_clusters('texTabular') + + syntax match texTabular '\\begin{tabular}\_[^{]\{-}\ze{' + \ contains=texBeginEnd + \ nextgroup=texTabularArg + \ contained + syntax region texTabularArg matchgroup=Delimiter + \ start='{' end='}' + \ contained + + syntax match texTabularCol /[lcr]/ + \ containedin=texTabularArg + \ contained + syntax match texTabularCol /[pmb]/ + \ containedin=texTabularArg + \ nextgroup=texTabularLength + \ contained + syntax match texTabularCol /\*/ + \ containedin=texTabularArg + \ nextgroup=texTabularMulti + \ contained + syntax region texTabularMulti matchgroup=Delimiter + \ start='{' end='}' + \ containedin=texTabularArg + \ nextgroup=texTabularArg + \ contained + + syntax match texTabularAtSep /@/ + \ containedin=texTabularArg + \ nextgroup=texTabularLength + \ contained + syntax match texTabularVertline /||\?/ + \ containedin=texTabularArg + \ contained + syntax match texTabularPostPre /[<>]/ + \ containedin=texTabularArg + \ nextgroup=texTabularPostPreArg + \ contained + + syntax region texTabularPostPreArg matchgroup=Delimiter + \ start='{' end='}' + \ containedin=texTabularArg + \ contains=texLength,texStatement,texMathDelimSingle + \ contained + + syntax region texTabularLength matchgroup=Delimiter + \ start='{' end='}' + \ containedin=texTabularArg + \ contains=texLength,texStatement + \ contained + + syntax match texMathDelimSingle /\$\$\?/ + \ containedin=texTabularPostPreArg + \ contained + + highlight def link texTabularCol Directory + highlight def link texTabularAtSep Type + highlight def link texTabularVertline Type + highlight def link texTabularPostPre Type + highlight def link texMathDelimSingle Delimiter +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/tikz.vim b/autoload/vimtex/syntax/p/tikz.vim new file mode 100644 index 00000000..fe0d7089 --- /dev/null +++ b/autoload/vimtex/syntax/p/tikz.vim @@ -0,0 +1,47 @@ +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#syntax#p#tikz#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'tikz') | return | endif + let b:vimtex_syntax.tikz = 1 + + call vimtex#syntax#misc#add_to_section_clusters('texTikzSet') + call vimtex#syntax#misc#add_to_section_clusters('texTikzpicture') + + " Define clusters + syntax cluster texTikz contains=texTikzEnv,texBeginEnd,texStatement,texTikzSemicolon,texComment,@texVimtexGlobal + syntax cluster texTikzOS contains=texTikzOptsCurly,texTikzEqual,texMathZoneX,texTypeSize,texStatement,texLength,texComment + + " Define tikz option groups + syntax match texTikzSet /\\tikzset\>/ + \ contains=texStatement skipwhite nextgroup=texTikzOptsCurly + syntax region texTikzOpts matchgroup=Delimiter + \ start='\[' end='\]' contained contains=@texTikzOS + syntax region texTikzOptsCurly matchgroup=Delimiter + \ start='{' end='}' contained contains=@texTikzOS + + syntax region texTikzpicture + \ start='\\begin{tikzpicture}'rs=s + \ end='\\end{tikzpicture}'re=e + \ keepend + \ transparent + \ contains=@texTikz + syntax match texTikzEnv /\v\\begin\{tikzpicture\}/ + \ contains=texBeginEnd nextgroup=texTikzOpts skipwhite + + syntax match texTikzEqual /=/ contained + syntax match texTikzSemicolon /;/ contained + + highlight def link texTikzEqual Operator + highlight def link texTikzSemicolon Delimiter +endfunction + +" }}}1 + + +endif diff --git a/autoload/vimtex/syntax/p/url.vim b/autoload/vimtex/syntax/p/url.vim new file mode 100644 index 00000000..a944cae9 --- /dev/null +++ b/autoload/vimtex/syntax/p/url.vim @@ -0,0 +1,18 @@ +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#syntax#p#url#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'url') | return | endif + let b:vimtex_syntax.url = 1 + + call vimtex#syntax#p#hyperref#load() +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/varioref.vim b/autoload/vimtex/syntax/p/varioref.vim new file mode 100644 index 00000000..020162d9 --- /dev/null +++ b/autoload/vimtex/syntax/p/varioref.vim @@ -0,0 +1,25 @@ +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#syntax#p#varioref#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'varioref') | return | endif + let b:vimtex_syntax.varioref = 1 + if get(g:, 'tex_fast', 'r') !~# 'r' | return | endif + + syntax match texStatement '\\Vref\>' nextgroup=texVarioRefZone + + syntax region texVarioRefZone contained matchgroup=Delimiter + \ start="{" end="}" + \ contains=@texRefGroup,texRefZone + + highlight link texVarioRefZone texRefZone +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/syntax/p/wiki.vim b/autoload/vimtex/syntax/p/wiki.vim new file mode 100644 index 00000000..46edf7b7 --- /dev/null +++ b/autoload/vimtex/syntax/p/wiki.vim @@ -0,0 +1,26 @@ +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#syntax#p#wiki#load() abort " {{{1 + if has_key(b:vimtex_syntax, 'wiki') | return | endif + let b:vimtex_syntax.wiki = 1 + + call vimtex#syntax#misc#add_to_section_clusters('texZoneWiki') + call vimtex#syntax#misc#include('markdown') + + syntax region texZoneWiki + \ start='\\wikimarkup\>' + \ end='\\nowikimarkup\>'re=e + \ keepend + \ transparent + \ contains=@vimtex_nested_markdown,@texFoldGroup,@texDocGroup +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/test.vim b/autoload/vimtex/test.vim new file mode 100644 index 00000000..9b9d50a2 --- /dev/null +++ b/autoload/vimtex/test.vim @@ -0,0 +1,98 @@ +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#test#assert(condition) abort " {{{1 + if a:condition | return 1 | endif + + call s:fail() +endfunction + +" }}}1 +function! vimtex#test#assert_equal(expect, observe) abort " {{{1 + if a:expect ==# a:observe | return 1 | endif + + call s:fail([ + \ 'expect: ' . string(a:expect), + \ 'observe: ' . string(a:observe), + \]) +endfunction + +" }}}1 +function! vimtex#test#assert_match(x, regex) abort " {{{1 + if a:x =~# a:regex | return 1 | endif + + call s:fail([ + \ 'x = ' . string(a:x), + \ 'regex = ' . a:regex, + \]) +endfunction + +" }}}1 + +function! vimtex#test#completion(context, ...) abort " {{{1 + let l:base = a:0 > 0 ? a:1 : '' + + try + silent execute 'normal GO' . a:context . "\<c-x>\<c-o>" + silent normal! u + return vimtex#complete#omnifunc(0, l:base) + catch /.*/ + call s:fail(v:exception) + endtry +endfunction + +" }}}1 +function! vimtex#test#keys(keys, context, expected) abort " {{{1 + normal! gg0dG + call append(1, a:context) + normal! ggdd + + let l:fail_msg = ['keys: ' . a:keys] + let l:fail_msg += ['context:'] + let l:fail_msg += map(copy(a:context), '" " . v:val') + let l:fail_msg += ['expected:'] + let l:fail_msg += map(copy(a:expected), '" " . v:val') + + try + silent execute 'normal' a:keys + catch + let l:fail_msg += ['error:'] + let l:fail_msg += [' ' . v:exception] + call s:fail(l:fail_msg) + endtry + + let l:result = getline(1, line('$')) + if l:result ==# a:expected | return 1 | endif + + let l:fail_msg += ['result:'] + let l:fail_msg += map(l:result, '" " . v:val') + call s:fail(l:fail_msg) +endfunction + +" }}}1 + +function! s:fail(...) abort " {{{1 + echo 'Assertion failed!' + + if a:0 > 0 && !empty(a:1) + if type(a:1) == type('') + echo a:1 + else + for line in a:1 + echo line + endfor + endif + endif + echon "\n" + + cquit +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/text_obj.vim b/autoload/vimtex/text_obj.vim new file mode 100644 index 00000000..27d1a8b6 --- /dev/null +++ b/autoload/vimtex/text_obj.vim @@ -0,0 +1,447 @@ +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#text_obj#init_buffer() abort " {{{1 + if !g:vimtex_text_obj_enabled | return | endif + + " Note: I've permitted myself long lines here to make this more readable. + for [l:map, l:name, l:opt] in [ + \ ['c', 'commands', ''], + \ ['d', 'delimited', 'delim_all'], + \ ['e', 'delimited', 'env_tex'], + \ ['$', 'delimited', 'env_math'], + \ ['P', 'sections', ''], + \ ['m', 'items', ''], + \] + let l:optional = empty(l:opt) ? '' : ',''' . l:opt . '''' + execute printf('xnoremap <silent><buffer> <plug>(vimtex-i%s) :<c-u>call vimtex#text_obj#%s(1, 1%s)<cr>', l:map, l:name, l:optional) + execute printf('xnoremap <silent><buffer> <plug>(vimtex-a%s) :<c-u>call vimtex#text_obj#%s(0, 1%s)<cr>', l:map, l:name, l:optional) + execute printf('onoremap <silent><buffer> <plug>(vimtex-i%s) :<c-u>call vimtex#text_obj#%s(1, 0%s)<cr>', l:map, l:name, l:optional) + execute printf('onoremap <silent><buffer> <plug>(vimtex-a%s) :<c-u>call vimtex#text_obj#%s(0, 0%s)<cr>', l:map, l:name, l:optional) + endfor +endfunction + +" }}}1 + +function! vimtex#text_obj#commands(is_inner, mode) abort " {{{1 + let l:obj = {} + let l:pos_save = vimtex#pos#get_cursor() + if a:mode + call vimtex#pos#set_cursor(getpos("'>")) + endif + + " Get the delimited text object positions + for l:count in range(v:count1) + if !empty(l:obj) + call vimtex#pos#set_cursor(vimtex#pos#prev(l:obj.cmd_start)) + endif + + let l:obj_prev = l:obj + let l:obj = {} + + let l:cmd = vimtex#cmd#get_current() + if empty(l:cmd) | break | endif + + let l:pos_start = copy(l:cmd.pos_start) + let l:pos_end = l:cmd.pos_end + + if a:is_inner + let l:pos_end.lnum = l:pos_start.lnum + let l:pos_end.cnum = l:pos_start.cnum + strlen(l:cmd.name) - 1 + let l:pos_start.cnum += 1 + endif + + if a:mode + \ && vimtex#pos#equal(l:pos_start, getpos("'<")) + \ && vimtex#pos#equal(l:pos_end, getpos("'>")) + let l:pos_old = l:cmd.pos_start + call vimtex#pos#set_cursor(vimtex#pos#prev(l:pos_old)) + + let l:cmd = vimtex#cmd#get_current() + if empty(l:cmd) | break | endif + + if vimtex#pos#smaller(l:pos_old, l:cmd.pos_end) + let l:pos_start = l:cmd.pos_start + let l:pos_end = l:cmd.pos_end + + if a:is_inner + let l:pos_end.lnum = l:pos_start.lnum + let l:pos_end.cnum = l:pos_start.cnum + strlen(l:cmd.name) - 1 + let l:pos_start.cnum += 1 + endif + endif + endif + + let l:obj = { + \ 'pos_start' : l:pos_start, + \ 'pos_end' : l:pos_end, + \ 'cmd_start' : l:cmd.pos_start, + \} + endfor + + if empty(l:obj) + if empty(l:obj_prev) || g:vimtex_text_obj_variant ==# 'targets' + if a:mode + normal! gv + else + call vimtex#pos#set_cursor(l:pos_save) + endif + return + endif + let l:obj = l:obj_prev + endif + + call vimtex#pos#set_cursor(l:pos_start) + normal! v + call vimtex#pos#set_cursor(l:pos_end) +endfunction + +" }}}1 +function! vimtex#text_obj#delimited(is_inner, mode, type) abort " {{{1 + let l:object = {} + let l:prev_object = {} + let l:pos_save = vimtex#pos#get_cursor() + let l:startpos = getpos("'>") + + " Get the delimited text object positions + for l:count in range(v:count1) + if !empty(l:object) + let l:pos_next = vimtex#pos#prev( + \ a:is_inner ? l:object.open : l:object.pos_start) + + if a:mode + let l:startpos = l:pos_next + else + call vimtex#pos#set_cursor(l:pos_next) + endif + endif + + if a:mode + let l:object = s:get_sel_delimited_visual(a:is_inner, a:type, l:startpos) + else + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + let l:object = empty(l:open) + \ ? {} : s:get_sel_delimited(l:open, l:close, a:is_inner) + endif + + if empty(l:object) + if !empty(l:prev_object) && g:vimtex_text_obj_variant !=# 'targets' + let l:object = l:prev_object + break + endif + + if a:mode + normal! gv + else + call vimtex#pos#set_cursor(l:pos_save) + endif + return + endif + + let l:prev_object = l:object + endfor + + " Handle empty inner objects + if vimtex#pos#smaller(l:object.pos_end, l:object.pos_start) + if v:operator ==# 'y' && !a:mode + return + endif + + if index(['c', 'd'], v:operator) >= 0 + call vimtex#pos#set_cursor(l:object.pos_start) + normal! ix + endif + + let l:object.pos_end = l:object.pos_start + endif + + " Apply selection + execute 'normal!' l:object.select_mode + call vimtex#pos#set_cursor(l:object.pos_start) + normal! o + call vimtex#pos#set_cursor(l:object.pos_end) +endfunction + +" }}}1 +function! vimtex#text_obj#sections(is_inner, mode) abort " {{{1 + let l:pos_save = vimtex#pos#get_cursor() + call vimtex#pos#set_cursor(vimtex#pos#next(l:pos_save)) + + " Get section border positions + let [l:pos_start, l:pos_end, l:type] + \ = s:get_sel_sections(a:is_inner, '') + if empty(l:pos_start) + call vimtex#pos#set_cursor(l:pos_save) + return + endif + + " Increase visual area if applicable + if a:mode + \ && visualmode() ==# 'V' + \ && getpos("'<")[1] == l:pos_start[0] + \ && getpos("'>")[1] == l:pos_end[0] + let [l:pos_start_new, l:pos_end_new, l:type] + \ = s:get_sel_sections(a:is_inner, l:type) + if !empty(l:pos_start_new) + let l:pos_start = l:pos_start_new + let l:pos_end = l:pos_end_new + endif + endif + + " Repeat for count + for l:count in range(v:count1 - 1) + let [l:pos_start_new, l:pos_end_new, l:type] + \ = s:get_sel_sections(a:is_inner, l:type) + + if empty(l:pos_start_new) | break | endif + let l:pos_start = l:pos_start_new + let l:pos_end = l:pos_end_new + endfor + + " Apply selection + call vimtex#pos#set_cursor(l:pos_start) + normal! V + call vimtex#pos#set_cursor(l:pos_end) +endfunction + +" }}}1 +function! vimtex#text_obj#items(is_inner, mode) abort " {{{1 + let l:pos_save = vimtex#pos#get_cursor() + + " Get section border positions + let [l:pos_start, l:pos_end] = s:get_sel_items(a:is_inner) + if empty(l:pos_start) + call vimtex#pos#set_cursor(l:pos_save) + return + endif + + " Apply selection + execute 'normal!' (v:operator ==# ':') ? visualmode() : 'v' + call vimtex#pos#set_cursor(l:pos_start) + normal! o + call vimtex#pos#set_cursor(l:pos_end) +endfunction + +" }}}1 + +function! s:get_sel_delimited_visual(is_inner, type, startpos) abort " {{{1 + if a:is_inner + call vimtex#pos#set_cursor(vimtex#pos#next(a:startpos)) + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if !empty(l:open) + let l:object = s:get_sel_delimited(l:open, l:close, a:is_inner) + + " Select next pair if we reached the same selection + if (l:object.select_mode ==# 'v' + \ && getpos("'<")[1:2] == l:object.pos_start + \ && getpos("'>")[1:2] == l:object.pos_end) + \ || (l:object.select_mode ==# 'V' + \ && getpos("'<")[1] == l:object.pos_start[0] + \ && getpos("'>")[1] == l:object.pos_end[0]) + call vimtex#pos#set_cursor(vimtex#pos#prev(l:open.lnum, l:open.cnum)) + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if empty(l:open) | return {} | endif + return s:get_sel_delimited(l:open, l:close, a:is_inner) + endif + endif + endif + + call vimtex#pos#set_cursor(a:startpos) + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if empty(l:open) | return {} | endif + let l:object = s:get_sel_delimited(l:open, l:close, a:is_inner) + if a:is_inner | return l:object | endif + + " Select next pair if we reached the same selection + if (l:object.select_mode ==# 'v' + \ && getpos("'<")[1:2] == l:object.pos_start + \ && getpos("'>")[1:2] == l:object.pos_end) + \ || (l:object.select_mode ==# 'V' + \ && getpos("'<")[1] == l:object.pos_start[0] + \ && getpos("'>")[1] == l:object.pos_end[0]) + call vimtex#pos#set_cursor(vimtex#pos#prev(l:open.lnum, l:open.cnum)) + let [l:open, l:close] = vimtex#delim#get_surrounding(a:type) + if empty(l:open) | return {} | endif + return s:get_sel_delimited(l:open, l:close, a:is_inner) + endif + + return l:object +endfunction + +" }}}1 +function! s:get_sel_delimited(open, close, is_inner) abort " {{{1 + " Determine if operator is linewise + let l:linewise = index(g:vimtex_text_obj_linewise_operators, v:operator) >= 0 + + let [l1, c1, l2, c2] = [a:open.lnum, a:open.cnum, a:close.lnum, a:close.cnum] + + " Adjust the borders + if a:is_inner + if has_key(a:open, 'env_cmd') && !empty(a:open.env_cmd) + let l1 = a:open.env_cmd.pos_end.lnum + let c1 = a:open.env_cmd.pos_end.cnum+1 + else + let c1 += len(a:open.match) + endif + let c2 -= 1 + + let l:is_inline = (l2 - l1) > 1 + \ && match(strpart(getline(l1), c1), '^\s*$') >= 0 + \ && match(strpart(getline(l2), 0, c2), '^\s*$') >= 0 + + if l:is_inline + let l1 += 1 + let c1 = strlen(matchstr(getline(l1), '^\s*')) + 1 + let l2 -= 1 + let c2 = strlen(getline(l2)) + if c2 == 0 && !l:linewise + let l2 -= 1 + let c2 = len(getline(l2)) + 1 + endif + elseif c2 == 0 + let l2 -= 1 + let c2 = len(getline(l2)) + 1 + endif + else + let c2 += len(a:close.match) - 1 + + let l:is_inline = (l2 - l1) > 1 + \ && match(strpart(getline(l1), 0, c1-1), '^\s*$') >= 0 + \ && match(strpart(getline(l2), 0, c2), '^\s*$') >= 0 + endif + + return { + \ 'open' : a:open, + \ 'close' : a:close, + \ 'pos_start' : [l1, c1], + \ 'pos_end' : [l2, c2], + \ 'is_inline' : l:is_inline, + \ 'select_mode' : l:is_inline && l:linewise + \ ? 'V' : (v:operator ==# ':') ? visualmode() : 'v', + \} +endfunction + +" }}}1 +function! s:get_sel_sections(is_inner, type) abort " {{{1 + let l:pos_save = vimtex#pos#get_cursor() + let l:min_val = get(s:section_to_val, a:type) + + " Get the position of the section start + while 1 + let l:pos_start = searchpos(s:section_search, 'bcnW') + if l:pos_start == [0, 0] | return [[], [], ''] | endif + + let l:sec_type = matchstr(getline(l:pos_start[0]), s:section_search) + let l:sec_val = s:section_to_val[l:sec_type] + + if !empty(a:type) + if l:sec_val >= l:min_val + call vimtex#pos#set_cursor(vimtex#pos#prev(l:pos_start)) + else + call vimtex#pos#set_cursor(l:pos_save) + break + endif + else + break + endif + endwhile + + " Get the position of the section end + while 1 + let l:pos_end = searchpos(s:section_search, 'nW') + if l:pos_end == [0, 0] + let l:pos_end = [line('$')+1, 1] + break + endif + + let l:cur_val = s:section_to_val[ + \ matchstr(getline(l:pos_end[0]), s:section_search)] + if l:cur_val <= l:sec_val + let l:pos_end[0] -= 1 + break + endif + + call vimtex#pos#set_cursor(l:pos_end) + endwhile + + " Adjust for inner text object + if a:is_inner + call vimtex#pos#set_cursor(l:pos_start[0]+1, l:pos_start[1]) + let l:pos_start = searchpos('\S', 'cnW') + call vimtex#pos#set_cursor(l:pos_end) + let l:pos_end = searchpos('\S', 'bcnW') + elseif l:sec_val ==# 'document' + let l:pos_start = [l:pos_start[0]+1, l:pos_start[1]] + endif + + return [l:pos_start, l:pos_end, l:sec_type] +endfunction + +" }}}1 +function! s:get_sel_items(is_inner) abort " {{{1 + let l:pos_cursor = vimtex#pos#get_cursor() + + " Find previous \item + call vimtex#pos#set_cursor(l:pos_cursor[0], 1) + let l:pos_start = searchpos('^\s*\\item\S*', 'bcnWz') + if l:pos_start == [0, 0] | return [[], []] | endif + + " Find end of current \item + call vimtex#pos#set_cursor(l:pos_start) + let l:pos_end = searchpos('\ze\n\s*\%(\\item\|\\end{itemize}\)', 'nW') + if l:pos_end == [0, 0] + \ || vimtex#pos#val(l:pos_cursor) > vimtex#pos#val(l:pos_end) + return [[], []] + endif + + " Adjust for outer text object + if a:is_inner + let l:pos_start[1] = searchpos('^\s*\\item\S*\s\?', 'cne')[1] + 1 + let l:pos_end[1] = col([l:pos_end[0], '$']) - 1 + endif + + return [l:pos_start, l:pos_end] +endfunction + +" }}}1 + + +" {{{1 Initialize module + +" Pattern to match section/chapter/... +let s:section_search = '\v%(%(\\@<!%(\\\\)*)@<=\%.*)@<!\s*\\\zs(' + \ . join([ + \ '%(sub)?paragraph>', + \ '%(sub)*section>', + \ 'chapter>', + \ 'part>', + \ 'appendix>', + \ '%(front|back|main)matter>', + \ '%(begin|end)\{\zsdocument\ze\}' + \ ], '|') + \ .')' + +" Dictionary to give values to sections in order to compare them +let s:section_to_val = { + \ 'document': 0, + \ 'frontmatter': 1, + \ 'mainmatter': 1, + \ 'appendix': 1, + \ 'backmatter': 1, + \ 'part': 1, + \ 'chapter': 2, + \ 'section': 3, + \ 'subsection': 4, + \ 'subsubsection': 5, + \ 'paragraph': 6, + \ 'subparagraph': 7, + \} + +" }}}1 + +endif diff --git a/autoload/vimtex/text_obj/cmdtargets.vim b/autoload/vimtex/text_obj/cmdtargets.vim new file mode 100644 index 00000000..611285c7 --- /dev/null +++ b/autoload/vimtex/text_obj/cmdtargets.vim @@ -0,0 +1,85 @@ +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#text_obj#cmdtargets#new(args) " {{{1 + return { + \ 'genFuncs': { + \ 'c': function('vimtex#text_obj#cmdtargets#current'), + \ 'n': function('vimtex#text_obj#cmdtargets#next'), + \ 'l': function('vimtex#text_obj#cmdtargets#last'), + \ }, + \ 'modFuncs': { + \ 'i': [function('vimtex#text_obj#cmdtargets#inner'), + \ function('targets#modify#drop')], + \ 'a': [function('targets#modify#keep')], + \ 'I': [function('vimtex#text_obj#cmdtargets#inner'), + \ function('targets#modify#shrink')], + \ 'A': [function('targets#modify#expand')], + \ }} +endfunction + +" }}}1 +function! vimtex#text_obj#cmdtargets#current(args, opts, state) " {{{1 + let target = s:select(a:opts.first ? 1 : 2) + call target.cursorE() " keep going from right end + return target +endfunction + +" }}}1 +function! vimtex#text_obj#cmdtargets#next(args, opts, state) " {{{1 + if targets#util#search('\\\S*{', 'W') > 0 + return targets#target#withError('no target') + endif + + let oldpos = getpos('.') + let target = s:select(1) + call setpos('.', oldpos) + return target +endfunction + +" }}}1 +function! vimtex#text_obj#cmdtargets#last(args, opts, state) " {{{1 + " Move to the last non-surrounding cmd + if targets#util#search('\\\S\+{\_.\{-}}', 'bWe') > 0 + return targets#target#withError('no target') + endif + + let oldpos = getpos('.') + let target = s:select(1) + call setpos('.', oldpos) + return target +endfunction + +" }}}1 +function! vimtex#text_obj#cmdtargets#inner(target, args) " {{{1 + if a:target.state().isInvalid() + return + endif + + call a:target.cursorS() + silent! normal! f{ + call a:target.setS() +endfunction + +" }}}1 + +function! s:select(count) " {{{1 + " Try to select command + silent! execute 'keepjumps normal v'.a:count."\<Plug>(vimtex-ac)v" + let target = targets#target#fromVisualSelection() + + if target.sc == target.ec && target.sl == target.el + return targets#target#withError('tex_cmd select') + endif + + return target +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/text_obj/envtargets.vim b/autoload/vimtex/text_obj/envtargets.vim new file mode 100644 index 00000000..10e3ae93 --- /dev/null +++ b/autoload/vimtex/text_obj/envtargets.vim @@ -0,0 +1,110 @@ +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#text_obj#envtargets#new(args) " {{{1 + return { + \ 'genFuncs': { + \ 'c': function('vimtex#text_obj#envtargets#current'), + \ 'n': function('vimtex#text_obj#envtargets#next'), + \ 'l': function('vimtex#text_obj#envtargets#last'), + \ }, + \ 'modFuncs': { + \ 'i': [function('vimtex#text_obj#envtargets#inner'), function('targets#modify#drop')], + \ 'a': [function('targets#modify#keep')], + \ 'I': [function('vimtex#text_obj#envtargets#inner'), function('targets#modify#shrink')], + \ 'A': [function('vimtex#text_obj#envtargets#expand')], + \ }} +endfunction + +" }}}1 +function! vimtex#text_obj#envtargets#current(args, opts, state) " {{{1 + let target = s:select(a:opts.first ? 1 : 2) + call target.cursorE() " keep going from right end + return target +endfunction + +" }}}1 +function! vimtex#text_obj#envtargets#next(args, opts, state) " {{{1 + if targets#util#search('\\begin{.*}', 'W') > 0 + return targets#target#withError('no target') + endif + + let oldpos = getpos('.') + let target = s:select(1) + call setpos('.', oldpos) + return target +endfunction + +" }}}1 +function! vimtex#text_obj#envtargets#last(args, opts, state) " {{{1 + if targets#util#search('\\end{.*}', 'bW') > 0 + return targets#target#withError('no target') + endif + + let oldpos = getpos('.') + let target = s:select(1) + call setpos('.', oldpos) + return target +endfunction + +" }}}1 +function! vimtex#text_obj#envtargets#inner(target, args) " {{{1 + call a:target.cursorS() + call a:target.searchposS('\\begin{.*}', 'Wce') + call a:target.cursorE() + call a:target.searchposE('\\end{.*}', 'bWc') +endfunction + +" }}}1 +function! vimtex#text_obj#envtargets#expand(target, args) " {{{1 + " Based on targets#modify#expand() from + " $VIMMRUNTIME/autoload/targets/modify.vim + + " Add outer whitespace + if a:0 == 0 || a:1 ==# '>' + call a:target.cursorE() + let [line, column] = searchpos('\S\|$', '') + if line > a:target.el || (line > 0 && column-1 > a:target.ec) + " non whitespace or EOL after trailing whitespace found + " not counting whitespace directly after end + return a:target.setE(line, column-1) + endif + endif + + if a:0 == 0 || a:1 ==# '<' + call a:target.cursorS() + let [line, column] = searchpos('\S', 'b') + if line < a:target.sl + return a:target.setS(line+1, 0) + elseif line > 0 + " non whitespace before leading whitespace found + return a:target.setS(line, column+1) + endif + " only whitespace in front of start + " include all leading whitespace from beginning of line + let a:target.sc = 1 + endif +endfunction + +" }}}1 + +function! s:select(count) " {{{1 + " Try to select environment + silent! execute 'keepjumps normal v'.a:count."\<Plug>(vimtex-ae)v" + let target = targets#target#fromVisualSelection() + + if target.sc == target.ec && target.sl == target.el + return targets#target#withError('tex_env select') + endif + + return target +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/text_obj/targets.vim b/autoload/vimtex/text_obj/targets.vim new file mode 100644 index 00000000..6bbdf710 --- /dev/null +++ b/autoload/vimtex/text_obj/targets.vim @@ -0,0 +1,49 @@ +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#text_obj#targets#enabled() abort " {{{1 + return exists('g:loaded_targets') + \ && ( (type(g:loaded_targets) == type(0) && g:loaded_targets) + \ || (type(g:loaded_targets) == type('') && !empty(g:loaded_targets))) + \ && ( g:vimtex_text_obj_variant ==# 'auto' + \ || g:vimtex_text_obj_variant ==# 'targets') +endfunction + +" }}}1 +function! vimtex#text_obj#targets#init() abort " {{{1 + let g:vimtex_text_obj_variant = 'targets' + + " Create intermediate mappings + omap <expr> <plug>(vimtex-targets-i) targets#e('o', 'i', 'i') + xmap <expr> <plug>(vimtex-targets-i) targets#e('x', 'i', 'i') + omap <expr> <plug>(vimtex-targets-a) targets#e('o', 'a', 'a') + xmap <expr> <plug>(vimtex-targets-a) targets#e('x', 'a', 'a') + + augroup vimtex_targets + autocmd! + autocmd User targets#sources call s:init_sources() + autocmd User targets#mappings#plugin call s:init_mappings() + augroup END +endfunction + +" }}}1 + +function! s:init_mappings() abort " {{{1 + call targets#mappings#extend({'e': {'tex_env': [{}]}}) + call targets#mappings#extend({'c': {'tex_cmd': [{}]}}) +endfunction + +" }}}1 +function! s:init_sources() abort " {{{1 + call targets#sources#register('tex_env', function('vimtex#text_obj#envtargets#new')) + call targets#sources#register('tex_cmd', function('vimtex#text_obj#cmdtargets#new')) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/toc.vim b/autoload/vimtex/toc.vim new file mode 100644 index 00000000..ac660112 --- /dev/null +++ b/autoload/vimtex/toc.vim @@ -0,0 +1,801 @@ +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#toc#init_buffer() abort " {{{1 + if !g:vimtex_toc_enabled | return | endif + + command! -buffer VimtexTocOpen call b:vimtex.toc.open() + command! -buffer VimtexTocToggle call b:vimtex.toc.toggle() + + nnoremap <buffer> <plug>(vimtex-toc-open) :call b:vimtex.toc.open()<cr> + nnoremap <buffer> <plug>(vimtex-toc-toggle) :call b:vimtex.toc.toggle()<cr> +endfunction + +" }}}1 +function! vimtex#toc#init_state(state) abort " {{{1 + if !g:vimtex_toc_enabled | return | endif + + let a:state.toc = vimtex#toc#new() +endfunction + +" }}}1 + +function! vimtex#toc#new(...) abort " {{{1 + return extend( + \ deepcopy(s:toc), + \ vimtex#util#extend_recursive( + \ deepcopy(g:vimtex_toc_config), + \ a:0 > 0 ? a:1 : {})) +endfunction + +" }}}1 +function! vimtex#toc#get_entries() abort " {{{1 + if !has_key(b:vimtex, 'toc') | return [] | endif + + return b:vimtex.toc.get_entries(0) +endfunction + +" }}}1 +function! vimtex#toc#refresh() abort " {{{1 + if has_key(b:vimtex, 'toc') + call b:vimtex.toc.get_entries(1) + endif +endfunction + +" }}}1 + +let s:toc = {} + +" +" Open and close TOC window +" +function! s:toc.open() abort dict " {{{1 + if self.is_open() | return | endif + + if has_key(self, 'layers') + for l:key in keys(self.layer_status) + let self.layer_status[l:key] = index(self.layers, l:key) >= 0 + endfor + endif + + let self.calling_file = expand('%:p') + let self.calling_line = line('.') + + call self.get_entries(0) + + if self.mode > 1 + call setloclist(0, map(filter(deepcopy(self.entries), 'v:val.active'), '{ + \ ''lnum'': v:val.line, + \ ''filename'': v:val.file, + \ ''text'': v:val.title, + \}')) + try + call setloclist(0, [], 'r', {'title': self.name}) + catch + endtry + if self.mode == 4 | lopen | endif + endif + + if self.mode < 3 + call self.create() + endif +endfunction + +" }}}1 +function! s:toc.is_open() abort dict " {{{1 + return bufwinnr(bufnr(self.name)) >= 0 +endfunction + +" }}}1 +function! s:toc.toggle() abort dict " {{{1 + if self.is_open() + call self.close() + else + call self.open() + if has_key(self, 'prev_winid') + call win_gotoid(self.prev_winid) + endif + endif +endfunction + +" }}}1 +function! s:toc.close() abort dict " {{{1 + let self.fold_level = &l:foldlevel + + if self.resize + silent exe 'set columns -=' . self.split_width + endif + + if self.split_pos ==# 'full' + silent execute 'buffer' self.prev_bufnr + else + silent execute 'bwipeout' bufnr(self.name) + endif +endfunction + +" }}}1 +function! s:toc.goto() abort dict " {{{1 + if self.is_open() + let l:prev_winid = win_getid() + silent execute bufwinnr(bufnr(self.name)) . 'wincmd w' + let b:toc.prev_winid = l:prev_winid + endif +endfunction + +" }}}1 + +" +" Get the TOC entries +" +function! s:toc.get_entries(force) abort dict " {{{1 + if has_key(self, 'entries') && !self.refresh_always && !a:force + return self.entries + endif + + let self.entries = vimtex#parser#toc() + let self.topmatters = vimtex#parser#toc#get_topmatters() + + " + " Sort todo entries + " + if self.todo_sorted + let l:todos = filter(copy(self.entries), 'v:val.type ==# ''todo''') + for l:t in l:todos[1:] + let l:t.level = 1 + endfor + call filter(self.entries, 'v:val.type !=# ''todo''') + let self.entries = l:todos + self.entries + endif + + " + " Add hotkeys to entries + " + if self.hotkeys_enabled + let k = strwidth(self.hotkeys) + let n = len(self.entries) + let m = len(s:base(n, k)) + let i = 0 + for entry in self.entries + let keys = map(s:base(i, k), 'strcharpart(self.hotkeys, v:val, 1)') + let keys = repeat([self.hotkeys[0]], m - len(keys)) + keys + let i+=1 + let entry.num = i + let entry.hotkey = join(keys, '') + endfor + endif + + " + " Apply active layers + " + for entry in self.entries + let entry.active = self.layer_status[entry.type] + endfor + + " + " Refresh if wanted + " + if a:force && self.is_open() + call self.refresh() + endif + + return self.entries +endfunction + +" }}}1 +function! s:toc.get_visible_entries() abort dict " {{{1 + return filter(deepcopy(get(self, 'entries', [])), 'self.entry_is_visible(v:val)') +endfunction + +" }}}1 +function! s:toc.entry_is_visible(entry) abort " {{{1 + return get(a:entry, 'active', 1) && !get(a:entry, 'hidden') + \ && (a:entry.type !=# 'content' || a:entry.level <= self.tocdepth) +endfunction + +" }}}1 + +" +" Creating, refreshing and filling the buffer +" +function! s:toc.create() abort dict " {{{1 + let l:bufnr = bufnr('') + let l:winid = win_getid() + let l:vimtex = get(b:, 'vimtex', {}) + let l:vimtex_syntax = get(b:, 'vimtex_syntax', {}) + + if self.split_pos ==# 'full' + silent execute 'edit' escape(self.name, ' ') + else + if self.resize + silent exe 'set columns +=' . self.split_width + endif + silent execute + \ self.split_pos self.split_width + \ 'new' escape(self.name, ' ') + endif + + let self.prev_bufnr = l:bufnr + let self.prev_winid = l:winid + let b:toc = self + let b:vimtex = l:vimtex + let b:vimtex_syntax = l:vimtex_syntax + + setlocal bufhidden=wipe + setlocal buftype=nofile + setlocal concealcursor=nvic + setlocal conceallevel=2 + setlocal cursorline + setlocal nobuflisted + setlocal nolist + setlocal nospell + setlocal noswapfile + setlocal nowrap + setlocal tabstop=8 + + if self.hide_line_numbers + setlocal nonumber + setlocal norelativenumber + endif + + call self.refresh() + call self.set_syntax() + + if self.fold_enable + let self.foldexpr = function('s:foldexpr') + let self.foldtext = function('s:foldtext') + setlocal foldmethod=expr + setlocal foldexpr=b:toc.foldexpr(v:lnum) + setlocal foldtext=b:toc.foldtext() + let &l:foldlevel = get(self, 'fold_level', + \ (self.fold_level_start > 0 + \ ? self.fold_level_start + \ : self.tocdepth)) + endif + + nnoremap <silent><nowait><buffer><expr> gg b:toc.show_help ? 'gg}}j' : 'gg' + nnoremap <silent><nowait><buffer> <esc>OA k + nnoremap <silent><nowait><buffer> <esc>OB j + nnoremap <silent><nowait><buffer> <esc>OC k + nnoremap <silent><nowait><buffer> <esc>OD j + nnoremap <silent><nowait><buffer> q :call b:toc.close()<cr> + nnoremap <silent><nowait><buffer> <esc> :call b:toc.close()<cr> + nnoremap <silent><nowait><buffer> <space> :call b:toc.activate_current(0)<cr> + nnoremap <silent><nowait><buffer> <2-leftmouse> :call b:toc.activate_current(0)<cr> + nnoremap <silent><nowait><buffer> <cr> :call b:toc.activate_current(1)<cr> + nnoremap <buffer><nowait><silent> h :call b:toc.toggle_help()<cr> + nnoremap <buffer><nowait><silent> f :call b:toc.filter()<cr> + nnoremap <buffer><nowait><silent> F :call b:toc.clear_filter()<cr> + nnoremap <buffer><nowait><silent> s :call b:toc.toggle_numbers()<cr> + nnoremap <buffer><nowait><silent> t :call b:toc.toggle_sorted_todos()<cr> + nnoremap <buffer><nowait><silent> r :call b:toc.get_entries(1)<cr> + nnoremap <buffer><nowait><silent> - :call b:toc.decrease_depth()<cr> + nnoremap <buffer><nowait><silent> + :call b:toc.increase_depth()<cr> + + for [type, key] in items(self.layer_keys) + execute printf( + \ 'nnoremap <buffer><nowait><silent> %s' + \ . ' :call b:toc.toggle_type(''%s'')<cr>', + \ key, type) + endfor + + if self.hotkeys_enabled + for entry in self.entries + execute printf( + \ 'nnoremap <buffer><nowait><silent> %s%s' + \ . ' :call b:toc.activate_hotkey(''%s'')<cr>', + \ self.hotkeys_leader, entry.hotkey, entry.hotkey) + endfor + endif + + " Jump to closest index + call vimtex#pos#set_cursor(self.get_closest_index()) + + if exists('#User#VimtexEventTocCreated') + doautocmd <nomodeline> User VimtexEventTocCreated + endif +endfunction + +" }}}1 +function! s:toc.refresh() abort dict " {{{1 + let l:toc_winnr = bufwinnr(bufnr(self.name)) + let l:buf_winnr = bufwinnr(bufnr('')) + + if l:toc_winnr < 0 + return + elseif l:buf_winnr != l:toc_winnr + silent execute l:toc_winnr . 'wincmd w' + endif + + call self.position_save() + setlocal modifiable + silent %delete _ + + call self.print_help() + call self.print_entries() + + 0delete _ + setlocal nomodifiable + call self.position_restore() + + if l:buf_winnr != l:toc_winnr + silent execute l:buf_winnr . 'wincmd w' + endif +endfunction + +" }}}1 +function! s:toc.set_syntax() abort dict "{{{1 + syntax clear + + if self.show_help + execute 'syntax match VimtexTocHelp' + \ '/^\%<' . self.help_nlines . 'l.*/' + \ 'contains=VimtexTocHelpKey,VimtexTocHelpLayerOn,VimtexTocHelpLayerOff' + + syntax match VimtexTocHelpKey /<\S*>/ contained + syntax match VimtexTocHelpKey /^\s*[-+<>a-zA-Z\/]\+\ze\s/ contained + \ contains=VimtexTocHelpKeySeparator + syntax match VimtexTocHelpKey /^Layers:\s*\zs[-+<>a-zA-Z\/]\+/ contained + syntax match VimtexTocHelpKeySeparator /\// contained + + syntax match VimtexTocHelpLayerOn /\w\++/ contained + \ contains=VimtexTocHelpConceal + syntax match VimtexTocHelpLayerOff /(hidden)/ contained + syntax match VimtexTocHelpLayerOff /\w\+-/ contained + \ contains=VimtexTocHelpConceal + syntax match VimtexTocHelpConceal /[+-]/ contained conceal + + highlight link VimtexTocHelpKeySeparator VimtexTocHelp + endif + + syntax match VimtexTocNum /\v(([A-Z]+>|\d+)(\.\d+)*)?\s*/ contained + execute 'syntax match VimtexTocTodo' + \ '/\v\s\zs%(' . toupper(join(g:vimtex_toc_todo_keywords, '|')) . '): /' + \ 'contained' + syntax match VimtexTocHotkey /\[[^]]\+\]/ contained + + syntax match VimtexTocSec0 /^L0.*/ contains=@VimtexTocStuff + syntax match VimtexTocSec1 /^L1.*/ contains=@VimtexTocStuff + syntax match VimtexTocSec2 /^L2.*/ contains=@VimtexTocStuff + syntax match VimtexTocSec3 /^L3.*/ contains=@VimtexTocStuff + syntax match VimtexTocSec4 /^L[4-9].*/ contains=@VimtexTocStuff + syntax match VimtexTocSecLabel /^L\d / contained conceal + \ nextgroup=VimtexTocNum + syntax cluster VimtexTocStuff + \ contains=VimtexTocSecLabel,VimtexTocHotkey,VimtexTocTodo,@Tex + + syntax match VimtexTocIncl /\v^L\d (\[i\])?\s*(\[\w+\] )?\w+ incl:/ + \ contains=VimtexTocSecLabel,VimtexTocHotkey + \ nextgroup=VimtexTocInclPath + syntax match VimtexTocInclPath /.*/ contained + + syntax match VimtexTocLabelsSecs /\v^L\d \s*(\[\w+\] )?(chap|sec):.*$/ + \ contains=VimtexTocSecLabel,VimtexTocHotkey + syntax match VimtexTocLabelsEq /\v^L\d \s*(\[\w+\] )?eq:.*$/ + \ contains=VimtexTocSecLabel,VimtexTocHotkey + syntax match VimtexTocLabelsFig /\v^L\d \s*(\[\w+\] )?fig:.*$/ + \ contains=VimtexTocSecLabel,VimtexTocHotkey + syntax match VimtexTocLabelsTab /\v^L\d \s*(\[\w+\] )?tab:.*$/ + \ contains=VimtexTocSecLabel,VimtexTocHotkey +endfunction + +" }}}1 + +" +" Print the TOC entries +" +function! s:toc.print_help() abort dict " {{{1 + let self.help_nlines = 0 + if !self.show_help | return | endif + + let help_text = [ + \ '<Esc>/q Close', + \ '<Space> Jump', + \ '<Enter> Jump and close', + \ ' r Refresh', + \ ' h Toggle help text', + \ ' t Toggle sorted TODOs', + \ ' -/+ Decrease/increase ToC depth (for content layer)', + \ ' f/F Apply/clear filter', + \] + + if self.layer_status.content + call add(help_text, ' s Hide numbering') + endif + call add(help_text, '') + + let l:first = 1 + let l:frmt = printf('%%-%ds', + \ max(map(values(self.layer_keys), 'strlen(v:val)')) + 2) + for [layer, status] in items(self.layer_status) + call add(help_text, + \ (l:first ? 'Layers: ' : ' ') + \ . printf(l:frmt, self.layer_keys[layer]) + \ . layer . (status ? '+' : '- (hidden)')) + let l:first = 0 + endfor + + call append('$', help_text) + call append('$', '') + + let self.help_nlines += len(help_text) + 1 +endfunction + +" }}}1 +function! s:toc.print_entries() abort dict " {{{1 + call self.set_number_format() + + for entry in self.get_visible_entries() + call self.print_entry(entry) + endfor +endfunction + +" }}}1 +function! s:toc.print_entry(entry) abort dict " {{{1 + let output = 'L' . a:entry.level . ' ' + if self.show_numbers + let number = a:entry.level >= self.tocdepth + 2 ? '' + \ : strpart(self.print_number(a:entry.number), + \ 0, self.number_width - 1) + let output .= printf(self.number_format, number) + endif + + if self.hotkeys_enabled + let output .= printf('[%S] ', a:entry.hotkey) + endif + + let output .= a:entry.title + + call append('$', output) +endfunction + +" }}}1 +function! s:toc.print_number(number) abort dict " {{{1 + if empty(a:number) | return '' | endif + if type(a:number) == type('') | return a:number | endif + + if get(a:number, 'part_toggle') + return s:int_to_roman(a:number.part) + endif + + let number = [ + \ a:number.chapter, + \ a:number.section, + \ a:number.subsection, + \ a:number.subsubsection, + \ a:number.subsubsubsection, + \ ] + + " Remove unused parts + while len(number) > 0 && number[0] == 0 + call remove(number, 0) + endwhile + while len(number) > 0 && number[-1] == 0 + call remove(number, -1) + endwhile + + " Change numbering in frontmatter, appendix, and backmatter + if self.topmatters > 1 + \ && (a:number.frontmatter || a:number.backmatter) + return '' + elseif a:number.appendix + let number[0] = nr2char(number[0] + 64) + endif + + return join(number, '.') +endfunction + +" }}}1 +function! s:toc.set_number_format() abort dict " {{{1 + let number_width = 0 + for entry in self.get_visible_entries() + let number_width = max([number_width, strlen(self.print_number(entry.number)) + 1]) + endfor + + let self.number_width = self.layer_status.content + \ ? max([0, min([2*(self.tocdepth + 2), number_width])]) + \ : 0 + let self.number_format = '%-' . self.number_width . 's' +endfunction + +" }}}1 + +" +" Interactions with TOC buffer/window +" +function! s:toc.activate_current(close_after) abort dict "{{{1 + let n = vimtex#pos#get_cursor_line() - 1 + if n < self.help_nlines | return {} | endif + + let l:count = 0 + for l:entry in self.get_visible_entries() + if l:count == n - self.help_nlines + return self.activate_entry(l:entry, a:close_after) + endif + let l:count += 1 + endfor + + return {} +endfunction + +" }}}1 +function! s:toc.activate_hotkey(key) abort dict "{{{1 + for entry in self.entries + if entry.hotkey ==# a:key + return self.activate_entry(entry, 1) + endif + endfor + + return {} +endfunction + +" }}}1 +function! s:toc.activate_entry(entry, close_after) abort dict "{{{1 + let self.prev_index = vimtex#pos#get_cursor_line() + let l:vimtex_main = get(b:vimtex, 'tex', '') + + " Save toc winnr info for later use + let toc_winnr = winnr() + + " Return to calling window + call win_gotoid(self.prev_winid) + + " Get buffer number, add buffer if necessary + let bnr = bufnr(a:entry.file) + if bnr == -1 + execute 'badd ' . fnameescape(a:entry.file) + let bnr = bufnr(a:entry.file) + endif + + " Set bufferopen command + " The point here is to use existing open buffer if the user has turned on + " the &switchbuf option to either 'useopen' or 'usetab' + let cmd = 'buffer! ' + if &switchbuf =~# 'usetab' + for i in range(tabpagenr('$')) + if index(tabpagebuflist(i + 1), bnr) >= 0 + let cmd = 'sbuffer! ' + break + endif + endfor + elseif &switchbuf =~# 'useopen' + if bufwinnr(bnr) > 0 + let cmd = 'sbuffer! ' + endif + endif + + " Open file buffer + execute 'keepalt' cmd bnr + + " Go to entry line + if has_key(a:entry, 'line') + call vimtex#pos#set_cursor(a:entry.line, 0) + endif + + " If relevant, enable vimtex stuff + if get(a:entry, 'link', 0) && !empty(l:vimtex_main) + let b:vimtex_main = l:vimtex_main + call vimtex#init() + endif + + " Ensure folds are opened + normal! zv + + " Keep or close toc window (based on options) + if a:close_after && self.split_pos !=# 'full' + call self.close() + else + " Return to toc window + execute toc_winnr . 'wincmd w' + endif + + " Allow user entry points through autocmd events + if exists('#User#VimtexEventTocActivated') + doautocmd <nomodeline> User VimtexEventTocActivated + endif +endfunction + +" }}}1 +function! s:toc.toggle_help() abort dict "{{{1 + let l:pos = vimtex#pos#get_cursor() + if self.show_help + let l:pos[1] -= self.help_nlines + call vimtex#pos#set_cursor(l:pos) + endif + + let self.show_help = self.show_help ? 0 : 1 + call self.refresh() + call self.set_syntax() + + if self.show_help + let l:pos[1] += self.help_nlines + call vimtex#pos#set_cursor(l:pos) + endif +endfunction + +" }}}1 +function! s:toc.toggle_numbers() abort dict "{{{1 + let self.show_numbers = self.show_numbers ? 0 : 1 + call self.refresh() +endfunction + +" }}}1 +function! s:toc.toggle_sorted_todos() abort dict "{{{1 + let self.todo_sorted = self.todo_sorted ? 0 : 1 + call self.get_entries(1) + call vimtex#pos#set_cursor(self.get_closest_index()) +endfunction + +" }}}1 +function! s:toc.toggle_type(type) abort dict "{{{1 + let self.layer_status[a:type] = !self.layer_status[a:type] + for entry in self.entries + if entry.type ==# a:type + let entry.active = self.layer_status[a:type] + endif + endfor + call self.refresh() +endfunction + +" }}}1 +function! s:toc.decrease_depth() abort dict "{{{1 + let self.tocdepth = max([self.tocdepth - 1, -2]) + call self.refresh() +endfunction + +" }}}1 +function! s:toc.increase_depth() abort dict "{{{1 + let self.tocdepth = min([self.tocdepth + 1, 5]) + call self.refresh() +endfunction + +" }}}1 +function! s:toc.filter() dict abort "{{{1 + let re_filter = input('filter entry title by: ') + for entry in self.entries + let entry.hidden = get(entry, 'hidden') || entry.title !~# re_filter + endfor + call self.refresh() +endfunction + +" }}}1 +function! s:toc.clear_filter() dict abort "{{{1 + for entry in self.entries + let entry.hidden = 0 + endfor + call self.refresh() +endfunction + +" }}}1 + +" +" Utility functions +" +function! s:toc.get_closest_index() abort dict " {{{1 + let l:calling_rank = 0 + let l:not_found = 1 + for [l:file, l:lnum, l:line] in vimtex#parser#tex(b:vimtex.tex) + let l:calling_rank += 1 + if l:file ==# self.calling_file && l:lnum >= self.calling_line + let l:not_found = 0 + break + endif + endfor + + if l:not_found + return [0, get(self, 'prev_index', self.help_nlines + 1), 0, 0] + endif + + let l:index = 0 + let l:dist = 0 + let l:closest_index = 1 + let l:closest_dist = 10000 + for l:entry in self.get_visible_entries() + let l:index += 1 + let l:dist = l:calling_rank - entry.rank + + if l:dist >= 0 && l:dist < l:closest_dist + let l:closest_dist = l:dist + let l:closest_index = l:index + endif + endfor + + return [0, l:closest_index + self.help_nlines, 0, 0] +endfunction + +" }}}1 +function! s:toc.position_save() abort dict " {{{1 + let self.position = vimtex#pos#get_cursor() +endfunction + +" }}}1 +function! s:toc.position_restore() abort dict " {{{1 + if self.position[1] <= self.help_nlines + let self.position[1] = self.help_nlines + 1 + endif + call vimtex#pos#set_cursor(self.position) +endfunction + +" }}}1 + + +function! s:foldexpr(lnum) abort " {{{1 + let pline = getline(a:lnum - 1) + let cline = getline(a:lnum) + let nline = getline(a:lnum + 1) + let l:pn = matchstr(pline, '^L\zs\d') + let l:cn = matchstr(cline, '^L\zs\d') + let l:nn = matchstr(nline, '^L\zs\d') + + " Don't fold options + if cline =~# '^\s*$' + return 0 + endif + + if l:nn > l:cn + return '>' . l:nn + endif + + if l:cn < l:pn + return l:cn + endif + + return '=' +endfunction + +" }}}1 +function! s:foldtext() abort " {{{1 + let l:line = getline(v:foldstart)[3:] + if b:toc.todo_sorted + \ && l:line =~# '\v%(' . join(g:vimtex_toc_todo_keywords, '|') . ')' + return substitute(l:line, '\w+\zs:.*', 's', '') + else + return l:line + endif +endfunction + +" }}}1 + +function! s:int_to_roman(number) abort " {{{1 + let l:number = a:number + let l:result = '' + for [l:val, l:romn] in [ + \ ['1000', 'M'], + \ ['900', 'CM'], + \ ['500', 'D'], + \ ['400', 'CD' ], + \ ['100', 'C'], + \ ['90', 'XC'], + \ ['50', 'L'], + \ ['40', 'XL'], + \ ['10', 'X'], + \ ['9', 'IX'], + \ ['5', 'V'], + \ ['4', 'IV'], + \ ['1', 'I'], + \] + while l:number >= l:val + let l:number -= l:val + let l:result .= l:romn + endwhile + endfor + + return l:result +endfunction + +" }}}1 +function! s:base(n, k) abort " {{{1 + if a:n < a:k + return [a:n] + else + return add(s:base(a:n/a:k, a:k), a:n % a:k) + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/util.vim b/autoload/vimtex/util.vim new file mode 100644 index 00000000..eccf466e --- /dev/null +++ b/autoload/vimtex/util.vim @@ -0,0 +1,273 @@ +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#util#command(cmd) abort " {{{1 + let l:a = @a + try + silent! redir @a + silent! execute a:cmd + redir END + finally + let l:res = @a + let @a = l:a + return split(l:res, "\n") + endtry +endfunction + +" }}}1 +function! vimtex#util#flatten(list) abort " {{{1 + let l:result = [] + + for l:element in a:list + if type(l:element) == type([]) + call extend(l:result, vimtex#util#flatten(l:element)) + else + call add(l:result, l:element) + endif + unlet l:element + endfor + + return l:result +endfunction + +" }}}1 +function! vimtex#util#get_os() abort " {{{1 + if has('win32') || has('win32unix') + return 'win' + elseif has('unix') + if has('mac') || system('uname') =~# 'Darwin' + return 'mac' + else + return 'linux' + endif + endif +endfunction + +" }}}1 +function! vimtex#util#in_comment(...) abort " {{{1 + return call('vimtex#util#in_syntax', ['texComment'] + a:000) +endfunction + +" }}}1 +function! vimtex#util#in_mathzone(...) abort " {{{1 + return call('vimtex#util#in_syntax', ['texMathZone'] + a:000) +endfunction + +" }}}1 +function! vimtex#util#in_syntax(name, ...) abort " {{{1 + + " Usage: vimtex#util#in_syntax(name, [line, col]) + + " Get position and correct it if necessary + let l:pos = a:0 > 0 ? [a:1, a:2] : [line('.'), col('.')] + if mode() ==# 'i' + let l:pos[1] -= 1 + endif + call map(l:pos, 'max([v:val, 1])') + + " Check syntax at position + return match(map(synstack(l:pos[0], l:pos[1]), + \ "synIDattr(v:val, 'name')"), + \ '^' . a:name) >= 0 +endfunction + +" }}}1 +function! vimtex#util#extend_recursive(dict1, dict2, ...) abort " {{{1 + let l:option = a:0 > 0 ? a:1 : 'force' + if index(['force', 'keep', 'error'], l:option) < 0 + throw 'E475: Invalid argument: ' . l:option + endif + + for [l:key, l:value] in items(a:dict2) + if !has_key(a:dict1, l:key) + let a:dict1[l:key] = l:value + elseif type(l:value) == type({}) + call vimtex#util#extend_recursive(a:dict1[l:key], l:value, l:option) + elseif l:option ==# 'error' + throw 'E737: Key already exists: ' . l:key + elseif l:option ==# 'force' + let a:dict1[l:key] = l:value + endif + unlet l:value + endfor + + return a:dict1 +endfunction + +" }}}1 +function! vimtex#util#shellescape(cmd) abort " {{{1 + " + " Path used in "cmd" only needs to be enclosed by double quotes. + " shellescape() on Windows with "shellslash" set will produce a path + " enclosed by single quotes, which "cmd" does not recognize and reports an + " error. + " + if has('win32') + let l:shellslash = &shellslash + set noshellslash + let l:cmd = escape(shellescape(a:cmd), '\') + let &shellslash = l:shellslash + return l:cmd + else + return escape(shellescape(a:cmd), '\') + endif +endfunction + +" }}}1 +function! vimtex#util#tex2unicode(line) abort " {{{1 + " Convert compositions to unicode + let l:line = a:line + for [l:pat, l:symbol] in s:tex2unicode_list + let l:line = substitute(l:line, l:pat, l:symbol, 'g') + endfor + + " Remove the \IeC macro + let l:line = substitute(l:line, '\\IeC\s*{\s*\([^}]\{-}\)\s*}', '\1', 'g') + + return l:line +endfunction + +" +" Define list for converting compositions like \"u to unicode ű +let s:tex2unicode_list = [ + \ ['\\''A', 'Á'], + \ ['\\`A', 'À'], + \ ['\\^A', 'À'], + \ ['\\¨A', 'Ä'], + \ ['\\"A', 'Ä'], + \ ['\\''a', 'á'], + \ ['\\`a', 'à'], + \ ['\\^a', 'à'], + \ ['\\¨a', 'ä'], + \ ['\\"a', 'ä'], + \ ['\\\~a', 'ã'], + \ ['\\''E', 'É'], + \ ['\\`E', 'È'], + \ ['\\^E', 'Ê'], + \ ['\\¨E', 'Ë'], + \ ['\\"E', 'Ë'], + \ ['\\''e', 'é'], + \ ['\\`e', 'è'], + \ ['\\^e', 'ê'], + \ ['\\¨e', 'ë'], + \ ['\\"e', 'ë'], + \ ['\\''I', 'Í'], + \ ['\\`I', 'Î'], + \ ['\\^I', 'Ì'], + \ ['\\¨I', 'Ï'], + \ ['\\"I', 'Ï'], + \ ['\\''i', 'í'], + \ ['\\`i', 'î'], + \ ['\\^i', 'ì'], + \ ['\\¨i', 'ï'], + \ ['\\"i', 'ï'], + \ ['\\''i', 'í'], + \ ['\\''O', 'Ó'], + \ ['\\`O', 'Ò'], + \ ['\\^O', 'Ô'], + \ ['\\¨O', 'Ö'], + \ ['\\"O', 'Ö'], + \ ['\\''o', 'ó'], + \ ['\\`o', 'ò'], + \ ['\\^o', 'ô'], + \ ['\\¨o', 'ö'], + \ ['\\"o', 'ö'], + \ ['\\o', 'ø'], + \ ['\\''U', 'Ú'], + \ ['\\`U', 'Ù'], + \ ['\\^U', 'Û'], + \ ['\\¨U', 'Ü'], + \ ['\\"U', 'Ü'], + \ ['\\''u', 'ú'], + \ ['\\`u', 'ù'], + \ ['\\^u', 'û'], + \ ['\\¨u', 'ü'], + \ ['\\"u', 'ü'], + \ ['\\`N', 'Ǹ'], + \ ['\\\~N', 'Ñ'], + \ ['\\''n', 'ń'], + \ ['\\`n', 'ǹ'], + \ ['\\\~n', 'ñ'], + \] + +" }}}1 +function! vimtex#util#tex2tree(str) abort " {{{1 + let tree = [] + let i1 = 0 + let i2 = -1 + let depth = 0 + while i2 < len(a:str) + let i2 = match(a:str, '[{}]', i2 + 1) + if i2 < 0 + let i2 = len(a:str) + endif + if i2 >= len(a:str) || a:str[i2] ==# '{' + if depth == 0 + let item = substitute(strpart(a:str, i1, i2 - i1), + \ '^\s*\|\s*$', '', 'g') + if !empty(item) + call add(tree, item) + endif + let i1 = i2 + 1 + endif + let depth += 1 + else + let depth -= 1 + if depth == 0 + call add(tree, vimtex#util#tex2tree(strpart(a:str, i1, i2 - i1))) + let i1 = i2 + 1 + endif + endif + endwhile + return tree +endfunction + +" }}}1 +function! vimtex#util#trim(str) abort " {{{1 + if exists('*trim') | return trim(a:str) | endif + + let l:str = substitute(a:str, '^\s*', '', '') + let l:str = substitute(l:str, '\s*$', '', '') + + return l:str +endfunction + +" }}}1 +function! vimtex#util#uniq(list) abort " {{{1 + if exists('*uniq') | return uniq(a:list) | endif + if len(a:list) <= 1 | return a:list | endif + + let l:uniq = [a:list[0]] + for l:next in a:list[1:] + if l:uniq[-1] != l:next + call add(l:uniq, l:next) + endif + endfor + return l:uniq +endfunction + +" }}}1 +function! vimtex#util#uniq_unsorted(list) abort " {{{1 + if len(a:list) <= 1 | return a:list | endif + + let l:visited = {} + let l:result = [] + for l:x in a:list + let l:key = string(l:x) + if !has_key(l:visited, l:key) + let l:visited[l:key] = 1 + call add(l:result, l:x) + endif + endfor + + return l:result +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/view.vim b/autoload/vimtex/view.vim new file mode 100644 index 00000000..bebb7be9 --- /dev/null +++ b/autoload/vimtex/view.vim @@ -0,0 +1,115 @@ +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#view#init_buffer() abort " {{{1 + if !g:vimtex_view_enabled | return | endif + + command! -buffer -nargs=? -complete=file VimtexView + \ call b:vimtex.viewer.view(<q-args>) + if has_key(b:vimtex.viewer, 'reverse_search') + command! -buffer -nargs=* VimtexRSearch + \ call b:vimtex.viewer.reverse_search() + endif + + nnoremap <buffer> <plug>(vimtex-view) + \ :call b:vimtex.viewer.view('')<cr> + if has_key(b:vimtex.viewer, 'reverse_search') + nnoremap <buffer> <plug>(vimtex-reverse-search) + \ :call b:vimtex.viewer.reverse_search()<cr> + endif +endfunction + +" }}}1 +function! vimtex#view#init_state(state) abort " {{{1 + if !g:vimtex_view_enabled | return | endif + if has_key(a:state, 'viewer') | return | endif + + try + let a:state.viewer = vimtex#view#{g:vimtex_view_method}#new() + catch /E117/ + call vimtex#log#warning( + \ 'Invalid viewer: ' . g:vimtex_view_method, + \ 'Please see :h g:vimtex_view_method') + return + endtry + + " Make the following code more concise + let l:v = a:state.viewer + + " + " Add compiler callback to callback hooks (if it exists) + " + if exists('*l:v.compiler_callback') + \ && index(g:vimtex_compiler_callback_hooks, + \ 'b:vimtex.viewer.compiler_callback') == -1 + call add(g:vimtex_compiler_callback_hooks, + \ 'b:vimtex.viewer.compiler_callback') + endif + + " + " Create view and/or callback hooks (if they exist) + " + for l:point in ['view', 'callback'] + execute 'let l:hook = ''g:vimtex_view_' + \ . g:vimtex_view_method . '_hook_' . l:point . '''' + if exists(l:hook) + execute 'let hookfunc = ''*'' . ' . l:hook + if exists(hookfunc) + execute 'let l:v.hook_' . l:point . ' = function(' . l:hook . ')' + endif + endif + endfor +endfunction + +" }}}1 + +function! vimtex#view#reverse_goto(line, filename) abort " {{{1 + if mode() ==# 'i' | stopinsert | endif + + let l:file = resolve(a:filename) + + " Open file if necessary + if !bufexists(l:file) + if filereadable(l:file) + execute 'silent edit' l:file + else + call vimtex#log#warning("Reverse goto failed for file:\n" . l:file) + return + endif + endif + + " Go to correct buffer and line + let l:bufnr = bufnr(l:file) + let l:winnr = bufwinnr(l:file) + execute l:winnr >= 0 + \ ? l:winnr . 'wincmd w' + \ : 'buffer ' . l:bufnr + + execute 'normal!' a:line . 'G' + normal! zMzvzz + + " Attempt to focus Vim + if executable('pstree') && executable('xdotool') + let l:pids = reverse(split(system('pstree -s -p ' . getpid()), '\D\+')) + + let l:xwinids = [] + call map(copy(l:pids), 'extend(l:xwinids, reverse(split(' + \ . "system('xdotool search --onlyvisible --pid ' . v:val)" + \ . ')))') + call filter(l:xwinids, '!empty(v:val)') + + if !empty(l:xwinids) + call system('xdotool windowactivate ' . l:xwinids[0] . ' &') + call feedkeys("\<c-l>", 'tn') + endif + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/view/common.vim b/autoload/vimtex/view/common.vim new file mode 100644 index 00000000..ca39cf0a --- /dev/null +++ b/autoload/vimtex/view/common.vim @@ -0,0 +1,211 @@ +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#view#common#apply_common_template(viewer) abort " {{{1 + return extend(a:viewer, deepcopy(s:common_template)) +endfunction + +" }}}1 +function! vimtex#view#common#apply_xwin_template(class, viewer) abort " {{{1 + let a:viewer.class = a:class + let a:viewer.xwin_id = 0 + call extend(a:viewer, deepcopy(s:xwin_template)) + return a:viewer +endfunction + +" }}}1 +function! vimtex#view#common#not_readable(output) abort " {{{1 + if !filereadable(a:output) + call vimtex#log#warning('Viewer cannot read PDF file!', a:output) + return 1 + else + return 0 + endif +endfunction + +" }}}1 + +let s:common_template = {} + +function! s:common_template.out() dict abort " {{{1 + return g:vimtex_view_use_temp_files + \ ? b:vimtex.root . '/' . b:vimtex.name . '_vimtex.pdf' + \ : b:vimtex.out(1) +endfunction + +" }}}1 +function! s:common_template.synctex() dict abort " {{{1 + return fnamemodify(self.out(), ':r') . '.synctex.gz' +endfunction + +" }}}1 +function! s:common_template.copy_files() dict abort " {{{1 + if !g:vimtex_view_use_temp_files | return | endif + + " + " Copy pdf file + " + let l:out = self.out() + if getftime(b:vimtex.out()) > getftime(l:out) + call writefile(readfile(b:vimtex.out(), 'b'), l:out, 'b') + endif + + " + " Copy synctex file + " + let l:old = b:vimtex.ext('synctex.gz') + let l:new = self.synctex() + if getftime(l:old) > getftime(l:new) + call rename(l:old, l:new) + endif +endfunction + +" }}}1 +function! s:common_template.pprint_items() abort dict " {{{1 + let l:list = [] + + if has_key(self, 'xwin_id') + call add(l:list, ['xwin id', self.xwin_id]) + endif + + if has_key(self, 'process') + call add(l:list, ['process', self.process]) + endif + + for l:key in filter(keys(self), 'v:val =~# ''^cmd_''') + call add(l:list, [l:key, self[l:key]]) + endfor + + return l:list +endfunction + +" }}}1 + +let s:xwin_template = {} + +function! s:xwin_template.view(file) dict abort " {{{1 + if empty(a:file) + let outfile = self.out() + else + let outfile = a:file + endif + if vimtex#view#common#not_readable(outfile) + return + endif + + if self.xwin_exists() + call self.forward_search(outfile) + else + if g:vimtex_view_use_temp_files + call self.copy_files() + endif + call self.start(outfile) + endif + + if has_key(self, 'hook_view') + call self.hook_view() + endif +endfunction + +" }}}1 +function! s:xwin_template.xwin_get_id() dict abort " {{{1 + if !executable('xdotool') | return 0 | endif + if self.xwin_id > 0 | return self.xwin_id | endif + + " Allow some time for the viewer to start properly + sleep 500m + + " + " Get the window ID + " + let cmd = 'xdotool search --class ' . self.class + let xwin_ids = split(system(cmd), '\n') + if len(xwin_ids) == 0 + call vimtex#log#warning('Viewer cannot find ' . self.class . ' window ID!') + let self.xwin_id = 0 + else + let self.xwin_id = xwin_ids[-1] + endif + + return self.xwin_id +endfunction + +" }}}1 +function! s:xwin_template.xwin_exists() dict abort " {{{1 + if !executable('xdotool') | return 0 | endif + + " + " If xwin_id is already set, check if it still exists + " + if self.xwin_id > 0 + let cmd = 'xdotool search --class ' . self.class + if index(split(system(cmd), '\n'), self.xwin_id) < 0 + let self.xwin_id = 0 + endif + endif + + " + " If xwin_id is unset, check if matching viewer windows exist + " + if self.xwin_id == 0 + if has_key(self, 'get_pid') + let cmd = 'xdotool search' + \ . ' --all --pid ' . self.get_pid() + \ . ' --name ' . fnamemodify(self.out(), ':t') + let self.xwin_id = get(split(system(cmd), '\n'), 0) + else + let cmd = 'xdotool search --name ' . fnamemodify(self.out(), ':t') + let ids = split(system(cmd), '\n') + let ids_already_used = filter(map(deepcopy(vimtex#state#list_all()), + \ "get(get(v:val, 'viewer', {}), 'xwin_id')"), 'v:val > 0') + for id in ids + if index(ids_already_used, id) < 0 + let self.xwin_id = id + break + endif + endfor + endif + endif + + return self.xwin_id > 0 +endfunction + +" }}}1 +function! s:xwin_template.xwin_send_keys(keys) dict abort " {{{1 + if a:keys ==# '' || !executable('xdotool') || self.xwin_id <= 0 + return + endif + + let cmd = 'xdotool key --window ' . self.xwin_id + let cmd .= ' ' . a:keys + silent call system(cmd) +endfunction + +" }}}1 +function! s:xwin_template.move(arg) abort " {{{1 + if !executable('xdotool') || self.xwin_id <= 0 + return + endif + + let l:cmd = 'xdotool windowmove ' . self.xwin_get_id() . ' ' . a:arg + silent call system(l:cmd) +endfunction + +" }}}1 +function! s:xwin_template.resize(arg) abort " {{{1 + if !executable('xdotool') || self.xwin_id <= 0 + return + endif + + let l:cmd = 'xdotool windowsize ' . self.xwin_get_id() . ' ' . a:arg + silent call system(l:cmd) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/view/general.vim b/autoload/vimtex/view/general.vim new file mode 100644 index 00000000..701fe33c --- /dev/null +++ b/autoload/vimtex/view/general.vim @@ -0,0 +1,111 @@ +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#view#general#new() abort " {{{1 + " Check if the viewer is executable + " * split to ensure that we handle stuff like "gio open" + let l:exe = get(split(g:vimtex_view_general_viewer), 0, '') + if empty(l:exe) || !executable(l:exe) + call vimtex#log#warning( + \ 'Selected viewer is not executable!', + \ '- Selection: ' . g:vimtex_view_general_viewer . + \ '- Executable: ' . l:exe . + \ '- Please see :h g:vimtex_view_general_viewer') + return {} + endif + + " Start from standard template + let l:viewer = vimtex#view#common#apply_common_template(deepcopy(s:general)) + + " Add callback hook + if exists('g:vimtex_view_general_callback') + let l:viewer.compiler_callback = function(g:vimtex_view_general_callback) + endif + + return l:viewer +endfunction + +" }}}1 + +let s:general = { + \ 'name' : 'General' + \} + +function! s:general.view(file) dict abort " {{{1 + if empty(a:file) + let outfile = self.out() + + " Only copy files if they don't exist + if g:vimtex_view_use_temp_files + \ && vimtex#view#common#not_readable(outfile) + call self.copy_files() + endif + else + let outfile = a:file + endif + + " Update the path for Windows on cygwin + if executable('cygpath') + let outfile = join( + \ vimtex#process#capture('cygpath -aw "' . outfile . '"'), '') + endif + + if vimtex#view#common#not_readable(outfile) | return | endif + + " Parse options + let l:cmd = g:vimtex_view_general_viewer + let l:cmd .= ' ' . g:vimtex_view_general_options + + " Substitute magic patterns + let l:cmd = substitute(l:cmd, '@line', line('.'), 'g') + let l:cmd = substitute(l:cmd, '@col', col('.'), 'g') + let l:cmd = substitute(l:cmd, '@tex', + \ vimtex#util#shellescape(expand('%:p')), 'g') + let l:cmd = substitute(l:cmd, '@pdf', vimtex#util#shellescape(outfile), 'g') + + " Start the view process + let self.process = vimtex#process#start(l:cmd, {'silent': 0}) + + if has_key(self, 'hook_view') + call self.hook_view() + endif +endfunction + +" }}}1 +function! s:general.latexmk_append_argument() dict abort " {{{1 + if g:vimtex_view_use_temp_files + return ' -view=none' + else + let l:option = g:vimtex_view_general_viewer + if !empty(g:vimtex_view_general_options_latexmk) + let l:option .= ' ' + let l:option .= substitute(g:vimtex_view_general_options_latexmk, + \ '@line', line('.'), 'g') + endif + return vimtex#compiler#latexmk#wrap_option('pdf_previewer', l:option) + endif +endfunction + +" }}}1 +function! s:general.compiler_callback(status) dict abort " {{{1 + if !a:status && g:vimtex_view_use_temp_files < 2 + return + endif + + if g:vimtex_view_use_temp_files + call self.copy_files() + endif + + if has_key(self, 'hook_callback') + call self.hook_callback() + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/view/mupdf.vim b/autoload/vimtex/view/mupdf.vim new file mode 100644 index 00000000..95d3710c --- /dev/null +++ b/autoload/vimtex/view/mupdf.vim @@ -0,0 +1,186 @@ +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#view#mupdf#new() abort " {{{1 + " Check if the viewer is executable + if !executable('mupdf') + call vimtex#log#error( + \ 'MuPDF is not executable!', + \ '- vimtex viewer will not work!') + return {} + endif + + " Use the xwin template + return vimtex#view#common#apply_xwin_template('MuPDF', + \ vimtex#view#common#apply_common_template(deepcopy(s:mupdf))) +endfunction + +" }}}1 + +let s:mupdf = { + \ 'name': 'MuPDF', + \} + +function! s:mupdf.start(outfile) dict abort " {{{1 + let l:cmd = 'mupdf ' . g:vimtex_view_mupdf_options + \ . ' ' . vimtex#util#shellescape(a:outfile) + let self.process = vimtex#process#start(l:cmd) + + call self.xwin_get_id() + call self.xwin_send_keys(g:vimtex_view_mupdf_send_keys) + + if g:vimtex_view_forward_search_on_start + call self.forward_search(a:outfile) + endif +endfunction + +" }}}1 +function! s:mupdf.forward_search(outfile) dict abort " {{{1 + if !executable('xdotool') | return | endif + if !executable('synctex') | return | endif + + let self.cmd_synctex_view = 'synctex view -i ' + \ . (line('.') + 1) . ':' + \ . (col('.') + 1) . ':' + \ . vimtex#util#shellescape(expand('%:p')) + \ . ' -o ' . vimtex#util#shellescape(a:outfile) + \ . " | grep -m1 'Page:' | sed 's/Page://' | tr -d '\n'" + let self.page = system(self.cmd_synctex_view) + + if self.page > 0 + let l:cmd = 'xdotool' + \ . ' type --window ' . self.xwin_id + \ . ' "' . self.page . 'g"' + call vimtex#process#run(l:cmd) + let self.cmd_forward_search = l:cmd + endif + + call self.focus_viewer() +endfunction + +" }}}1 +function! s:mupdf.reverse_search() dict abort " {{{1 + if !executable('xdotool') | return | endif + if !executable('synctex') | return | endif + + let outfile = self.out() + if vimtex#view#common#not_readable(outfile) | return | endif + + if !self.xwin_exists() + call vimtex#log#warning('Reverse search failed (is MuPDF open?)') + return + endif + + " Get page number + let self.cmd_getpage = 'xdotool getwindowname ' . self.xwin_id + let self.cmd_getpage .= " | sed 's:.* - \\([0-9]*\\)/.*:\\1:'" + let self.cmd_getpage .= " | tr -d '\n'" + let self.page = system(self.cmd_getpage) + if self.page <= 0 | return | endif + + " Get file + let self.cmd_getfile = 'synctex edit ' + let self.cmd_getfile .= "-o \"" . self.page . ':288:108:' . outfile . "\"" + let self.cmd_getfile .= "| grep 'Input:' | sed 's/Input://' " + let self.cmd_getfile .= "| head -n1 | tr -d '\n' 2>/dev/null" + let self.file = system(self.cmd_getfile) + + " Get line + let self.cmd_getline = 'synctex edit ' + let self.cmd_getline .= "-o \"" . self.page . ':288:108:' . outfile . "\"" + let self.cmd_getline .= "| grep -m1 'Line:' | sed 's/Line://' " + let self.cmd_getline .= "| head -n1 | tr -d '\n'" + let self.line = system(self.cmd_getline) + + " Go to file and line + silent exec 'edit ' . fnameescape(self.file) + if self.line > 0 + silent exec ':' . self.line + " Unfold, move to top line to correspond to top pdf line, and go to end of + " line in case the corresponding pdf line begins on previous pdf page. + normal! zvztg_ + endif +endfunction + +" }}}1 +function! s:mupdf.compiler_callback(status) dict abort " {{{1 + if !a:status && g:vimtex_view_use_temp_files < 2 + return + endif + + if g:vimtex_view_use_temp_files + call self.copy_files() + endif + + if !filereadable(self.out()) | return | endif + + if g:vimtex_view_automatic + " + " Search for existing window created by latexmk + " It may be necessary to wait some time before it is opened and + " recognized. Sometimes it is very quick, other times it may take + " a second. This way, we don't block longer than necessary. + " + if !has_key(self, 'started_through_callback') + for l:dummy in range(30) + sleep 50m + if self.xwin_exists() | break | endif + endfor + endif + + if !self.xwin_exists() && !has_key(self, 'started_through_callback') + call self.start(self.out()) + let self.started_through_callback = 1 + endif + endif + + if g:vimtex_view_use_temp_files || get(b:vimtex.compiler, 'callback') + call self.xwin_send_keys('r') + endif + + if has_key(self, 'hook_callback') + call self.hook_callback() + endif +endfunction + +" }}}1 +function! s:mupdf.latexmk_append_argument() dict abort " {{{1 + if g:vimtex_view_use_temp_files + let cmd = ' -view=none' + else + let cmd = vimtex#compiler#latexmk#wrap_option('new_viewer_always', '0') + let cmd .= vimtex#compiler#latexmk#wrap_option('pdf_update_method', '2') + let cmd .= vimtex#compiler#latexmk#wrap_option('pdf_update_signal', 'SIGHUP') + let cmd .= vimtex#compiler#latexmk#wrap_option('pdf_previewer', + \ 'mupdf ' . g:vimtex_view_mupdf_options) + endif + + return cmd +endfunction + +" }}}1 +function! s:mupdf.focus_viewer() dict abort " {{{1 + if !executable('xdotool') | return | endif + + if self.xwin_id > 0 + silent call system('xdotool windowactivate ' . self.xwin_id . ' --sync') + silent call system('xdotool windowraise ' . self.xwin_id) + endif +endfunction + +" }}}1 +function! s:mupdf.focus_vim() dict abort " {{{1 + if !executable('xdotool') | return | endif + + silent call system('xdotool windowactivate ' . v:windowid . ' --sync') + silent call system('xdotool windowraise ' . v:windowid) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/view/skim.vim b/autoload/vimtex/view/skim.vim new file mode 100644 index 00000000..c3dc6dec --- /dev/null +++ b/autoload/vimtex/view/skim.vim @@ -0,0 +1,114 @@ +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#view#skim#new() abort " {{{1 + " Check if Skim is installed + let l:cmd = join([ + \ 'osascript -e ', + \ '''tell application "Finder" to POSIX path of ', + \ '(get application file id (id of application "Skim") as alias)''', + \]) + + if system(l:cmd) + call vimtex#log#error('Skim is not installed!') + return {} + endif + + return vimtex#view#common#apply_common_template(deepcopy(s:skim)) +endfunction + +" }}}1 + +let s:skim = { + \ 'name' : 'Skim', + \ 'startskim' : 'open -a Skim', + \} + +function! s:skim.view(file) dict abort " {{{1 + if empty(a:file) + let outfile = self.out() + + " Only copy files if they don't exist + if g:vimtex_view_use_temp_files + \ && vimtex#view#common#not_readable(outfile) + call self.copy_files() + endif + else + let outfile = a:file + endif + if vimtex#view#common#not_readable(outfile) | return | endif + + let l:cmd = join([ + \ 'osascript', + \ '-e ''set theLine to ' . line('.') . ' as integer''', + \ '-e ''set theFile to POSIX file "' . outfile . '"''', + \ '-e ''set thePath to POSIX path of (theFile as alias)''', + \ '-e ''set theSource to POSIX file "' . expand('%:p') . '"''', + \ '-e ''tell application "Skim"''', + \ '-e ''try''', + \ '-e ''set theDocs to get documents whose path is thePath''', + \ '-e ''if (count of theDocs) > 0 then revert theDocs''', + \ '-e ''end try''', + \ '-e ''open theFile''', + \ '-e ''tell front document to go to TeX line theLine from theSource', + \ g:vimtex_view_skim_reading_bar ? 'showing reading bar true''' : '''', + \ g:vimtex_view_skim_activate ? '-e ''activate''' : '', + \ '-e ''end tell''', + \]) + + let self.process = vimtex#process#start(l:cmd) + + if has_key(self, 'hook_view') + call self.hook_view() + endif +endfunction + +" }}}1 +function! s:skim.compiler_callback(status) dict abort " {{{1 + if !a:status && g:vimtex_view_use_temp_files < 2 + return + endif + + if g:vimtex_view_use_temp_files + call self.copy_files() + endif + + if !filereadable(self.out()) | return | endif + + let l:cmd = join([ + \ 'osascript', + \ '-e ''set theFile to POSIX file "' . self.out() . '"''', + \ '-e ''set thePath to POSIX path of (theFile as alias)''', + \ '-e ''tell application "Skim"''', + \ '-e ''try''', + \ '-e ''set theDocs to get documents whose path is thePath''', + \ '-e ''if (count of theDocs) > 0 then revert theDocs''', + \ '-e ''end try''', + \ '-e ''open theFile''', + \ '-e ''end tell''', + \]) + + let self.process = vimtex#process#start(l:cmd) + + if has_key(self, 'hook_callback') + call self.hook_callback() + endif +endfunction + +" }}}1 +function! s:skim.latexmk_append_argument() dict abort " {{{1 + if g:vimtex_view_use_temp_files || g:vimtex_view_automatic + return ' -view=none' + else + return vimtex#compiler#latexmk#wrap_option('pdf_previewer', self.startskim) + endif +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/view/zathura.vim b/autoload/vimtex/view/zathura.vim new file mode 100644 index 00000000..48e8e27a --- /dev/null +++ b/autoload/vimtex/view/zathura.vim @@ -0,0 +1,155 @@ +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#view#zathura#new() abort " {{{1 + " Check if the viewer is executable + if !executable('zathura') + call vimtex#log#error('Zathura is not executable!') + return {} + endif + + if executable('ldd') + let l:shared = split(system('ldd =zathura')) + if v:shell_error == 0 + \ && empty(filter(l:shared, 'v:val =~# ''libsynctex''')) + call vimtex#log#warning('Zathura is not linked to libsynctex!') + let s:zathura.has_synctex = 0 + endif + endif + + " Check if the xdotool is available + if !executable('xdotool') + call vimtex#log#warning('Zathura requires xdotool for forward search!') + endif + + " + " Use the xwin template + " + return vimtex#view#common#apply_xwin_template('Zathura', + \ vimtex#view#common#apply_common_template(deepcopy(s:zathura))) +endfunction + +" }}}1 + +let s:zathura = { + \ 'name' : 'Zathura', + \ 'has_synctex' : 1, + \} + +function! s:zathura.start(outfile) dict abort " {{{1 + let l:cmd = 'zathura' + if self.has_synctex + let l:cmd .= ' -x "' . g:vimtex_compiler_progname + \ . ' --servername ' . v:servername + \ . ' --remote-expr ' + \ . '\"vimtex#view#reverse_goto(%{line}, ''%{input}'')\""' + if g:vimtex_view_forward_search_on_start + let l:cmd .= ' --synctex-forward ' + \ . line('.') + \ . ':' . col('.') + \ . ':' . vimtex#util#shellescape(expand('%:p')) + endif + endif + let l:cmd .= ' ' . g:vimtex_view_zathura_options + let l:cmd .= ' ' . vimtex#util#shellescape(a:outfile) + let self.process = vimtex#process#start(l:cmd) + + call self.xwin_get_id() + let self.outfile = a:outfile +endfunction + +" }}}1 +function! s:zathura.forward_search(outfile) dict abort " {{{1 + if !self.has_synctex | return | endif + if !filereadable(self.synctex()) | return | endif + + let l:cmd = 'zathura --synctex-forward ' + let l:cmd .= line('.') + let l:cmd .= ':' . col('.') + let l:cmd .= ':' . vimtex#util#shellescape(expand('%:p')) + let l:cmd .= ' ' . vimtex#util#shellescape(a:outfile) + call vimtex#process#run(l:cmd) + let self.cmd_forward_search = l:cmd + let self.outfile = a:outfile +endfunction + +" }}}1 +function! s:zathura.compiler_callback(status) dict abort " {{{1 + if !a:status && g:vimtex_view_use_temp_files < 2 + return + endif + + if g:vimtex_view_use_temp_files + call self.copy_files() + endif + + if !filereadable(self.out()) | return | endif + + if g:vimtex_view_automatic + " + " Search for existing window created by latexmk + " It may be necessary to wait some time before it is opened and + " recognized. Sometimes it is very quick, other times it may take + " a second. This way, we don't block longer than necessary. + " + if !has_key(self, 'started_through_callback') + for l:dummy in range(30) + sleep 50m + if self.xwin_exists() | break | endif + endfor + endif + + if !self.xwin_exists() && !has_key(self, 'started_through_callback') + call self.start(self.out()) + let self.started_through_callback = 1 + endif + endif + + if has_key(self, 'hook_callback') + call self.hook_callback() + endif +endfunction + +" }}}1 +function! s:zathura.latexmk_append_argument() dict abort " {{{1 + if g:vimtex_view_use_temp_files + let cmd = ' -view=none' + else + let zathura = 'zathura ' . g:vimtex_view_zathura_options + if self.has_synctex + let zathura .= ' -x \"' . g:vimtex_compiler_progname + \ . ' --servername ' . v:servername + \ . ' --remote +\%{line} \%{input}\" \%S' + endif + + let cmd = vimtex#compiler#latexmk#wrap_option('new_viewer_always', '0') + let cmd .= vimtex#compiler#latexmk#wrap_option('pdf_previewer', zathura) + endif + + return cmd +endfunction + +" }}}1 +function! s:zathura.get_pid() dict abort " {{{1 + " First try to match full output file name + let cmd = 'pgrep -nf "zathura.*' + \ . escape(get(self, 'outfile', self.out()), '~\%.') . '"' + let pid = str2nr(system(cmd)[:-2]) + + " Now try to match correct servername as fallback + if empty(pid) + let cmd = 'pgrep -nf "zathura.+--servername ' . v:servername . '"' + let pid = str2nr(system(cmd)[:-2]) + endif + + return pid +endfunction + +" }}}1 + +endif |