diff --git a/clients/vim/README.md b/clients/vim/README.md index 1aeab05..4ce859a 100644 --- a/clients/vim/README.md +++ b/clients/vim/README.md @@ -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 diff --git a/clients/vim/autoload/tabby.vim b/clients/vim/autoload/tabby.vim index 2db3337..748133b 100644 --- a/clients/vim/autoload/tabby.vim +++ b/clients/vim/autoload/tabby.vim @@ -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 = "\\=tabby#ComsumeInsertion()\" + let insertion = "\\=tabby#ConsumeInsertion()\" 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("\", suffix_chars_to_replace) . "\\=tabby#ComsumeInsertion()\" + let insertion = repeat("\", suffix_chars_to_replace) . "\\=tabby#ConsumeInsertion()\" 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 diff --git a/clients/vim/autoload/tabby/inline_completion.vim b/clients/vim/autoload/tabby/inline_completion.vim new file mode 100644 index 0000000..cbc0df6 --- /dev/null +++ b/clients/vim/autoload/tabby/inline_completion.vim @@ -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 diff --git a/clients/vim/autoload/tabby/job.vim b/clients/vim/autoload/tabby/job.vim new file mode 100644 index 0000000..1103335 --- /dev/null +++ b/clients/vim/autoload/tabby/job.vim @@ -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