300 lines
9.0 KiB
VimL
300 lines
9.0 KiB
VimL
|
|
" 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
|