299 lines
8.4 KiB
VimL
299 lines
8.4 KiB
VimL
" Main functions
|
|
|
|
if exists('g:autoloaded_tabby')
|
|
finish
|
|
endif
|
|
let g:autoloaded_tabby = 1
|
|
|
|
let s:status = "initializing"
|
|
let s:message = ""
|
|
|
|
function! tabby#Status()
|
|
if s:status == "initializing"
|
|
echo 'Tabby is initializing.'
|
|
elseif s:status == "initialization_failed"
|
|
echo 'Tabby initialization failed.'
|
|
echo s:message
|
|
elseif s:status == "initialization_done"
|
|
let agent_status = tabby#agent#Status()
|
|
if agent_status == 'notInitialized'
|
|
echo 'Tabby is initializing.'
|
|
elseif agent_status == 'exited'
|
|
echo 'Tabby agent exited unexpectedly.'
|
|
elseif agent_status == 'ready'
|
|
echo 'Tabby is online.'
|
|
let agent_issues = tabby#agent#Issues()
|
|
if len(agent_issues) > 0
|
|
if agent_issues[0] == 'slowCompletionResponseTime'
|
|
echo 'Completion requests appear to take too much time.'
|
|
elseif agent_issues[0] == 'highCompletionTimeoutRate'
|
|
echo 'Most completion requests timed out.'
|
|
endif
|
|
elseif g:tabby_trigger_mode == 'manual'
|
|
echo 'You can use ' . g:tabby_keybinding_trigger_or_dismiss .
|
|
\ ' in insert mode to trigger completion manually.'
|
|
elseif g:tabby_trigger_mode == 'auto'
|
|
echo 'Automatic inline completion is enabled.'
|
|
endif
|
|
elseif agent_status == 'disconnected'
|
|
echo 'Tabby cannot connect to server. Please check your settings.'
|
|
elseif agent_status == 'unauthorized'
|
|
echo 'Authorization required. Use `:Tabby auth` to continue.'
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
function! tabby#OnVimEnter()
|
|
call tabby#globals#Load()
|
|
|
|
let check_job = tabby#job#Check()
|
|
if !check_job.ok
|
|
let s:status = "initialization_failed"
|
|
let s:message = check_job.message
|
|
return
|
|
endif
|
|
|
|
let check_virtual_text = tabby#virtual_text#Check()
|
|
if !check_virtual_text.ok
|
|
let s:status = "initialization_failed"
|
|
let s:errmsg = check_virtual_text.message
|
|
return
|
|
endif
|
|
call tabby#virtual_text#Init()
|
|
|
|
let node_binary = expand(g:tabby_node_binary)
|
|
if !executable(node_binary)
|
|
let s:status = "initialization_failed"
|
|
let s:message = 'Node.js binary not found. Please install Node.js version >= 18.0.'
|
|
return
|
|
endif
|
|
|
|
let node_version_command = node_binary . ' --version'
|
|
let version_output = system(node_version_command)
|
|
let node_version = matchstr(version_output, '\d\+\.\d\+\.\d\+')
|
|
let major_version = str2nr(split(node_version, '\.')[0])
|
|
if major_version < 18
|
|
let s:status = "initialization_failed"
|
|
let s:message = 'Node.js version is too old: ' . node_version . '. Please install Node.js version >= 18.0.'
|
|
return
|
|
endif
|
|
|
|
if !filereadable(g:tabby_node_script)
|
|
let s:message = 'Tabby agent script not found. Please reinstall Tabby plugin.'
|
|
return
|
|
endif
|
|
|
|
let command = node_binary . ' ' . g:tabby_node_script
|
|
call tabby#agent#Open(command)
|
|
|
|
call tabby#keybindings#Map()
|
|
|
|
let s:status = "initialization_done"
|
|
endfunction
|
|
|
|
function! tabby#OnVimLeave()
|
|
call tabby#agent#Close()
|
|
endfunction
|
|
|
|
function! tabby#OnTextChanged()
|
|
if s:status != "initialization_done"
|
|
return
|
|
endif
|
|
if g:tabby_trigger_mode == 'auto'
|
|
" FIXME: Do not dismiss when type over the same as the completion text, or backspace in replace range.
|
|
call tabby#Dismiss()
|
|
call tabby#Trigger(v:false)
|
|
endif
|
|
endfunction
|
|
|
|
function! tabby#OnCursorMoved()
|
|
if s:current_completion_request == s:GetCompletionContext(v:false)
|
|
return
|
|
endif
|
|
call tabby#Dismiss()
|
|
if s:ongoing_request_id != 0
|
|
call tabby#agent#CancelRequest(s:ongoing_request_id)
|
|
endif
|
|
endfunction
|
|
|
|
function! tabby#OnInsertLeave()
|
|
call tabby#Dismiss()
|
|
if s:ongoing_request_id != 0
|
|
call tabby#agent#CancelRequest(s:ongoing_request_id)
|
|
endif
|
|
endfunction
|
|
|
|
function! tabby#TriggerOrDismiss()
|
|
if s:status != "initialization_done"
|
|
return ''
|
|
endif
|
|
if s:current_completion_response != {}
|
|
call tabby#Dismiss()
|
|
else
|
|
call tabby#Trigger(v:true)
|
|
endif
|
|
return ''
|
|
endfunction
|
|
|
|
" Store the context of ongoing completion request
|
|
let s:current_completion_request = {}
|
|
let s:ongoing_request_id = 0
|
|
|
|
function! tabby#Trigger(is_manual)
|
|
if s:status != "initialization_done"
|
|
return
|
|
endif
|
|
if s:ongoing_request_id != 0
|
|
call tabby#agent#CancelRequest(s:ongoing_request_id)
|
|
endif
|
|
let s:current_completion_request = s:GetCompletionContext(a:is_manual)
|
|
let request = s:current_completion_request
|
|
let OnResponse = { response -> s:HandleCompletionResponse(request, response) }
|
|
let s:ongoing_request_id = tabby#agent#ProvideCompletions(request, OnResponse)
|
|
endfunction
|
|
|
|
" Store the completion response that is shown as inline completion.
|
|
let s:current_completion_response = {}
|
|
|
|
function! s:HandleCompletionResponse(request, response)
|
|
if s:current_completion_request != a:request
|
|
return
|
|
endif
|
|
let s:ongoing_request_id = 0
|
|
if (type(a:response) != v:t_dict) || !has_key(a:response, 'choices') ||
|
|
\ (type(a:response.choices) != v:t_list)
|
|
return
|
|
endif
|
|
call tabby#Dismiss()
|
|
if (len(a:response.choices) == 0)
|
|
return
|
|
endif
|
|
" Only support single choice completion for now
|
|
let choice = a:response.choices[0]
|
|
call tabby#virtual_text#Render(s:current_completion_request, choice)
|
|
let s:current_completion_response = a:response
|
|
|
|
call tabby#agent#PostEvent(#{
|
|
\ type: "view",
|
|
\ completion_id: a:response.id,
|
|
\ choice_index: choice.index,
|
|
\ })
|
|
endfunction
|
|
|
|
" Used as a buffer to store the text that should be inserted when user accepts
|
|
" the completion.
|
|
let s:text_to_insert = ''
|
|
|
|
function! tabby#ConsumeInsertion()
|
|
let text = s:text_to_insert
|
|
let s:text_to_insert = ''
|
|
return text
|
|
endfunction
|
|
|
|
function! tabby#Accept(...)
|
|
if s:current_completion_response == {}
|
|
" keybindings fallback
|
|
if a:0 < 1
|
|
return "\<Ignore>"
|
|
elseif type(a:1) == v:t_string
|
|
return a:1
|
|
elseif type(a:1) == v:t_func
|
|
return call(a:1, [])
|
|
endif
|
|
endif
|
|
|
|
let response = s:current_completion_response
|
|
let choice = response.choices[0]
|
|
if (type(choice.text) != v:t_string) || (len(choice.text) == 0)
|
|
return
|
|
endif
|
|
let prefix_replace_chars = s:current_completion_request.position - choice.replaceRange.start
|
|
let suffix_replace_chars = choice.replaceRange.end - s:current_completion_request.position
|
|
let s:text_to_insert = strcharpart(choice.text, prefix_replace_chars)
|
|
let insertion = repeat("\<Del>", suffix_replace_chars) . "\<C-R>\<C-O>=tabby#ConsumeInsertion()\<CR>"
|
|
|
|
if s:text_to_insert[-1:] == "\n"
|
|
" Add a char and remove, workaround for insertion bug if ends with newline
|
|
let s:text_to_insert .= "_"
|
|
let insertion .= "\<BS>"
|
|
endif
|
|
|
|
call tabby#Dismiss()
|
|
|
|
call tabby#agent#PostEvent(#{
|
|
\ type: "select",
|
|
\ completion_id: response.id,
|
|
\ choice_index: choice.index,
|
|
\ })
|
|
|
|
return insertion
|
|
endfunction
|
|
|
|
function! tabby#Dismiss()
|
|
let s:current_completion_response = {}
|
|
call tabby#virtual_text#Clear()
|
|
endfunction
|
|
|
|
function! s:GetCompletionContext(is_manual)
|
|
return #{
|
|
\ filepath: expand('%:p'),
|
|
\ language: s:GetLanguage(),
|
|
\ text: join(getbufline('%', 1, '$'), "\n"),
|
|
\ position: s:GetCursorPosition(),
|
|
\ manually: a:is_manual,
|
|
\ }
|
|
endfunction
|
|
|
|
" Count the number of characters from the beginning of the buffer to the cursor.
|
|
function! s:GetCursorPosition()
|
|
let lines = getline(1, line('.') - 1)
|
|
if col('.') > 1
|
|
let lines += [strpart(getline(line('.')), 0, col('.') - 1)]
|
|
else
|
|
let lines += ['']
|
|
endif
|
|
return strchars(join(lines, "\n"))
|
|
endfunction
|
|
|
|
function! s:GetLanguage()
|
|
let filetype = getbufvar('%', '&filetype')
|
|
if has_key(g:tabby_filetype_dict, filetype)
|
|
return g:tabby_filetype_dict[filetype]
|
|
else
|
|
return filetype
|
|
endif
|
|
endfunction
|
|
|
|
function! tabby#Auth()
|
|
if s:status != "initialization_done"
|
|
echo 'Tabby is not ready for auth.'
|
|
return
|
|
endif
|
|
if tabby#agent#Status() != 'unauthorized'
|
|
echo 'Already authorized.'
|
|
return
|
|
endif
|
|
call tabby#agent#RequestAuthUrl({ data -> s:HandleAuthUrl(data) })
|
|
echo 'Generating authorization URL, please wait...'
|
|
endfunction
|
|
|
|
function! s:HandleAuthUrl(data)
|
|
if (type(a:data) != v:t_dict) || !has_key(a:data, 'authUrl') || !has_key(a:data, 'code')
|
|
echo 'Failed to create authorization URL.'
|
|
return
|
|
endif
|
|
call tabby#agent#WaitForAuthToken(a:data.code)
|
|
let command = ''
|
|
if executable('xdg-open')
|
|
let command = 'xdg-open ' . a:data.authUrl
|
|
elseif executable('open')
|
|
let command = 'open ' . a:data.authUrl
|
|
elseif executable('wslview')
|
|
let command = 'wslview ' . a:data.authUrl
|
|
endif
|
|
if command != ''
|
|
call system(command)
|
|
endif
|
|
echo a:data.authUrl
|
|
endfunction
|