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:
2026-07-27 15:01:49 +02:00
parent 73ed7f3d5d
commit f855c5ccae
27 changed files with 3918 additions and 1170 deletions

View File

@@ -0,0 +1,179 @@
" vim_feature_test.vim — comprehensive checks of the Klammertext Vim plugin,
" beyond the fixture runs in editor_test.sh (which cover the Reindent and
" Align commands' output bytes). Run headlessly:
"
" KT_FIX=<fixture dir> KT_OUT=<result file> \
" vim -N -n -u NONE -i NONE -es --not-a-term \
" --cmd 'set rtp^=<doc/edit/vim>' -c 'source vim_feature_test.vim'
"
" What is checked:
" * ftdetect: .kt maps to the klammertext filetype, overriding the stock
" runtime's .kt -> kotlin mapping
" * syntax: token classes at known positions (three @-tiers open/close,
" removal line/block/file with markers, verbatim @code interior,
" ^-escapes, abbreviated @name-arg)
" * commands: KlammertextJumpToMatch (open->close, close->open,
" multibyte columns), KlammertextCheck (location list)
" * with +python3: 'indentexpr' drives the = operator (gg=G equals the
" expected fixture), and the CursorMoved match highlighter sets
" KlammertextMatch / KlammertextMismatch matches
"
" Writes PASS/FAIL lines to $KT_OUT; exits nonzero if anything failed.
let s:results = []
function! s:Check(name, ok, ...) abort
let detail = (a:ok || !a:0) ? '' : ' -- ' . string(a:1)
call add(s:results, (a:ok ? 'PASS ' : 'FAIL ') . a:name . detail)
endfunction
function! s:Syn(l, c) abort
return synIDattr(synID(a:l, a:c, 1), 'name')
endfunction
filetype plugin on
if has('syntax')
syntax enable
endif
" --- ftdetect: .kt is klammertext (the Kotlin override) --------------------
execute 'edit! ' . fnameescape($KT_FIX . '/indent_list.kt')
call s:Check('ftdetect .kt -> klammertext (overrides kotlin)',
\ &filetype ==# 'klammertext', &filetype)
" --- syntax token classes ---------------------------------------------------
if has('syntax')
enew!
call setline(1, [
\ '@i abc @ x',
\ '@@def : x @@',
\ '@@@target x @@@',
\ '# removed line',
\ '#[ rem ]# after',
\ '^@ escaped',
\ '@code x @ y code@',
\ 'name@ x',
\ '@name-arg x',
\ '## tail',
\ 'still removed'])
set filetype=klammertext
call s:Check('syntax loaded', b:current_syntax ==# 'klammertext')
call s:Check('app open @i', s:Syn(1, 1) ==# 'klammertextAppOpen', s:Syn(1, 1))
call s:Check('app bare close', s:Syn(1, 8) ==# 'klammertextAppClose', s:Syn(1, 8))
call s:Check('plain text', s:Syn(1, 10) ==# '', s:Syn(1, 10))
call s:Check('def open @@def', s:Syn(2, 1) ==# 'klammertextDefOpen', s:Syn(2, 1))
call s:Check('def bare close', s:Syn(2, 11) ==# 'klammertextDefClose', s:Syn(2, 11))
call s:Check('sys open @@@target', s:Syn(3, 1) ==# 'klammertextSysOpen', s:Syn(3, 1))
call s:Check('sys bare close', s:Syn(3, 13) ==# 'klammertextSysClose', s:Syn(3, 13))
call s:Check('line marker #', s:Syn(4, 1) ==# 'klammertextMarkerLine', s:Syn(4, 1))
call s:Check('line removed text', s:Syn(4, 5) ==# 'klammertextRemovedLine', s:Syn(4, 5))
call s:Check('block marker #[', s:Syn(5, 1) ==# 'klammertextMarker', s:Syn(5, 1))
call s:Check('block removed text', s:Syn(5, 5) ==# 'klammertextRemovedBlock', s:Syn(5, 5))
call s:Check('block marker ]#', s:Syn(5, 8) ==# 'klammertextMarker', s:Syn(5, 8))
call s:Check('text after block', s:Syn(5, 12) ==# '', s:Syn(5, 12))
call s:Check('escape ^@ consumed', s:Syn(6, 1) ==# 'klammertextEscape'
\ && s:Syn(6, 2) ==# 'klammertextEscape', s:Syn(6, 2))
call s:Check('verbatim open', s:Syn(7, 1) ==# 'klammertextAppOpen', s:Syn(7, 1))
call s:Check('verbatim interior @', s:Syn(7, 9) ==# 'klammertextVerbatim', s:Syn(7, 9))
call s:Check('verbatim close', s:Syn(7, 17) ==# 'klammertextAppClose', s:Syn(7, 17))
call s:Check('named close name@', s:Syn(8, 1) ==# 'klammertextAppClose'
\ && s:Syn(8, 5) ==# 'klammertextAppClose', s:Syn(8, 1))
call s:Check('abbreviated @name-', s:Syn(9, 1) ==# 'klammertextAppOpen'
\ && s:Syn(9, 6) ==# '' && s:Syn(9, 7) ==# '', s:Syn(9, 6))
call s:Check('file marker ##', s:Syn(10, 1) ==# 'klammertextMarker', s:Syn(10, 1))
call s:Check('## removes to EOF', s:Syn(11, 3) ==# 'klammertextRemovedFile', s:Syn(11, 3))
else
call add(s:results, 'SKIP syntax checks (this Vim lacks +syntax)')
endif
" --- jump-to-match (the CLI shell-out path) ---------------------------------
enew!
call setline(1, ['@i abc @ x'])
set filetype=klammertext
call cursor(1, 1)
KlammertextJumpToMatch
call s:Check('jump open -> close', col('.') == 8, col('.'))
KlammertextJumpToMatch
call s:Check('jump close -> open', col('.') == 1, col('.'))
" multibyte: 2-byte umlauts before the delimiters
enew!
call setline(1, ['üü @i x @'])
set filetype=klammertext
call cursor(1, 6) " byte col 6 = the @ of @i (2 x 2-byte ü)
KlammertextJumpToMatch
call s:Check('jump with multibyte line',
\ (exists('*charcol') ? charcol('.') : col('.'))
\ == (exists('*charcol') ? 9 : 11),
\ [col('.'), getline('.')])
" --- KlammertextCheck: diagnostics into the location list -------------------
enew!
call setline(1, ['@i abc', '@ol x ul@'])
set filetype=klammertext
KlammertextCheck
let s:ll = getloclist(0)
call s:Check('check fills the location list', len(s:ll) == 2, s:ll)
call s:Check('check positions', len(s:ll) == 2
\ && s:ll[0].lnum == 1 && s:ll[0].col == 1
\ && s:ll[1].lnum == 2 && s:ll[1].col == 7, s:ll)
call s:Check('check messages', len(s:ll) == 2
\ && s:ll[0].text =~# 'never closed' && s:ll[1].text =~# 'ul@', s:ll)
lclose
enew!
call setline(1, ['@i balanced @'])
set filetype=klammertext
KlammertextCheck
call s:Check('check clean buffer -> empty list', len(getloclist(0)) == 0)
" --- +python3: indentexpr (= operator) and live match highlighting ---------
if has('python3')
for s:fix in ['indent_list', 'indent_document', 'indent_named_close']
execute 'edit! ' . fnameescape($KT_FIX . '/' . s:fix . '.kt')
call s:Check(s:fix . ': indentexpr is set',
\ &indentexpr ==# 'klammertext#IndentExpr()', &indentexpr)
normal! gg=G
call s:Check(s:fix . ': gg=G equals expected fixture',
\ getline(1, '$') ==# readfile($KT_FIX . '/' . s:fix . '_expected.kt'))
edit!
endfor
" live match highlighting via the CursorMoved autocmd
enew!
call setline(1, ['@i abc @ x'])
set filetype=klammertext
call cursor(1, 1)
doautocmd CursorMoved
let s:m = filter(getmatches(), 'v:val.group =~# "^Klammertext"')
call s:Check('highlight: matched pair boxed',
\ len(s:m) == 1 && s:m[0].group ==# 'KlammertextMatch'
\ && len(filter(keys(s:m[0]), 'v:val =~# "^pos"')) == 2, s:m)
call cursor(1, 5) " not on a delimiter
doautocmd CursorMoved
call s:Check('highlight: cleared off-delimiter',
\ empty(filter(getmatches(), 'v:val.group =~# "^Klammertext"')))
enew!
call setline(1, ['@ol x ul@'])
set filetype=klammertext
call cursor(1, 1)
doautocmd CursorMoved
let s:m = filter(getmatches(), 'v:val.group =~# "^Klammertext"')
call s:Check('highlight: mismatch in red group',
\ len(s:m) == 1 && s:m[0].group ==# 'KlammertextMismatch', s:m)
" default mappings exist (buffer-local, LocalLeader)
call s:Check('mappings defined',
\ !empty(maparg('<LocalLeader>j', 'n'))
\ && !empty(maparg('<LocalLeader>i', 'n'))
\ && !empty(maparg('<LocalLeader>a', 'n')))
else
call add(s:results, 'SKIP +python3 checks (this Vim lacks python3)')
endif
" --- write results and exit -------------------------------------------------
call writefile(s:results, $KT_OUT)
if len(filter(copy(s:results), 'v:val =~# "^FAIL"')) > 0
cquit!
endif
qa!