Editor support generalized: shared core, language server, Vim and VS Code (from dev eb5baf9cbe59)
doc/edit/ now holds a shared Python implementation of the language's structural layer (klammertext_edit.py) and a dependency-free language server (klammertext_ls.py), with integrations for Emacs, Sublime Text, Vim, and Visual Studio Code. The editor test suite in tst/ covers the core's API and CLI, the language server protocol, the VS Code extension, headless Vim, and Emacs byte-equality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
299
doc/edit/vim/autoload/klammertext.vim
Normal file
299
doc/edit/vim/autoload/klammertext.vim
Normal file
@@ -0,0 +1,299 @@
|
||||
" 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
|
||||
Reference in New Issue
Block a user