tabby/clients/vim/autoload/tabby/virtual_text.vim

198 lines
6.2 KiB
VimL

" Handles virtual text (aka ghost text) for rendering inline completion
if exists('g:autoloaded_tabby_virtual_text')
finish
endif
let g:autoloaded_tabby_virtual_text = 1
let s:vim = exists('*prop_type_add')
let s:nvim = !s:vim && has('nvim') && exists('*nvim_buf_set_extmark')
function! tabby#virtual_text#Check()
return #{
\ ok: s:vim || s:nvim,
\ message: 'Tabby requires Vim 9.0.0534+ with +textprop feature support, or NeoVim 0.6.0+.',
\ }
endfunction
function! tabby#virtual_text#Init()
hi def TabbyCompletion guifg=#808080 ctermfg=245
hi def TabbyCompletionReplaceRange guifg=#303030 ctermfg=236 guibg=#808080 ctermbg=245
if s:vim
let s:prop_type_completion = 'TabbyCompletion'
if prop_type_get(s:prop_type_completion) != {}
call prop_type_delete(s:prop_type_completion)
endif
call prop_type_add(s:prop_type_completion, #{
\ highlight: 'TabbyCompletion',
\ priority: 99,
\ combine: 0,
\ override: 1,
\ })
let s:prop_type_replace = 'TabbyCompletionReplaceRange'
if prop_type_get(s:prop_type_replace) != {}
call prop_type_delete(s:prop_type_replace)
endif
call prop_type_add(s:prop_type_replace, #{
\ highlight: 'TabbyCompletionReplaceRange',
\ priority: 99,
\ combine: 0,
\ override: 1,
\ })
endif
if s:nvim
let s:nvim_namespace = nvim_create_namespace('TabbyCompletion')
let s:nvim_highlight_completion = 'TabbyCompletion'
let s:nvim_highlight_replace = 'TabbyCompletionReplaceRange'
endif
endfunction
function! tabby#virtual_text#Render(request, choice)
if (type(a:choice.text) != v:t_string) || (len(a:choice.text) == 0)
return
endif
let prefix_replace_chars = a:request.position - a:choice.replaceRange.start
let suffix_replace_chars = a:choice.replaceRange.end - a:request.position
let text = strcharpart(a:choice.text, prefix_replace_chars)
if len(text) == 0
return
endif
let current_line_suffix = strpart(getline('.'), col('.') - 1)
if strchars(current_line_suffix) < suffix_replace_chars
return
endif
let text_lines = split(text, "\n")
" split will not give an empty line if text starts or ends with "\n"
if text[:0] == "\n"
call insert(text_lines, '')
endif
if text[-1:] == "\n"
call add(text_lines, '')
endif
" FIXME: no replace range processing for nvim for now, we need
" feat `virt_text_pos: "inline"` after nvim 0.10.0
if s:nvim
let text_lines[-1] .= strcharpart(current_line_suffix, suffix_replace_chars)
call s:AddInlay(text_lines[0], col('.'))
if len(text_lines) > 1
call s:AddLinesBelow(text_lines[1:])
endif
return
endif
" Replace range processing for vim
if suffix_replace_chars == 0
call s:AddInlay(text_lines[0], col('.'))
if len(text_lines) > 1
if strchars(current_line_suffix) > 0
call s:MarkReplaceRange(range(col('.'), col('.') + len(current_line_suffix)))
let text_lines[-1] .= current_line_suffix
endif
call s:AddLinesBelow(text_lines[1:])
endif
elseif suffix_replace_chars == 1
let replace_char = strcharpart(current_line_suffix, 0, 1)
let inlay = ''
if strchars(text_lines[0]) > 0 && stridx(text_lines[0], replace_char) != 0
let inlay = split(text_lines[0], replace_char)[0]
endif
call s:AddInlay(inlay, col('.'))
if inlay != text_lines[0]
let inlay_suffix = strpart(text_lines[0], len(inlay) + len(replace_char))
call s:AddInlay(inlay_suffix, col('.') + len(replace_char))
endif
if len(text_lines) > 1
if strchars(current_line_suffix) > 0
let range_start = col('.')
if inlay != text_lines[0]
let range_start += len(replace_char)
endif
call s:MarkReplaceRange(range(range_start, col('.') + len(current_line_suffix)))
let text_lines[-1] .= strcharpart(current_line_suffix, 1)
endif
call s:AddLinesBelow(text_lines[1:])
endif
else
let replace_char = strcharpart(current_line_suffix, 0, suffix_replace_chars)
call s:AddInlay(text_lines[0], col('.'))
call s:MarkReplaceRange(range(col('.'), col('.') + len(replace_char)))
if len(text_lines) > 1
if strchars(current_line_suffix) > suffix_replace_chars
call s:MarkReplaceRange(range(col('.') + len(replace_char), col('.') + len(current_line_suffix)))
let text_lines[-1] .= strcharpart(current_line_suffix, suffix_replace_chars)
endif
call s:AddLinesBelow(text_lines[1:])
endif
endif
endfunction
function! s:AddInlay(inlay, column)
if s:vim
if len(a:inlay) > 0
call prop_add(line('.'), a:column, #{
\ type: s:prop_type_completion,
\ text: a:inlay,
\ })
endif
endif
if s:nvim
if len(a:inlay) > 0
" FIXME: using virt_text_pos: "inline" after nvim 0.10.0
call nvim_buf_set_extmark(0, s:nvim_namespace, line('.') - 1, col('.') - 1, #{
\ virt_text_win_col: virtcol('.') - 1,
\ virt_text: [[a:inlay, s:nvim_highlight_completion]],
\ })
endif
endif
endfunction
function! s:AddLinesBelow(lines_below)
if s:vim
for line_blow in a:lines_below
let text = line_blow
if len(text) == 0
let text = ' '
endif
call prop_add(line('.'), 0, #{
\ type: s:prop_type_completion,
\ text: text,
\ text_align: 'below',
\ })
endfor
endif
if s:nvim
call nvim_buf_set_extmark(0, s:nvim_namespace, line('.') - 1, col('.') - 1, #{
\ virt_lines: map(a:lines_below, { i, l -> [[l, s:nvim_highlight_completion]] })
\ })
endif
endfunction
function! s:MarkReplaceRange(replace_range)
if s:vim
call prop_add(line('.'), a:replace_range[0], #{
\ type: s:prop_type_replace,
\ length: len(a:replace_range),
\ })
endif
if s:nvim
call nvim_buf_add_highlight(0, s:nvim_namespace, s:nvim_highlight_replace, line('.') - 1,
\ a:replace_range[0] - 1, a:replace_range[-1])
endif
endfunction
function! tabby#virtual_text#Clear()
if s:vim
call prop_remove(#{
\ type: s:prop_type_completion,
\ all: 1,
\ })
call prop_remove(#{
\ type: s:prop_type_replace,
\ all: 1,
\ })
endif
if s:nvim
call nvim_buf_clear_namespace(0, s:nvim_namespace, 0, -1)
endif
endfunction