Files
klammertext/doc/edit/vim/autoload/klammertext.vim

300 lines
9.0 KiB
VimL
Raw Normal View History

" autoload/klammertext.vim — implementation of the Klammertext commands.
"
" All the structure-aware work (indentation, table alignment, delimiter
" matching, diagnostics) is done by the shared Python core,
" klammertext_edit.py — the single implementation used by the Sublime,
" Vim, and VS Code integrations and by the language server. This file is
" glue: it locates the core, runs it (as a stdin/stdout filter, or
" in-process via +python3 where that is faster), and applies the results
" to the buffer.
"
" Configuration:
" g:klammertext_edit_py full path to klammertext_edit.py (overrides the
" search described below)
" g:klammertext_python the Python interpreter (default "python3")
" --- locating the shared core ---------------------------------------------
" Search order: an explicit g:klammertext_edit_py; a vendored copy at this
" plugin's root (the layout the editing zip installs); ../shared relative
" to the plugin root (the repository layout); $KLAMMERTEXT_HOME.
let s:plugin_root = expand('<sfile>:p:h:h')
function! s:EditScript() abort
if exists('g:klammertext_edit_py')
return g:klammertext_edit_py
endif
for candidate in [
\ s:plugin_root . '/klammertext_edit.py',
\ fnamemodify(s:plugin_root, ':h') . '/shared/klammertext_edit.py',
\ (empty($KLAMMERTEXT_HOME) ? '' :
\ $KLAMMERTEXT_HOME . '/doc/edit/shared/klammertext_edit.py')]
if !empty(candidate) && filereadable(candidate)
return candidate
endif
endfor
return ''
endfunction
function! s:Python() abort
return exists('g:klammertext_python') ? g:klammertext_python : 'python3'
endfunction
" Run the core CLI over the whole buffer. ARGS is the argument string
" (e.g. "indent 3-7"). Returns a dict {ok, lines, msg}: LINES is the
" transformed buffer (for the filter modes), MSG the stderr message.
function! s:RunCore(args) abort
let script = s:EditScript()
if empty(script)
return {'ok': 0, 'lines': [], 'msg':
\ 'Klammertext: cannot locate klammertext_edit.py'
\ . ' (set g:klammertext_edit_py)'}
endif
let errfile = tempname()
let cmd = s:Python() . ' ' . shellescape(script) . ' ' . a:args
\ . ' 2>' . shellescape(errfile)
let out = systemlist(cmd, getline(1, '$'))
let msg = filereadable(errfile) ? join(readfile(errfile), ' ') : ''
call delete(errfile)
if v:shell_error
return {'ok': 0, 'lines': [], 'msg':
\ empty(msg) ? 'Klammertext: the shared core failed' : msg}
endif
return {'ok': 1, 'lines': out, 'msg': msg}
endfunction
" Replace the buffer with LINES, touching only the lines that changed (so
" undo stays small and the cursor does not move). The transformations
" never add or remove lines; refuse if the count disagrees.
function! s:ApplyLines(lines) abort
if len(a:lines) != line('$')
echohl ErrorMsg
echo 'Klammertext: unexpected line count from the shared core'
echohl None
return 0
endif
let changed = 0
for i in range(1, line('$'))
if getline(i) !=# a:lines[i - 1]
call setline(i, a:lines[i - 1])
let changed += 1
endif
endfor
return changed
endfunction
function! s:CursorArgs() abort
let col = exists('*charcol') ? charcol('.') : col('.')
return line('.') . ' ' . col
endfunction
" --- the commands ----------------------------------------------------------
function! klammertext#Reindent(first, last) abort
let r = s:RunCore('indent ' . a:first . '-' . a:last)
if !r.ok
echohl ErrorMsg | echo r.msg | echohl None
return
endif
call s:ApplyLines(r.lines)
endfunction
function! klammertext#AlignTable() abort
let r = s:RunCore('align ' . s:CursorArgs())
if !r.ok
echohl ErrorMsg | echo r.msg | echohl None
return
endif
call s:ApplyLines(r.lines)
if !empty(r.msg)
echo 'Klammertext: ' . r.msg
endif
endfunction
function! klammertext#JumpToMatch() abort
let script = s:EditScript()
if empty(script)
echohl ErrorMsg
echo 'Klammertext: cannot locate klammertext_edit.py'
echohl None
return
endif
let out = systemlist(s:Python() . ' ' . shellescape(script)
\ . ' match ' . s:CursorArgs(), getline(1, '$'))
if v:shell_error || empty(out)
echohl ErrorMsg | echo 'Klammertext: the shared core failed' | echohl None
return
endif
let parts = split(out[0], ' ')
if parts[0] ==# 'none'
echo 'Klammertext: ' . join(parts[1:], ' ')
return
endif
" "match L C" or "mismatch L C message..."
let lnum = str2nr(parts[1])
let ccol = str2nr(parts[2])
if exists('*setcursorcharpos')
call setcursorcharpos(lnum, ccol)
else
call cursor(lnum, ccol)
endif
if parts[0] ==# 'mismatch'
echohl WarningMsg
echo 'Klammertext: ' . join(parts[3:], ' ')
echohl None
endif
endfunction
function! klammertext#Check() abort
let script = s:EditScript()
if empty(script)
echohl ErrorMsg
echo 'Klammertext: cannot locate klammertext_edit.py'
echohl None
return
endif
let out = systemlist(s:Python() . ' ' . shellescape(script) . ' check',
\ getline(1, '$'))
if v:shell_error
echohl ErrorMsg | echo 'Klammertext: the shared core failed' | echohl None
return
endif
let items = []
for line in out
let m = matchlist(line, '^\(\d\+\):\(\d\+\): \(.*\)$')
if !empty(m)
call add(items, {'bufnr': bufnr('%'), 'lnum': str2nr(m[1]),
\ 'col': str2nr(m[2]), 'text': m[3], 'type': 'E'})
endif
endfor
call setloclist(0, items, ' ')
call setloclist(0, [], 'a', {'title': 'Klammertext delimiter check'})
if empty(items)
lclose
echo 'Klammertext: no delimiter problems found'
else
lopen
endif
endfunction
" --- +python3: in-process indentexpr and live match highlighting ----------
" These load the shared core into Vim's embedded Python once, so the =
" operator and the per-cursor-move matcher run without spawning processes.
let s:py_ready = 0
function! s:PySetup() abort
if s:py_ready
return 1
endif
let script = s:EditScript()
if empty(script) || !has('python3')
return 0
endif
let g:klammertext_py_dir = fnamemodify(script, ':h')
py3 << EOF
import sys
import vim
_kt_dir = vim.eval('g:klammertext_py_dir')
if _kt_dir not in sys.path:
sys.path.insert(0, _kt_dir)
import klammertext_edit as _kt
def _kt_buffer_and_offset():
buf = vim.current.buffer
lines = buf[:]
s = '\n'.join(lines)
row, bytecol = vim.current.window.cursor # bytecol is 0-based bytes
line = lines[row - 1] if row <= len(lines) else ''
charcol = len(line.encode('utf-8')[:bytecol].decode('utf-8', 'replace'))
offset = sum(len(l) + 1 for l in lines[:row - 1]) + charcol
return s, lines, offset
def _kt_indent(lnum):
buf = vim.current.buffer
lines = buf[:]
s = '\n'.join(lines)
bol = sum(len(l) + 1 for l in lines[:lnum - 1])
col = _kt.target_column(s, bol)
return -1 if col is None else col
def _kt_pos(lines, offset):
"""(1-based line, 0-based char col, the line's text) for char OFFSET."""
line = 0
while line < len(lines) and offset > len(lines[line]):
offset -= len(lines[line]) + 1
line += 1
text = lines[line] if line < len(lines) else ''
return (line + 1, offset, text)
def _kt_match():
"""[] when not on a delimiter; else [mismatch, message,
l1, c1, len1, (l2, c2, len2)?] with byte columns for matchaddpos()."""
s, lines, offset = _kt_buffer_and_offset()
m = _kt.match_at(s, offset)
if m is None:
return []
out = [1 if m['mismatch'] else 0, m['message'] or '']
for tok in [m['token'], m['match_token']]:
if tok is None:
continue
line, ccol, text = _kt_pos(lines, tok[0])
bcol = len(text[:ccol].encode('utf-8')) + 1
blen = len(text[ccol:ccol + (tok[1] - tok[0])].encode('utf-8'))
out.extend([line, bcol, blen])
return out
EOF
let s:py_ready = 1
return 1
endfunction
function! klammertext#IndentExpr() abort
if !s:PySetup()
return -1
endif
return py3eval('_kt_indent(' . v:lnum . ')')
endfunction
function! s:ClearMatchHighlight() abort
if exists('w:klammertext_match_ids')
for id in w:klammertext_match_ids
silent! call matchdelete(id)
endfor
endif
let w:klammertext_match_ids = []
endfunction
function! s:UpdateMatchHighlight() abort
call s:ClearMatchHighlight()
let r = py3eval('_kt_match()')
if empty(r)
return
endif
let group = r[0] ? 'KlammertextMismatch' : 'KlammertextMatch'
let pos = []
let i = 2
while i + 3 <= len(r)
call add(pos, [r[i], r[i + 1], r[i + 2]])
let i += 3
endwhile
if !empty(pos)
call add(w:klammertext_match_ids, matchaddpos(group, pos))
endif
if r[0] && !empty(r[1])
echo 'Klammertext: ' . r[1]
endif
endfunction
function! klammertext#SetupMatchHighlight() abort
if !s:PySetup()
return
endif
hi def link KlammertextMatch MatchParen
hi def KlammertextMismatch guifg=#ff5555 gui=bold ctermfg=203 cterm=bold
augroup klammertextMatch
autocmd! * <buffer>
autocmd CursorMoved,CursorMovedI <buffer> call s:UpdateMatchHighlight()
autocmd BufLeave,WinLeave <buffer> call s:ClearMatchHighlight()
augroup END
endfunction