Add neovim support. (#87)

* Add neovim support.

* Fix lint.
add-tracing
Zhiming Ma 2023-04-12 03:36:27 +08:00 committed by GitHub
parent f3aa91c715
commit 9c4b13174e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 259 additions and 41 deletions

View File

@ -2,7 +2,7 @@
## Requirements
1. VIM 9.0+ with `+job` and `+textprop` features enabled. NeoVIM is not supported at the moment.
1. VIM 9.0+ with `+job` and `+textprop` features enabled, or NeoVIM 0.6.0+.
2. Node.js 16.0+.
## Getting started

View File

@ -73,17 +73,18 @@ function! tabby#Start()
return
endif
if !exists('*job_start') || !exists('*prop_type_add')
let s:errmsg = 'Tabby requires Vim 9.0+ with +job and +textprop support.'
let check_job = tabby#job#Check()
if !check_job.ok
let s:errmsg = check_job.message
return
endif
hi def TabbyCompletion guifg=#808080 ctermfg=8
let s:prop_type = 'TabbyCompletion'
if prop_type_get(s:prop_type) != v:null
call prop_type_delete(s:prop_type)
let check_inline_completion = tabby#inline_completion#Check()
if !check_inline_completion.ok
let s:errmsg = check_inline_completion.message
return
endif
call prop_type_add(s:prop_type, {'highlight': 'TabbyCompletion'})
call tabby#inline_completion#Init()
if !executable('node')
let s:errmsg = 'Tabby requires node to be installed.'
@ -100,7 +101,7 @@ function! tabby#Start()
let s:tabby_status = 'connecting'
let command = 'node ' . node_script
let s:tabby = job_start(command, #{
let s:tabby = tabby#job#Start(command, #{
\ in_mode: 'json',
\ out_mode: 'json',
\ out_cb: function('s:HandleNotification'),
@ -114,7 +115,7 @@ endfunction
function! tabby#Stop()
if tabby#Running()
call job_stop(s:tabby)
call tabby#job#Stop(s:tabby)
endif
endfunction
@ -146,7 +147,7 @@ function! s:UpdateServerUrl()
if !tabby#Running()
return
endif
call ch_sendexpr(s:tabby, #{
call tabby#job#Send(s:tabby, #{
\ func: 'setServerUrl',
\ args: [g:tabby_server_url],
\ })
@ -161,7 +162,7 @@ function! s:GetCompletion(id)
if l:language == 'unknown'
return
endif
call ch_sendexpr(s:tabby, #{
call tabby#job#Send(s:tabby, #{
\ func: 'getCompletion',
\ args: [#{
\ prompt: s:GetPrompt(),
@ -179,7 +180,7 @@ function! s:PostEvent(event_type)
if !exists('s:completion') || !exists('s:completion_index')
return
endif
call ch_sendexpr(s:tabby, #{
call tabby#job#Send(s:tabby, #{
\ func: 'postEvent',
\ args: [#{
\ type: a:event_type,
@ -190,7 +191,7 @@ function! s:PostEvent(event_type)
endfunction
function! s:HandleNotification(channel, data)
if a:data.event == 'statusChanged'
if has_key(a:data, 'event') && (a:data.event == 'statusChanged')
let s:tabby_status = a:data.status
endif
endfunction
@ -199,10 +200,8 @@ function! s:HandleCompletion(id, channel, data)
if !exists('s:trigger_id') || (a:id != s:trigger_id)
return
endif
if a:data == v:null
return
endif
if (type(a:data.choices) == v:t_list) && (len(a:data.choices) > 0)
if (type(a:data) == v:t_dict) && has_key(a:data, 'choices') &&
\ (type(a:data.choices) == v:t_list) && (len(a:data.choices) > 0)
let s:completion = a:data
let s:completion_index = 0
call tabby#Show()
@ -263,7 +262,7 @@ endfunction
" Completion control
function! tabby#Show()
call s:RemoveProp()
call s:RemoveCompletion()
if !s:CompletionAvailable()
return
endif
@ -276,22 +275,12 @@ function! tabby#Show()
return
endif
let lines = split(choice.text, "\n")
call prop_add(line('.'), col('.'), #{
\ type: s:prop_type,
\ text: lines[0],
\ })
for line in lines[1:]
call prop_add(line('.'), 0, #{
\ type: s:prop_type,
\ text: line,
\ text_align: 'below',
\ })
endfor
call tabby#inline_completion#Show(lines)
let s:prop_shown_lines = lines
call s:PostEvent('view')
endfunction
function! tabby#ComsumeInsertion()
function! tabby#ConsumeInsertion()
if !exists('s:text_to_insert')
return ''
else
@ -308,14 +297,14 @@ function! tabby#Accept(fallback)
let lines = s:prop_shown_lines
if len(lines) == 1
let s:text_to_insert = lines[0]
let insertion = "\<C-R>\<C-O>=tabby#ComsumeInsertion()\<CR>"
let insertion = "\<C-R>\<C-O>=tabby#ConsumeInsertion()\<CR>"
else
let current_line = getbufline('%', line('.'), line('.'))[0]
let suffix_chars_to_replace = len(current_line) - col('.') + 1
let s:text_to_insert = join(lines, "\n")
let insertion = repeat("\<Del>", suffix_chars_to_replace) . "\<C-R>\<C-O>=tabby#ComsumeInsertion()\<CR>"
let insertion = repeat("\<Del>", suffix_chars_to_replace) . "\<C-R>\<C-O>=tabby#ConsumeInsertion()\<CR>"
endif
call s:RemoveProp()
call s:RemoveCompletion()
call s:PostEvent('select')
return insertion
endfunction
@ -324,7 +313,7 @@ function! tabby#Dismiss(fallback)
if !exists('s:prop_shown_lines')
return a:fallback
endif
call s:RemoveProp()
call s:RemoveCompletion()
return ''
endfunction
@ -363,7 +352,7 @@ function! tabby#Prev()
endfunction
function! tabby#Clear()
call s:RemoveProp()
call s:RemoveCompletion()
if exists('s:scheduled')
call timer_stop(s:scheduled)
unlet s:scheduled
@ -389,11 +378,8 @@ function! s:CompletionAvailable()
return v:true
endfunction
function! s:RemoveProp()
call prop_remove(#{
\ type: s:prop_type,
\ all: v:true,
\ })
function! s:RemoveCompletion()
call tabby#inline_completion#Clear()
if exists('s:prop_shown_lines')
unlet s:prop_shown_lines
endif

View File

@ -0,0 +1,72 @@
if exists('g:autoloaded_tabby_inline_completion')
finish
endif
let g:autoloaded_tabby_inline_completion = 1
let s:vim = exists('*prop_type_add')
let s:nvim = !s:vim && has('nvim') && exists('*nvim_buf_set_extmark')
function! tabby#inline_completion#Check()
return #{
\ ok: s:vim || s:nvim,
\ message: 'Tabby requires Vim 9.0+ with +textprop feature support, or NeoVim 0.6.0+.',
\}
endfunction
function! tabby#inline_completion#Init()
hi def TabbyCompletion guifg=#808080 ctermfg=8
if s:vim
let s:prop_type = 'TabbyCompletion'
if prop_type_get(s:prop_type) != v:null
call prop_type_delete(s:prop_type)
endif
call prop_type_add(s:prop_type, {'highlight': 'TabbyCompletion'})
endif
if s:nvim
let s:nvim_namespace = nvim_create_namespace('TabbyCompletion')
let s:nvim_highlight = 'TabbyCompletion'
let s:nvim_extmark_id = 1
endif
endfunction
function! tabby#inline_completion#Show(lines)
if len(a:lines) == 0
return
endif
if s:vim
call prop_add(line('.'), col('.'), #{
\ type: s:prop_type,
\ text: a:lines[0],
\ })
for line in a:lines[1:]
call prop_add(line('.'), 0, #{
\ type: s:prop_type,
\ text: line,
\ text_align: 'below',
\ })
endfor
endif
if s:nvim
let opt = #{
\ id: s:nvim_extmark_id,
\ virt_text_win_col: virtcol('.') - 1,
\ virt_text: [[a:lines[0], s:nvim_highlight]],
\}
if len(a:lines) > 1
let opt.virt_lines = map(a:lines[1:], { i, l -> [[l, s:nvim_highlight]] })
endif
call nvim_buf_set_extmark(0, s:nvim_namespace, line('.') - 1, col('.') - 1, opt)
endif
endfunction
function! tabby#inline_completion#Clear()
if s:vim
call prop_remove(#{
\ type: s:prop_type,
\ all: v:true,
\ })
endif
if s:nvim
call nvim_buf_del_extmark(0, s:nvim_namespace, s:nvim_extmark_id)
endif
endfunction

View File

@ -0,0 +1,160 @@
if exists('g:autoloaded_tabby_job')
finish
endif
let g:autoloaded_tabby_job = 1
let s:vim = exists('*job_start')
let s:nvim = !s:vim && has('nvim') && exists('*jobstart')
function! tabby#job#Check()
return #{
\ ok: s:vim || s:nvim,
\ message: 'Tabby requires Vim 9.0+ with +job feature support, or NeoVim 0.6.0+.',
\}
endfunction
let s:nvim_job_map = {}
" Assume Json IO
" Options 'out_cb', 'err_cb', 'exit_cb' supported
" Return job id
function! tabby#job#Start(command, ...)
let options = get(a:, 1, {})
if s:vim
let opt = #{
\ in_mode: 'json',
\ out_mode: 'json',
\ }
if has_key(options, 'out_cb')
let opt.out_cb = options.out_cb
endif
if has_key(options, 'err_cb')
let opt.err_cb = options.err_cb
endif
if has_key(options, 'exit_cb')
let opt.exit_cb = options.exit_cb
endif
return job_start(a:command, opt)
endif
if s:nvim
let id = jobstart(a:command, #{
\ on_stdout: function('s:NvimHandleStdout'),
\ on_stderr: function('s:NvimHandleStderr'),
\ on_exit: function('s:NvimHandleExit'),
\})
let s:nvim_job_map[id] = #{
\ out_buffer: '',
\ requests: {},
\}
if has_key(options, 'out_cb')
let s:nvim_job_map[id].out_cb = options.out_cb
endif
if has_key(options, 'err_cb')
let s:nvim_job_map[id].err_cb = options.err_cb
endif
if has_key(options, 'exit_cb')
let s:nvim_job_map[id].exit_cb = options.exit_cb
endif
return id
endif
endfunction
function! tabby#job#Stop(job)
if s:vim
return job_stop(a:job)
endif
if s:nvim
let ret = jobstop(a:job)
call jobwait([a:job], 100)
if has_key(s:nvim_job_map, a:job)
unlet s:nvim_job_map[a:job]
endif
return ret
endif
endfunction
" Align to Vim's ch_sendexpr
" Options 'callback' supported
function! tabby#job#Send(job, data, ...)
let options = get(a:, 1, {})
if s:vim
return ch_sendexpr(a:job, a:data, options)
endif
if s:nvim
let id = s:NextRequestId()
let request = [id, a:data]
let s:nvim_job_map[a:job].requests[id] = {}
if has_key(options, 'callback')
let s:nvim_job_map[a:job].requests[id].callback = options.callback
endif
call chansend(a:job, json_encode(request) . "\n")
endif
endfunction
let s:request_id = 1
function! s:NextRequestId()
let s:request_id += 1
return s:request_id
endfunction
function! s:NvimHandleStdout(job, data, event)
if !has_key(s:nvim_job_map, a:job)
return
endif
let buf = s:nvim_job_map[a:job].out_buffer
for data_line in a:data
let buf .= data_line
try
let decoded = json_decode(buf)
let buf = ''
catch
continue
endtry
call s:NvimHandleOutDecoded(a:job, decoded)
endfor
let s:nvim_job_map[a:job].out_buffer = buf
endfunction
function! s:NvimHandleOutDecoded(job, decoded)
if !has_key(s:nvim_job_map, a:job)
return
endif
if type(a:decoded) != v:t_list || len(a:decoded) < 1 || (type(a:decoded[0]) != v:t_number)
return
endif
let id = a:decoded[0]
if len(a:decoded) >= 2
let data = a:decoded[1]
else
let data = {}
endif
if (id > 0) && has_key(s:nvim_job_map[a:job].requests, id)
let request = s:nvim_job_map[a:job].requests[id]
if has_key(request, 'callback')
call request.callback(a:job, data)
endif
unlet s:nvim_job_map[a:job].requests[id]
else
if has_key(s:nvim_job_map[a:job], 'out_cb')
call s:nvim_job_map[a:job].out_cb(a:job, data)
endif
endif
endfunction
function! s:NvimHandleStderr(job, data, event)
if !has_key(s:nvim_job_map, a:job)
return
endif
if has_key(s:nvim_job_map[a:job], 'err_cb')
call s:nvim_job_map[a:job].err_cb(a:job, join(a:data, "\n"))
endif
endfunction
function! s:NvimHandleExit(job, status, event)
if !has_key(s:nvim_job_map, a:job)
return
endif
if has_key(s:nvim_job_map[a:job], 'exit_cb')
call s:nvim_job_map[a:job].exit_cb(a:job, a:status)
endif
endfunction