first commit

This commit is contained in:
zoe 2022-04-02 20:23:20 +02:00
commit 9420badb70
34 changed files with 3571 additions and 0 deletions

View file

@ -0,0 +1,264 @@
let s:status_poll_interval = 5 * 1000 " 5sec in milliseconds
let s:timer = -1
let s:watch_timer = -1
if !kite#utils#windows()
let s:kite_symbol = nr2char(printf('%d', '0x27E0'))
else
let s:kite_symbol = '[k]'
endif
let s:inited = 0
let s:kite_auto_launched = 0
function kite#enable_auto_start()
call kite#utils#set_setting('start_kited_at_startup', 1)
call s:launch_kited()
call kite#utils#info('Kite: auto-start enabled')
endfunction
function kite#disable_auto_start()
call kite#utils#set_setting('start_kited_at_startup', 0)
call kite#utils#info('Kite: auto-start disabled')
endfunction
function kite#symbol()
return s:kite_symbol
endfunction
function kite#statusline()
if exists('b:kite_status')
return b:kite_status
else
return ''
endif
endfunction
function! kite#max_file_size()
" Fallback to 1MB
return get(b:, 'kite_max_file_size', 1048576)
endfunction
function! kite#configure_completeopt()
" If the user has configured completeopt, leave it alone.
redir => output
silent verbose set completeopt
redir END
if len(split(output, '\n')) > 1 | return | endif
set completeopt=menuone,noinsert
endfunction
function! s:setup_options()
let s:pumheight = &pumheight
if &pumheight == 0
set pumheight=10
endif
let s:updatetime = &updatetime
if &updatetime == 4000
set updatetime=100
endif
let s:shortmess = &shortmess
set shortmess+=c
if kite#utils#windows()
" Avoid taskbar flashing on Windows when executing system() calls.
let s:shelltemp = &shelltemp
set noshelltemp
endif
endfunction
function! s:restore_options()
if !exists('s:pumheight') | return | endif
let &pumheight = s:pumheight
unlet s:pumheight
let &updatetime = s:updatetime
let &shortmess = s:shortmess
if kite#utils#windows()
let &shelltemp = s:shelltemp
endif
endfunction
function! kite#bufenter()
if kite#languages#supported_by_plugin()
call s:launch_kited()
if !kite#utils#kite_running()
call kite#status#status()
call s:start_status_timer()
call s:start_watching_for_kited()
return
endif
call s:stop_watching_for_kited()
if kite#languages#supported_by_kited()
if g:kite_completions
call s:disable_completion_plugins()
endif
call s:setup_options()
call s:setup_events()
call s:setup_mappings()
call s:set_max_file_size()
if g:kite_completions
setlocal completefunc=kite#completion#complete
endif
call kite#events#event('focus')
call kite#status#status()
call s:start_status_timer()
return
end
end
" Buffer is not a supported language.
call s:restore_options()
call s:stop_status_timer()
endfunction
function s:setup_events()
augroup KiteEvents
autocmd! * <buffer>
autocmd CursorHold,CursorHoldI <buffer> call kite#events#event('selection')
autocmd TextChanged,TextChangedI <buffer> call kite#events#event('edit')
autocmd FocusGained <buffer> call kite#events#event('focus')
if g:kite_completions
autocmd InsertCharPre <buffer> call kite#completion#insertcharpre()
autocmd TextChangedI <buffer> call kite#completion#autocomplete()
autocmd CompleteDone <buffer> call kite#completion#replace_range()
if &ft == 'go'
autocmd CompleteDone <buffer> call kite#completion#expand_newlines()
endif
if &ft == 'python'
autocmd CompleteDone <buffer> call kite#snippet#complete_done()
endif
endif
if exists('g:kite_documentation_continual') && g:kite_documentation_continual
autocmd CursorHold,CursorHoldI <buffer> call kite#docs#docs()
endif
augroup END
endfunction
function! s:setup_mappings()
if exists('g:kite_tab_complete') && g:kite_completions
imap <buffer> <expr> <Tab> pumvisible() ? "\<C-y>" : "\<Tab>"
endif
if empty(maparg('K', 'n')) && !hasmapto('(kite-docs)', 'n')
nmap <silent> <buffer> K <Plug>(kite-docs)
endif
if empty(maparg('<C-]>', 'n'))
nmap <silent> <buffer> <C-]> :KiteGotoDefinition<CR>
endif
endfunction
function! s:set_max_file_size()
let max_file_size = kite#client#max_file_size()
if max_file_size != -1
let b:kite_max_file_size = max_file_size
endif
endfunction
function! s:start_status_timer()
if s:timer == -1
let s:timer = timer_start(s:status_poll_interval,
\ function('kite#status#status'),
\ {'repeat': -1}
\ )
else
call timer_pause(s:timer, 0) " unpause
endif
endfunction
function! s:stop_status_timer()
call timer_pause(s:timer, 1)
endfunction
function! s:launch_kited()
if !s:kite_auto_launched && kite#utils#get_setting('start_kited_at_startup', 1)
call kite#utils#launch_kited()
let s:kite_auto_launched = 1
endif
endfunction
function! s:start_watching_for_kited()
if s:watch_timer == -1
let s:watch_timer = timer_start(s:status_poll_interval,
\ function('kite#activate_when_ready'),
\ {'repeat': -1}
\ )
else
call timer_pause(s:watch_timer, 0) " unpause
endif
endfunction
function! kite#activate_when_ready(...)
if kite#utils#kite_running()
call kite#bufenter()
endif
endfunction
function! s:stop_watching_for_kited()
call timer_pause(s:watch_timer, 1)
endfunction
function! s:disable_completion_plugins()
" coc.nvim
if exists('g:did_coc_loaded')
let b:coc_suggest_disable = 1
" Alternatively:
" autocmd BufEnter *.python :CocDisable
" autocmd BufLeave *.python :CocEnable
call kite#utils#warn("disabling coc.nvim's completions in this buffer")
endif
" Jedi
if exists('*jedi#setup_completion')
" This may not be enough: https://github.com/davidhalter/jedi-vim/issues/614
let g:jedi#completions_enabled = 0
call kite#utils#warn("disabling jedi-vim's completions")
" Alternatively:
" call kite#utils#warn('please uninstall jedi-vim and restart vim/nvim')
" finish
endif
" YouCompleteMe
if exists('g:loaded_youcompleteme') && !exists('g:ycm_filetype_blacklist.python')
let g:ycm_filetype_blacklist.python = 1
call kite#utils#warn("disabling YouCompleteMe's completions for python files")
endif
" Deoplete
if exists('*deoplete#disable')
call deoplete#disable()
call kite#utils#warn("disabling deoplete's completions")
endif
endfunction

View file

@ -0,0 +1,123 @@
let s:async_sync_id = 0
let s:async_sync_outputs = {}
function! s:next_async_sync_id()
let async_sync_id = s:async_sync_id
let s:async_sync_id += 1
return async_sync_id
endfunction
function! s:async_sync_output(async_sync_id, output)
if type(a:output) == v:t_list
" Ensure empty list becomes an empty string.
let output = join(a:output, "\n")
else
" Assume this is a string
let output = a:output
endif
let s:async_sync_outputs[a:async_sync_id] = output " job can now be garbage collected
endfunction
" Executes `cmd` asynchronously but looks synchronous to the caller.
function! kite#async#sync(cmd)
let async_sync_id = s:next_async_sync_id()
let job_handle = kite#async#execute(a:cmd, function('s:async_sync_output', [async_sync_id]))
let s:async_sync_outputs[async_sync_id] = job_handle
let job_type = type(job_handle) " Assume not a string
let vim = !has('nvim')
while type(s:async_sync_outputs[async_sync_id]) == job_type
if vim | call job_status(job_handle) | endif
sleep 5m
endwhile
let output = s:async_sync_outputs[async_sync_id]
unlet s:async_sync_outputs[async_sync_id]
return output
endfunction
" Optional argument is data (JSON) to pass to cmd's stdin.
" Returns the job / job id.
function! kite#async#execute(cmd, handler, ...)
let options = {
\ 'stdoutbuffer': [],
\ 'handler': a:handler,
\ }
let command = s:build_command(a:cmd)
if has('nvim')
let jobid = jobstart(command, extend(options, {
\ 'on_stdout': function('s:on_stdout_nvim'),
\ 'on_exit': function('s:on_exit_nvim')
\ }))
if a:0
call chansend(jobid, a:1)
call chanclose(jobid, 'stdin')
endif
return jobid
else
let job = job_start(command, {
\ 'out_cb': function('s:on_stdout_vim', options),
\ 'exit_cb': function('s:on_exit_vim', options)
\ })
if a:0
let channel = job_getchannel(job)
call ch_sendraw(channel, a:1)
call ch_close_in(channel)
endif
return job
endif
endfunction
function! s:build_command(cmd)
if has('nvim')
if has('unix')
return ['sh', '-c', a:cmd]
elseif has('win64') || has('win32')
return ['cmd.exe', '/c', a:cmd]
else
throw 'unknown os'
endif
else
if has('unix')
return ['sh', '-c', a:cmd]
elseif has('win64') || has('win32')
return 'cmd.exe /c '.a:cmd
else
throw 'unknown os'
endif
endif
endfunction
function! s:on_stdout_vim(_channel, data) dict
" a:data - an output line
call add(self.stdoutbuffer, a:data)
endfunction
function! s:on_exit_vim(job, exit_status) dict
" Allow time for any buffered data to trigger out_cb.
" 5m is an educated guess.
sleep 5m
call self.handler(self.stdoutbuffer)
endfunction
function! s:on_stdout_nvim(_job_id, data, event) dict
if empty(self.stdoutbuffer)
let self.stdoutbuffer = a:data
else
let self.stdoutbuffer = self.stdoutbuffer[:-2] +
\ [self.stdoutbuffer[-1] . a:data[0]] +
\ a:data[1:]
endif
endfunction
function! s:on_exit_nvim(_job_id, _data, _event) dict
call map(self.stdoutbuffer, 'substitute(v:val, "\r$", "", "")')
call self.handler(self.stdoutbuffer)
endfunction

View file

@ -0,0 +1,329 @@
let s:port = empty($KITED_TEST_PORT) ? 46624 : $KITED_TEST_PORT
let s:channel_base = 'localhost:'.s:port
let s:base_url = 'http://127.0.0.1:'.s:port
let s:editor_path = '/clientapi/editor'
let s:onboarding_path = '/clientapi/plugins/onboarding_file?editor=vim'
let s:hover_path = '/api/buffer/vim'
let s:docs_path = 'kite://docs/'
let s:status_path = '/clientapi/status?filename='
let s:languages_path = '/clientapi/languages'
let s:copilot_path = 'kite://home'
let s:counter_path = '/clientapi/metrics/counters'
let s:settings_path = 'kite://settings'
let s:permissions_path = 'kite://settings/permissions'
let s:max_file_size_path = '/clientapi/settings/max_file_size_kb'
let s:codenav_path = '/codenav/editor/related'
function! kite#client#docs(word)
let url = s:docs_path.a:word
call kite#utils#browse(url)
endfunction
function! kite#client#settings()
call kite#utils#browse(s:settings_path)
endfunction
function! kite#client#permissions()
call kite#utils#browse(s:permissions_path)
endfunction
function! kite#client#copilot()
call kite#utils#browse(s:copilot_path)
endfunction
function! kite#client#counter(json, handler)
let path = s:counter_path
if has('channel')
call s:async(function('s:timer_post', [path, g:kite_long_timeout, a:json, a:handler]))
else
call kite#async#execute(s:external_http_cmd(s:base_url.path, g:kite_long_timeout, 1), a:handler, a:json)
endif
endfunction
function! kite#client#onboarding_file(handler)
let path = s:onboarding_path
if has('channel')
let response = s:internal_http(path, g:kite_short_timeout)
else
let response = s:external_http(s:base_url.path, g:kite_short_timeout)
endif
return a:handler(s:parse_response(response))
endfunction
function! kite#client#status(filename, handler)
let path = s:status_path.kite#utils#url_encode(a:filename)
if has('channel')
let response = s:internal_http(path, g:kite_short_timeout)
else
let response = s:external_http(s:base_url.path, g:kite_short_timeout)
endif
return a:handler(s:parse_response(response))
endfunction
function! kite#client#languages(handler)
let path = s:languages_path
if has('channel')
let response = s:internal_http(path, g:kite_short_timeout)
else
let response = s:external_http(s:base_url.path, g:kite_short_timeout)
endif
return a:handler(s:parse_response(response))
endfunction
" Returns max file size in bytes, or -1 if not available.
function! kite#client#max_file_size()
let path = s:max_file_size_path
if has('channel')
let response = s:internal_http(path, g:kite_short_timeout)
else
let response = s:external_http(s:base_url.path, g:kite_short_timeout)
endif
let result = s:parse_response(response)
if result.status == 200
return result.body * 1024
else
return -1
endif
endfunction
function! kite#client#hover(filename, hash, cursor, handler)
call s:wait_for_pending_events()
let path = s:hover_path.'/'.a:filename.'/'.a:hash.'/hover?cursor_runes='.a:cursor
if has('channel')
call s:async(function('s:timer_get', [path, g:kite_long_timeout, a:handler]))
else
call kite#async#execute(s:external_http_cmd(s:base_url.path, g:kite_long_timeout, 0),
\ function('s:parse_and_handle', [a:handler]))
endif
endfunction
function! kite#client#signatures(json, handler)
let path = s:editor_path.'/signatures'
if has('channel')
call s:async(function('s:timer_post', [path, g:kite_long_timeout, a:json, a:handler]))
else
call kite#async#execute(s:external_http_cmd(s:base_url.path, g:kite_long_timeout, 1),
\ function('s:parse_and_handle', [a:handler]), a:json)
endif
endfunction
function! kite#client#completions(json, handler)
let path = s:editor_path.'/complete'
if has('channel')
call s:async(function('s:timer_post', [path, g:kite_long_timeout, a:json, a:handler]))
else
call kite#async#execute(s:external_http_cmd(s:base_url.path, g:kite_long_timeout, 1),
\ function('s:parse_and_handle', [a:handler]), a:json)
endif
endfunction
function! kite#client#request_related(json, handler)
let path = s:codenav_path
let timeout = 10000 "10s
if has('channel')
call s:async(function('s:timer_post', [path, timeout, a:json, a:handler]))
else
call kite#async#execute(s:external_http_cmd(s:base_url.path, timeout, 1),
\ function('s:parse_and_handle', [a:handler]), a:json)
endif
endfunction
function! kite#client#post_event(json, handler)
let path = s:editor_path.'/event'
if has('channel')
call s:async(function('s:timer_post', [path, g:kite_short_timeout, a:json, a:handler]))
else
call kite#async#execute(s:external_http_cmd(s:base_url.path, g:kite_short_timeout, 1),
\ function('s:parse_and_handle', [a:handler]), a:json)
endif
endfunction
function! s:timer_get(path, timeout, handler, timer)
call a:handler(s:parse_response(s:internal_http(a:path, a:timeout)))
endfunction
function! s:timer_post(path, timeout, json, handler, timer)
call a:handler(s:parse_response(s:internal_http(a:path, a:timeout, a:json)))
endfunction
function! s:async(callback)
call timer_start(0, a:callback)
endfunction
function! s:on_std_out(_channel, message) dict
let self.stdoutbuffer .= a:message
endfunction
" Optional argument is json to be posted
function! s:internal_http(path, timeout, ...)
" Use HTTP 1.0 (not 1.1) to avoid having to parse chunked responses.
if a:0
let str = 'POST '.a:path." HTTP/1.0\nHost: localhost\nContent-Type: application/x-www-form-urlencoded\nContent-Length: ".len(a:1)."\n\n".a:1
else
let str = 'GET '.a:path." HTTP/1.0\nHost: localhost\n\n"
endif
call kite#utils#log('')
call kite#utils#log(map(split(str, '\n', 1), '"> ".v:val'))
let options = {'stdoutbuffer': ''}
try
let channel = ch_open(s:channel_base, {
\ 'mode': 'raw',
\ 'callback': function('s:on_std_out', options)
\ })
catch /E898\|E901\|E902/
call kite#utils#log('| Cannot open channel: '.str)
return ''
endtry
try
call ch_sendraw(channel, str)
catch /E630\|E631\|E906/
call kite#utils#log('| Cannot send over channel: '.str)
return ''
endtry
let start = reltime()
while ch_status(channel) !=# 'closed'
if reltimefloat(reltime(start))*1000 > a:timeout
call kite#utils#log('| Timed out waiting for response (timeout: '.a:timeout.'ms)')
try
call ch_close(channel)
catch /E906/
" noop
endtry
return ''
endif
sleep 5m
endwhile
call kite#utils#log('| Received complete response: '.string(reltimefloat(reltime(start))*1000).'ms')
return options.stdoutbuffer
endfunction
" Optional argument is json to be posted
function! s:external_http(url, timeout, ...)
let cmd = s:external_http_cmd(a:url, a:timeout, a:0)
if a:0
return system(cmd, a:1)
else
return system(cmd)
endif
endif
endfunction
" data argument is a boolean
function! s:external_http_cmd(endpoint, timeout, data)
let cmd = s:http_binary
let cmd .= ' --timeout '.a:timeout.'ms'
if a:data
let cmd .= ' -'
endif
let cmd .= ' '.s:shellescape(a:endpoint)
call kite#utils#log('')
call kite#utils#log('> '.cmd)
return cmd
endfunction
function! s:parse_and_handle(handler, out)
call a:handler(s:parse_response(a:out))
endfunction
" Returns the integer HTTP response code and the string body in a dictionary.
"
" lines - either a list (from async commands) or a string (from sync)
function! s:parse_response(lines)
if empty(a:lines)
return {'status': 0, 'body': ''}
endif
if type(a:lines) == v:t_string
let lines = split(a:lines, '\r\?\n', 1)
else
let lines = a:lines
endif
call kite#utils#log(map(copy(lines), '"< ".v:val'))
if type(a:lines) == v:t_string
let lines = split(a:lines, '\r\?\n')
else
let lines = a:lines
endif
" Ignore occasional 100 Continue.
let i = match(lines, '^HTTP/1.[01] [2345]\d\d ')
if i == -1
return {'status': 0, 'body': ''}
endif
let status = split(lines[i], ' ')[1]
let sep = match(lines, '^$', i)
let body = join(lines[sep+1:], "\n")
return {'status': status, 'body': body}
endfunction
function! s:wait_for_pending_events()
while kite#events#any_events_pending()
sleep 5m
endwhile
endfunction
" Only used with NeoVim on not-Windows, in async jobs.
function! s:shellescape(str)
let [_shell, &shell] = [&shell, 'sh']
let escaped = shellescape(a:str)
let &shell = _shell
return escaped
endfunction
let s:http_binary = kite#utils#lib('kite-http')
if !empty($KITED_TEST_PORT)
function! kite#client#request_history()
let ret = json_decode(
\ s:parse_response(
\ s:internal_http('/testapi/request-history', 500)
\ ).body
\ )
if type(ret) != type([])
throw '/testapi/request-history did not return a list (type '.type(ret).')'
endif
return ret
endfunction
function! kite#client#reset_request_history()
call s:internal_http('/testapi/request-history/reset', 500)
endfunction
endif

View file

@ -0,0 +1,39 @@
function! kite#codenav#from_file()
let filepath = kite#utils#filepath(0)
call kite#codenav#request_related(filepath, v:null)
endfunction
function! kite#codenav#from_line()
let filepath = kite#utils#filepath(0)
call kite#codenav#request_related(filepath, line("."))
endfunction
function! kite#codenav#request_related(filename, line)
let json = json_encode({
\ 'editor': 'vim',
\ 'location': {'filename': a:filename, 'line': a:line}
\ })
call kite#client#request_related(json, function('kite#codenav#handler'))
endfunction
function! kite#codenav#handler(response) abort
if a:response.status != 200
if a:response.status == 0
call kite#utils#warn("Kite could not be reached. Please check that Kite Engine is running.")
return
endif
let err = json_decode(a:response.body)
if empty(err) || type(err.message) != v:t_string
call kite#utils#warn("Oops! Something went wrong with Code Finder. Please try again later.")
return
endif
call kite#utils#warn(err.message)
endif
endfunction

View file

@ -0,0 +1,454 @@
let s:should_trigger_completion = 0
let s:completion_counter = 0
let s:begin = 0
let s:end = 0
function! kite#completion#replace_range()
if empty(v:completed_item) | return | endif
if !exists('s:startcol') | return | endif
let startcol = s:startcol
unlet s:startcol
if has_key(v:completed_item, 'user_data') && !empty(v:completed_item.user_data)
let range = json_decode(v:completed_item.user_data).range
let placeholders = json_decode(v:completed_item.user_data).placeholders
elseif exists('b:kite_completions') && has_key(b:kite_completions, v:completed_item.word)
let range = json_decode(b:kite_completions[v:completed_item.word]).range
let placeholders = json_decode(b:kite_completions[v:completed_item.word]).placeholders
else
return
endif
" The range seems to be wrong when placeholders are involved so stop here.
if !empty(placeholders) | return | endif
let col = col('.')
let _col = col
" end of range
let n = range.end - s:offset_before_completion
if n > 0
execute 'normal! "_'.n.'x'
let col -= n
endif
" start of range
let range_begin_col = col('.') - (kite#utils#character_offset() - range.begin)
let n = startcol - range_begin_col
if n > 0
call kite#utils#goto_character(range.begin + 1)
execute 'normal! "_'.n.'x'
let col -= n
endif
" restore cursor position
if col != _col
execute 'normal!' (col+1).'|'
call s:feedkeys("\<Esc>la")
endif
endfunction
function! kite#completion#expand_newlines()
if empty(v:completed_item) | return | endif
if match(v:completed_item.word, '\n') == -1 | return | endif
let parts = split(getline('.'), '\n', 1)
delete _
call append(line('.')-1, parts)
-1
" startinsert! doesn't seem to work with: package main^@import ""^@
call s:feedkeys("\<Esc>A")
endfunction
function! kite#completion#insertcharpre()
let s:should_trigger_completion = 1
" Trigger a fresh completion after every keystroke when the popup menu
" is visible (by calling the function which TextChangedI would call
" (TextChangedI is not triggered when the popup menu is visible)).
if pumvisible()
call kite#utils#log('# Trigger autocomplete because of pumvisible(): '.v:char)
call kite#completion#autocomplete()
endif
endfunction
function! kite#completion#autocomplete()
if !g:kite_auto_complete | return | endif
if exists('b:kite_skip') && b:kite_skip | return | endif
if wordcount().bytes > kite#max_file_size() | return | endif
if s:should_trigger_completion
let s:should_trigger_completion = 0
call s:feedkeys("\<C-X>\<C-U>")
endif
endfunction
" Manual invocation calls this method.
function! kite#completion#complete(findstart, base)
if a:findstart
if !s:completeopt_suitable()
let g:kite_auto_complete = 0
return -3
endif
" Store the buffer contents and cursor position here because when Vim
" calls this function the second time (with a:findstart == 0) Vim has
" already deleted the text between `start` and the cursor position.
let s:cursor = kite#utils#character_offset()
let s:text = kite#utils#buffer_contents()
let s:startcol = s:findstart()
return s:startcol
else
" Leave CTRL-X submode so user can invoke other completion methods.
call s:feedkeys("\<C-e>")
call s:get_completions()
if has('patch-8.1.0716')
return v:none
else
return []
endif
endif
endfunction
function! s:findstart()
let line = getline('.')
let start = col('.') - 1
let s:signature = s:before_function_call_argument(line[:start-1]) && s:begin == 0
if !s:signature
while start > 0 && line[start - 1] =~ '\w'
let start -= 1
endwhile
endif
return start
endfunction
function! s:get_completions()
if s:signature
call kite#signature#increment_completion_counter()
else
let s:completion_counter = s:completion_counter + 1
endif
let filename = kite#utils#filepath(0)
if s:signature
let params = {
\ 'filename': filename,
\ 'editor': 'vim',
\ 'text': s:text,
\ 'cursor_runes': s:cursor,
\ 'offset_encoding': 'utf-32'
\ }
else
let params = {
\ 'no_snippets': (g:kite_snippets ? v:false : v:true),
\ 'no_unicode': (kite#utils#windows() ? v:true : v:false),
\ 'filename': filename,
\ 'editor': 'vim',
\ 'text': s:text,
\ 'position': {
\ 'begin': (s:begin > 0 ? s:begin : s:cursor),
\ 'end': (s:end > 0 ? s:end : s:cursor),
\ },
\ 'offset_encoding': 'utf-32',
\ 'placeholders': []
\ }
let s:begin = 0
let s:end = 0
endif
let json = json_encode(params)
if s:signature
call kite#client#signatures(json, function('kite#signature#handler', [kite#signature#completion_counter(), s:startcol]))
else
call kite#client#completions(json, function('kite#completion#handler', [s:completion_counter, s:startcol]))
endif
endfunction
function! kite#completion#handler(counter, startcol, response) abort
call kite#utils#log('completion: '.a:response.status)
" Ignore old completion results.
if a:counter != s:completion_counter
return
endif
if a:response.status != 200
return
endif
" This should not happen but evidently it sometimes does (#107).
if empty(a:response.body)
return
endif
let json = json_decode(a:response.body)
" API should return 404 status when no completions but it sometimes
" return 200 status and an empty response body, or "completions":"null".
if empty(json) || type(json.completions) != v:t_list
return
endif
" 'display' is the LHS of each option in the completion menu
let max_display_length = s:max_display_length(json.completions, 0)
" 'hint' is the RHS of each option in the completion menu
" Add 1 for leading space we add
let max_hint_length = s:max_hint_length(json.completions) + 1
let available_win_width = s:winwidth() - a:startcol
let max_width = available_win_width > g:kite_completion_max_width
\ ? g:kite_completion_max_width : available_win_width
" pad LHS text gap RHS text gap kite branding pad scrollbar
" | | | | | | | |
let menu_width = 1 + max_display_length + 1 + max_hint_length + 1 + strdisplaywidth(kite#symbol()) + 2 + 1
if menu_width < max_width " no truncation
let lhs_width = max_display_length
let rhs_width = max_hint_length
elseif menu_width - 1 - max_hint_length < max_width " truncate rhs
let lhs_width = max_display_length
let rhs_width = max_width - (1 + max_display_length + 1 + strdisplaywidth(kite#symbol()) + 2 + 1)
else " drop rhs and truncate lhs
let lhs_width = max_width - (1 + 1 + strdisplaywidth(kite#symbol()) + 2 + 1)
let rhs_width = 0
endif
let matches = []
for c in json.completions
call add(matches, s:adapt(c, lhs_width, rhs_width, 0))
if has_key(c, 'children')
for child in c.children
call add(matches, s:adapt(child, lhs_width, rhs_width, 1))
endfor
endif
endfor
if !has('patch-8.0.1493')
let b:kite_completions = {}
for item in filter(copy(matches), 'has_key(v:val, "user_data")')
let b:kite_completions[item.word] = item.user_data
endfor
endif
if mode(1) ==# 'i'
let s:startcol = a:startcol+1
let s:offset_before_completion = kite#utils#character_offset()
call complete(a:startcol+1, matches)
endif
endfunction
function! s:adapt(completion_option, lhs_width, rhs_width, nesting)
let display = s:indent(a:nesting) . a:completion_option.display
let display = kite#utils#truncate(display, a:lhs_width)
" Ensure a minimum separation between abbr and menu of two spaces.
" (Vim lines up the menus so that they are left-aligned 1 space after the longest abbr).
let hint = ' ' . a:completion_option.hint
let hint = kite#utils#ralign(hint, a:rhs_width)
" Add the branding
let hint .= ' '.kite#symbol()
return {
\ 'word': a:completion_option.snippet.text,
\ 'abbr': display,
\ 'info': a:completion_option.documentation.text,
\ 'menu': hint,
\ 'equal': 1,
\ 'user_data': json_encode({'placeholders': a:completion_option.snippet.placeholders, 'range': a:completion_option.replace})
\ }
endfunction
function! s:max_hint_length(completions)
let max = 0
for e in a:completions
let len = strdisplaywidth(e.hint)
if len > max
let max = len
endif
if has_key(e, 'children')
let len = s:max_hint_length(e.children)
if len > max
let max = len
endif
endif
endfor
return max
endfunction
function! s:max_display_length(completions, nesting)
let max = 0
for e in a:completions
let len = strdisplaywidth(s:indent(a:nesting) . e.display)
if len > max
let max = len
endif
if has_key(e, 'children')
let len = s:max_display_length(e.children, a:nesting+1)
if len > max
let max = len
endif
endif
endfor
return max
endfunction
function! s:indent(nesting)
return repeat(' ', a:nesting)
endfunction
" Returns truthy if the cursor is:
"
" - just after an open parenthesis; or
" - just after a comma inside a function call; or
" - just after an equals sign inside a function call.
"
" Note this differs from all the other editor plugins. They can all show both
" a signature popup and a completions popup at the same time, whereas Vim can
" only show one popup. Therefore we need to switch its purpose between
" signature info and completions info at appropriate points inside a function
" call's arguments.
"
" line - the line up to the cursor position
function! s:before_function_call_argument(line)
" Other editors basically do this:
" return a:line =~ '\v[(][^)]*$'
return a:line =~ '\v[(]([^)]+[=,])?\s*$'
endfunction
" Returns the width of the part of the current window which holds the buffer contents.
function! s:winwidth()
let w = winwidth(0)
if &number
let w -= &numberwidth
endif
let w -= &foldcolumn
if &signcolumn == 'yes' || (&signcolumn == 'auto' && s:signs_in_buffer())
" TODO: neovim multiple sign columns
let w -= 2
endif
return w
endfunction
" Returns 1 if the current buffer has any signs, 0 otherwise.
function! s:signs_in_buffer()
let bufinfo = getbufinfo(bufnr(''))[0]
let signs = has_key(bufinfo, 'signs') ? bufinfo.signs : []
return !empty(signs)
endfunction
function! s:completeopt_suitable()
let copts = split(&completeopt, ',')
if g:kite_auto_complete
if index(copts, 'menuone') == -1
call s:popup_warn("Kite: completeopt must contain 'menuone'")
return 0
endif
if index(copts, 'noinsert') == -1 && index(copts, 'noselect') == -1
call s:popup_warn("Kite: completeopt must contain 'noinsert' and/or 'noselect'")
return 0
endif
endif
return 1
endfunction
" feedkeys() by default adds keys to the end of the typeahead buffer. Any
" keys already in the buffer will be processed first and may change Vim's
" state, making the queued keys no longer appropriate (e.g. an insert mode key
" combo being applied in normal mode). To avoid this we use the 'i' flag
" which ensures the keys are processed immediately.
function s:feedkeys(keys)
call feedkeys(a:keys, 'i')
endfunction
function! s:popup_warn(msg)
if exists('*popup_notification')
call popup_notification(a:msg, {
\ 'pos': 'botleft',
\ 'line': 'cursor-1',
\ 'col': 'cursor',
\ 'moved': 'any',
\ 'time': 2000
\ })
elseif exists('*nvim_open_win')
let lines = s:border(a:msg)
let buf = nvim_create_buf(v:false, v:true)
call nvim_buf_set_lines(buf, 0, -1, v:true, lines)
let winid = nvim_open_win(buf, v:false, {
\ 'relative': 'cursor',
\ 'anchor': 'SW',
\ 'row': 0,
\ 'col': 0,
\ 'width': strdisplaywidth(lines[0]),
\ 'height': len(lines),
\ 'focusable': v:false,
\ 'style': 'minimal'
\ })
call nvim_win_set_option(winid, 'winhighlight', 'Normal:WarningMsg')
call timer_start(2000, {-> execute("call nvim_win_close(".winid.", v:true)")})
else
call kite#utils#warn(a:msg)
endif
endfunction
" Converts:
"
" A quick brown fox.
"
" Into:
"
" +--------------------+
" | A quick brown fox. |
" +--------------------+
"
function! s:border(text)
return [
\ '+'.repeat('-', strdisplaywidth(a:text)+2).'+',
\ '| '.a:text.' |',
\ '+'.repeat('-', strdisplaywidth(a:text)+2).'+'
\ ]
endfunction

View file

@ -0,0 +1,24 @@
function! kite#docs#docs()
if &filetype != 'python'
call kite#utils#warn('Docs are only available for Python')
return
endif
if empty(expand('<cword>')) | return | endif
let b:kite_id = ''
call kite#hover#hover()
while b:kite_id == ''
sleep 5m
endwhile
if b:kite_id == -1
call kite#utils#info('No documentation available.')
return
endif
call kite#client#docs(b:kite_id)
endfunction

View file

@ -0,0 +1,42 @@
let kite#document#Document = {}
" NOTE: this has to be called with a g: prefix.
function! kite#document#Document.New(dict)
let newDocument = copy(self)
let newDocument.dict = a:dict
return newDocument
endfunction
" Query the document, returning `default` if `key` does not exist
" or if the value at `key` is not the same type as `default`.
function! kite#document#Document.dig(key, default)
let v = copy(self.dict)
for k in split(a:key, '\.')
let matchlist = matchlist(k, '\v(\w+)\[(-?\d+)\]') " foo[42]
if !empty(matchlist)
let kk = matchlist[1] " foo
if has_key(v, kk)
let v = get(v[kk], str2nr(matchlist[2]), a:default)
else
return a:default
endif
elseif type(v) == v:t_dict && has_key(v, k)
let v = v[k]
else
return a:default
endif
endfor
if type(v) == type(a:default)
return v
endif
return a:default
endfunction

View file

@ -0,0 +1,46 @@
let s:events_pending = 0
function! kite#events#any_events_pending()
return s:events_pending > 0
endfunction
function! kite#events#event(action)
let filename = kite#utils#filepath(0)
if wordcount().bytes < kite#max_file_size()
let action = a:action
let text = kite#utils#buffer_contents()
else
let action = 'skip'
let text = ''
endif
let [sel_start, sel_end] = kite#utils#selected_region_characters()
if [sel_start, sel_end] == [-1, -1]
return
endif
let selections = [{ 'start': sel_start, 'end': sel_end, 'encoding': 'utf-32' }]
let json = json_encode({
\ 'source': 'vim',
\ 'filename': filename,
\ 'text': text,
\ 'action': action,
\ 'selections': selections,
\ 'editor_version': kite#utils#vim_version(),
\ 'plugin_version': kite#utils#plugin_version()
\ })
let s:events_pending += 1
call kite#client#post_event(json, function('kite#events#handler', [bufnr('')]))
endfunction
function! kite#events#handler(bufnr, response)
let s:events_pending -= 1
call setbufvar(a:bufnr, 'kite_skip', (a:response.status == 0 || a:response.status == 403))
endfunction

View file

@ -0,0 +1,66 @@
function! kite#hover#hover()
if exists('b:kite_skip') && b:kite_skip | return | endif
if wordcount().bytes > kite#max_file_size() | return | endif
let filename = kite#utils#filepath(1)
let hash = kite#utils#buffer_md5()
let cursor = kite#utils#cursor_characters()
call kite#client#hover(filename, hash, cursor, function('kite#hover#handler'))
endfunction
function! kite#hover#goto_definition()
if &filetype != 'python'
call kite#utils#warn('Go to definition is only available for Python')
return
endif
if exists('b:kite_skip') && b:kite_skip | return | endif
if wordcount().bytes > kite#max_file_size() | return | endif
let filename = kite#utils#filepath(1)
let hash = kite#utils#buffer_md5()
let cursor = kite#utils#cursor_characters()
call kite#client#hover(filename, hash, cursor, function('kite#hover#goto_definition_handler'))
endfunction
function! kite#hover#handler(response)
if a:response.status == 200
let json = json_decode(a:response.body)
let sym = type(json.symbol) == v:t_list ? json.symbol[0] : json.symbol
let id = sym.id
if empty(id)
let b:kite_id = -1
else
let b:kite_id = id
endif
else
let b:kite_id = -1
endif
endfunction
function! kite#hover#goto_definition_handler(response)
if a:response.status != 200
call kite#utils#warn('unable to find a definition.')
return
endif
let json = json_decode(a:response.body)
let definition = json.report.definition
if type(definition) != type({})
call kite#utils#warn('unable to find a definition.')
return
endif
if definition.filename !=# expand('%:p')
execute 'edit' definition.filename
end
execute definition.line
normal! zz
endfunction

View file

@ -0,0 +1,65 @@
let s:languages_supported_by_kited = []
" Returns true if we want Kite completions for the current buffer, false otherwise.
function! kite#languages#supported_by_plugin()
" Return false if the file extension is not recognised by kited.
let recognised_extensions = [
\ 'c',
\ 'cc',
\ 'cpp',
\ 'cs',
\ 'css',
\ 'go',
\ 'h',
\ 'hpp',
\ 'html',
\ 'java',
\ 'js',
\ 'jsx',
\ 'kt',
\ 'less',
\ 'm',
\ 'php',
\ 'py',
\ 'pyw',
\ 'rb',
\ 'scala',
\ 'sh',
\ 'ts',
\ 'tsx',
\ 'vue',
\ ]
if index(recognised_extensions, expand('%:e')) == -1
return 0
endif
if g:kite_supported_languages == ['*']
return 1
endif
" Return false if the buffer's language is not one for which we want Kite completions.
if index(g:kite_supported_languages, &filetype) == -1
return 0
endif
return 1
endfunction
" Returns true if the current buffer's language is supported by kited, false otherwise.
function! kite#languages#supported_by_kited()
" Only check kited's languages once.
if empty(s:languages_supported_by_kited)
" A list of language names, e.g. ['bash', 'c', 'javascript', 'ruby', ...]
let s:languages_supported_by_kited = kite#client#languages(function('kite#languages#handler'))
endif
return index(s:languages_supported_by_kited, &filetype) != -1
endfunction
function! kite#languages#handler(response)
if a:response.status != 200 | return [] | endif
return json_decode(a:response.body)
endfunction

View file

@ -0,0 +1,29 @@
"
" Editor feature metrics
"
let s:prompted = 0
" Optional argument is value by which to increment named metric.
" Defaults to 1.
function! kite#metrics#requested(name)
call s:increment('vim_'.a:name.'_requested')
endfunction
function! kite#metrics#fulfilled(name)
call s:increment('vim_'.a:name.'_fulfilled')
endfunction
function! s:increment(name)
let json = json_encode({'name': a:name, 'value': 1})
call kite#client#counter(json, function('kite#metrics#handler'))
endfunction
function! kite#metrics#handler(response)
" Noop
endfunction

View file

@ -0,0 +1,77 @@
let s:text = [
\ 'Kite is now integrated with Vim',
\ '',
\ 'Kite is an AI-powered programming assistant',
\ 'that shows you the right information at the right',
\ 'time to keep you in the flow.',
\ '',
\ 'Please choose:',
\ '',
\ 'Learn how to use Kite',
\ 'Hide',
\ 'Hide forever',
\ ]
let s:option = 'onboarding_required'
function! kite#onboarding#call(force)
if !a:force
if !kite#utils#get_setting(s:option, 1)
return
endif
endif
call kite#client#onboarding_file(function('kite#onboarding#handler'))
endfunction
function! kite#onboarding#handler(response) abort
if a:response.status == 200
silent execute 'tabedit' json_decode(a:response.body)
call kite#utils#set_setting(s:option, 0)
else
if exists('*popup_menu')
if !has('patch-8.1.1799')
call s:unmap_menu_keys()
endif
let title = s:text[0]
let winid = popup_menu(s:text[1:], {
\ 'title': ' '.title.' ',
\ 'callback': 'kite#onboarding#popup_callback',
\ })
call win_execute(winid, "normal! ".repeat('j', len(s:text[1:])-3))
else
let s:text[-3] = '1. '.s:text[-3]
let s:text[-2] = '2. '.s:text[-2]
let s:text[-1] = '3. '.s:text[-1]
call s:handle_choice(inputlist(s:text)-1)
endif
endif
endfunction
" Invoked when popup closes.
function! kite#onboarding#popup_callback(_, result)
call s:handle_choice(2 - len(s:text[1:]) + a:result)
endfunction
function! s:handle_choice(index)
if a:index == 0 " learn now
call kite#utils#browse('https://help.kite.com/category/47-vim-integration')
elseif a:index == 2 " hide forever
call kite#utils#set_setting(s:option, 0)
endif
endfunction
function! s:unmap_menu_keys()
silent! nunmap <CR>
silent! nunmap <Space>
silent! nunmap j
silent! nunmap k
silent! nunmap x
endfunction

View file

@ -0,0 +1,209 @@
let s:completion_counter = 0
function! kite#signature#increment_completion_counter()
let s:completion_counter = s:completion_counter + 1
endfunction
function! kite#signature#completion_counter()
return s:completion_counter
endfunction
function! kite#signature#handler(counter, startcol, response) abort
call kite#utils#log('signature: '.a:response.status)
" Ignore old completion results.
if a:counter != s:completion_counter
return
endif
if a:response.status != 200
return
endif
let json = json_decode(a:response.body)
let call = g:kite#document#Document.New(json.calls[0])
let function_name = call.dig('func_name', '')
if empty(function_name)
let function_name = call.dig('callee.repr', '')
endif
let function_name = split(function_name, '\.')[-1]
let spacer = {'word': '', 'empty': 1, 'dup': 1}
let indent = ' '
let completions = []
let wrap_width = 50
"
" Signature
"
let parameters = []
let return_type = ''
let [current_arg, in_kwargs] = [call.dig('arg_index', 0), call.dig('language_details.python.in_kwargs', 0)]
let kind = call.dig('callee.kind', '')
" 1. Name of function with parameters.
if kind ==# 'function'
" 1.b.1. Parameters
for parameter in call.dig('callee.details.function.parameters', [])
" 1.b.1.a. Name
let name = parameter.name
" 1.b.1.b. Default value
if kite#utils#present(parameter.language_details.python, 'default_value')
let name .= '='.parameter.language_details.python.default_value[0].repr
endif
" 2. Highlight current argument
if !in_kwargs && len(parameters) == current_arg
let name = '*'.name.'*'
endif
call add(parameters, name)
endfor
" 1.b.2. vararg indicator
let vararg = call.dig('callee.details.function.language_details.python.vararg', {})
if !empty(vararg)
call add(parameters, '*'.vararg.name)
endif
" 1.b.3. keyword arguments indicator
let kwarg = call.dig('callee.details.function.language_details.python.kwarg', {})
if !empty(kwarg)
call add(parameters, '**'.kwarg.name)
endif
" 1.b.4. Return type
let return_value = call.dig('callee.details.function.return_value', [])
if !empty(return_value)
let return_type = ' -> '.return_value[0].type
endif
elseif kind ==# 'type'
" 1.c.1. Parameters
for parameter in call.dig('callee.details.type.language_details.python.constructor.parameters', [])
" 1.c.1.a. Name
let name = parameter.name
" 1.c.1.b. Default value
if kite#utils#present(parameter.language_details.python, 'default_value')
let name .= '='.parameter.language_details.python.default_value[0].repr
endif
" 2. Highlight current argument
if !in_kwargs && len(parameters) == current_arg
let name = '*'.name.'*'
endif
call add(parameters, name)
endfor
" 1.c.2. vararg indicator
let vararg = call.dig('callee.details.type.language_details.python.constructor.language_details.python.vararg', {})
if !empty(vararg)
call add(parameters, '*'.vararg.name)
endif
" 1.c.3. keyword arguments indicator
let kwarg = call.dig('callee.details.type.language_details.python.constructor.language_details.python.kwarg', {})
if !empty(kwarg)
call add(parameters, '*'.kwarg.name)
endif
" 1.c.4. Return type
let return_type = ' -> '.function_name
endif
" The completion popup does not wrap long lines so we wrap manually.
for line in kite#utils#wrap(kite#symbol().' '.function_name.'('.join(parameters, ', ').')'.return_type, wrap_width, 4)
let completion = {
\ 'word': '',
\ 'abbr': line,
\ 'empty': 1,
\ 'dup': 1
\ }
call add(completions, completion)
endfor
" 3. Keyword arguments
let kwarg_parameters = call.dig('callee.details.function.kwarg_parameters', [])
if !empty(kwarg_parameters)
call add(completions, spacer)
call add(completions, s:heading('**kw'))
for kwarg in kwarg_parameters
let name = kwarg.name
let types = kite#utils#map_join(kwarg.inferred_value, 'repr', '|')
if empty(types)
let types = ''
endif
call add(completions, {
\ 'word': name.'=',
\ 'abbr': indent.name,
\ 'menu': types,
\ 'empty': 1,
\ 'dup': 1
\ })
endfor
endif
" 4. Popular patterns
if kite#signature#should_show_popular_patterns()
let signatures = call.dig('signatures', [])
if len(signatures) > 0
call add(completions, spacer)
call add(completions, s:heading('How Others Used This'))
endif
for signature in signatures
let sigdoc = g:kite#document#Document.New(signature)
" b. Arguments
let arguments = []
for arg in sigdoc.dig('args', [])
call add(arguments, arg.name)
endfor
" c. Keyword arguments
for kwarg in sigdoc.dig('language_details.python.kwargs', [])
let name = kwarg.name
let examples = kite#utils#coerce(kwarg.types[0], 'examples', [])
if len(examples) > 0
let name .= '='.examples[0]
endif
call add(arguments, name)
endfor
for line in kite#utils#wrap(function_name.'('.join(arguments, ', ').')', wrap_width, 2)
let completion = {
\ 'word': '',
\ 'abbr': indent.line,
\ 'empty': 1,
\ 'equal': 1,
\ 'dup': 1
\ }
call add(completions, completion)
endfor
endfor
endif
if mode(1) ==# 'i'
call complete(a:startcol+1, completions)
endif
endfunction
function! kite#signature#should_show_popular_patterns()
return kite#utils#get_setting('show_popular_patterns', 0)
endfunction
function! kite#signature#show_popular_patterns()
call kite#utils#set_setting('show_popular_patterns', 1)
endfunction
function! kite#signature#hide_popular_patterns()
call kite#utils#set_setting('show_popular_patterns', 0)
endfunction
function s:heading(text)
return {'abbr': a:text.':', 'word': '', 'empty': 1, 'dup': 1}
endfunction

View file

@ -0,0 +1,413 @@
function! s:setup_stack()
if exists('b:kite_stack') | return | endif
" stack:
" [
" { index: 0, placeholders: { ... } }, <-- depth 0
" { index: 0, placeholders: { ... } }, <-- depth 1
" ... <-- depth n
" ]
"
" index - the currently active placeholder at that depth
let b:kite_stack = {'stack': []}
function! b:kite_stack.pop()
return remove(self.stack, -1)
endfunction
function! b:kite_stack.peek()
return get(self.stack, -1)
endfunction
function! b:kite_stack.push(item)
call add(self.stack, a:item)
endfunction
function! b:kite_stack.is_empty()
return empty(self.stack)
endfunction
function! b:kite_stack.empty()
let self.stack = []
endfunction
endfunction
function! kite#snippet#complete_done()
if empty(v:completed_item) | return | endif
call s:setup_stack()
if has_key(v:completed_item, 'user_data') && !empty(v:completed_item.user_data)
let placeholders = json_decode(v:completed_item.user_data).placeholders
elseif exists('b:kite_completions') && has_key(b:kite_completions, v:completed_item.word)
let placeholders = json_decode(b:kite_completions[v:completed_item.word]).placeholders
let b:kite_completions = {}
else
return
endif
" Send the edit event. Normally this is sent automatically on TextChanged(I).
" But for some reason this doesn't fire when a completion has a snippet placeholder.
call kite#events#event('edit')
if empty(placeholders)
if b:kite_stack.is_empty()
return
else
call kite#snippet#next_placeholder()
return
endif
endif
let b:kite_linenr = line('.')
let b:kite_line_length = col('$')
call s:setup_maps()
call s:setup_autocmds()
" Calculate column number (col_begin) of start of each placeholder, and placeholder length.
let inserted_text = v:completed_item.word
let insertion_start = col('.') - strdisplaywidth(inserted_text)
let b:kite_insertion_end = col('.')
for ph in placeholders
let ph.col_begin = insertion_start + ph.begin
let ph.length = ph.end - ph.begin
unlet ph.begin ph.end
endfor
" Update placeholder locations.
"
" todo move this into the push() function?
" note this is very similar to s:update_placeholder_locations()
if !b:kite_stack.is_empty()
" current placeholder which has just been completed
let level = b:kite_stack.peek()
let ph = level.placeholders[level.index]
let ph_new_length = col('.') - ph.col_begin
let ph_length_delta = ph_new_length - ph.length
let ph.length = ph_new_length
let marker = ph.col_begin
" following placeholders at same level
for ph in level.placeholders[level.index+1:]
let ph.col_begin += ph_length_delta
endfor
" placeholders at outer levels
for level in b:kite_stack.stack[:-2]
for ph in level.placeholders
if ph.col_begin > marker
let ph.col_begin += ph_length_delta
endif
endfor
endfor
endif
call b:kite_stack.push({'placeholders': placeholders, 'index': 0})
" Move to first placeholder.
call s:placeholder(0)
endfunction
" Go to next placeholder at current level, if there is one, or first placeholder at next level otherwise.
function! kite#snippet#next_placeholder()
call s:update_placeholder_locations()
call s:placeholder(b:kite_stack.peek().index + 1)
endfunction
function! kite#snippet#previous_placeholder(...)
call s:placeholder(b:kite_stack.peek().index - 1 - (a:0 ? a:1 : 0))
endfunction
" Move to the placeholder at index and select its text.
function! s:placeholder(index)
let index = a:index
let level = b:kite_stack.peek()
let placeholders = level.placeholders
" Clear highlights before we pop the stack.
call s:clear_all_placeholder_highlights()
if index < 0
" If no other levels in stack
if len(b:kite_stack.stack) == 1
" Stay with first placeholder and proceed
let index = 0
else
call b:kite_stack.pop()
call s:placeholder(b:kite_stack.peek().index - 1)
return
endif
endif
" if navigating forward from last placeholder of current level
if index == len(placeholders)
" If no other levels in stack
if len(b:kite_stack.stack) == 1
call s:goto_initial_completion_end()
else
call b:kite_stack.pop()
call s:placeholder(b:kite_stack.peek().index + 1)
endif
return
endif
call s:highlight_current_level_placeholders()
let level.index = index
let ph = placeholders[index]
" store line length before placeholder gets changed by user
" let b:kite_line_length = col('$')
if ph.length == 0
normal! h
return
endif
" insert mode -> normal mode
stopinsert
let linenr = line('.')
call setpos("'<", [0, linenr, ph.col_begin])
call setpos("'>", [0, linenr, ph.col_begin + ph.length - (mode() == 'n' ? 1 : 0)])
" normal mode -> visual mode -> select mode
execute "normal! gv\<C-G>"
if mode() ==# 'S'
execute "normal! \<C-O>gh"
endif
endfunction
function! s:goto_initial_completion_end()
" call setpos('.', [0, b:kite_linenr, b:kite_insertion_end + col('$') - b:kite_line_length - 1])
call setpos('.', [0, b:kite_linenr, col('$')])
startinsert!
call s:teardown()
endfunction
" Adjust current and subsequent placeholders for the amount of text entered
" at the placeholder we are leaving.
function! s:update_placeholder_locations()
if !exists('b:kite_line_length') | return | endif
let line_length_delta = col('$') - b:kite_line_length
" current placeholder
let level = b:kite_stack.peek()
let ph = level.placeholders[level.index]
let marker = ph.col_begin
let ph.length += line_length_delta
" subsequent placeholders at current level
for ph in level.placeholders[level.index+1:]
let ph.col_begin += line_length_delta
endfor
" placeholders at outer levels
for level in b:kite_stack.stack[:-2]
for ph in level.placeholders
if ph.col_begin > marker
let ph.col_begin += line_length_delta
endif
endfor
endfor
let b:kite_line_length = col('$')
endfunction
function! s:highlight_current_level_placeholders()
let group = s:highlight_group_for_placeholders()
if empty(group) | return | endif
let linenr = line('.')
for ph in b:kite_stack.peek().placeholders
let ph.matchid = matchaddpos(group, [[linenr, ph.col_begin, ph.length]])
endfor
endfunction
" Clears highlights of placeholders in the stack.
"
" Note: if we need a way to clear highlights of placeholders which are no
" longer in the stack (because they have been popped) we could use a custom
" highlight group (e.g. KiteUnderline linked to Underline), call getmatches(),
" and remove all matches using the custom highlight group.
function! s:clear_all_placeholder_highlights()
for level in b:kite_stack.stack
for ph in level.placeholders
if has_key(ph, 'matchid')
call matchdelete(ph.matchid)
unlet ph.matchid
endif
endfor
endfor
endfunction
" Many plugins use vmap for visual-mode mappings but vmap maps both
" visual-mode and select-mode (they should use xmap instead). Assume any
" visual-mode mappings for printable characters are not wanted and remove them
" (but remember them so we can restore them afterwards). Similarly for map.
" Assume any select-only-mode maps are deliberate.
"
" :help mapmode-s
" :help Select-mode-mapping
function! s:remove_smaps_for_printable_characters()
let b:kite_maps = []
let printable_keycodes = [
\ '<Space>',
\ '<Bslash>',
\ '<Tab>',
\ '<C-Tab>',
\ '<NL>',
\ '<CR>',
\ '<BS>',
\ '<Leader>',
\ '<LocalLeader>'
\ ]
" Get a list of maps active in select mode.
for scope in ['<buffer>', '']
redir => maps | silent execute 'smap' scope | redir END
let mappings = split(maps, "\n")
" 'No mapping found' or localised equivalent (starts with capital letter).
if len(mappings) == 1 && mappings[0][0] =~ '\u' | continue | endif
" Assume select-mode maps are deliberate and ignore them.
call filter(mappings, 'v:val[0:2] !~# "s"')
for mapping in mappings
let lhs = matchlist(mapping, '\v^...(\S+)\s.*')[1]
" ^^^ ^^^
" mode lhs
" Ignore keycodes for non-printable characters, e.g. <Left>
if lhs[0] == '<' && index(printable_keycodes, lhs) == -1 | continue | endif
" Remember the mapping so we can restore it later.
call add(b:kite_maps, maparg(lhs, 's', 0, 1))
" Remove the mapping.
silent! execute 'sunmap' scope lhs
endfor
endfor
endfunction
function! s:restore_smaps()
for mapping in b:kite_maps
silent! execute mapping.mode . (mapping.noremap ? 'nore' : '') . 'map '
\ . (mapping.buffer ? '<buffer> ' : '')
\ . (mapping.expr ? '<expr> ' : '')
\ . (mapping.nowait ? '<nowait> ' : '')
\ . (mapping.silent ? '<silent> ' : '')
\ . mapping.lhs . ' '
\ . substitute(mapping.rhs, '<SID>', '<SNR>'.mapping.sid.'_', 'g')
endfor
unlet! b:kite_maps
endfunction
function! s:setup_maps()
execute 'inoremap <buffer> <silent> <expr>' g:kite_next_placeholder 'pumvisible() ? "<C-Y>" : "<C-\><C-O>:call kite#snippet#next_placeholder()<CR>"'
execute 'inoremap <buffer> <silent> <expr>' g:kite_previous_placeholder 'pumvisible() ? "<C-Y><C-G>:<C-U>call kite#snippet#previous_placeholder(2)<CR>" : "<C-\><C-O>:call kite#snippet#previous_placeholder()<CR>"'
execute 'snoremap <buffer> <silent>' g:kite_next_placeholder '<Esc>:call kite#snippet#next_placeholder()<CR>'
execute 'snoremap <buffer> <silent>' g:kite_previous_placeholder '<Esc>:call kite#snippet#previous_placeholder()<CR>'
call s:remove_smaps_for_printable_characters()
endfunction
function! kite#snippet#teardown_maps()
execute 'silent! iunmap <buffer>' g:kite_next_placeholder
execute 'silent! iunmap <buffer>' g:kite_previous_placeholder
execute 'silent! sunmap <buffer>' g:kite_next_placeholder
execute 'silent! sunmap <buffer>' g:kite_previous_placeholder
endfunction
function! s:setup_autocmds()
augroup KiteSnippets
autocmd! * <buffer>
autocmd CursorMovedI <buffer>
\ call s:update_placeholder_locations() |
\ call s:clear_all_placeholder_highlights() |
\ call s:highlight_current_level_placeholders()
autocmd CursorMoved,CursorMovedI <buffer> call s:cursormoved()
autocmd InsertLeave <buffer> call s:insertleave()
augroup END
endfunction
function! s:teardown_autocmds()
autocmd! KiteSnippets * <buffer>
endfunction
" Called to deactivate all placeholders.
function! s:teardown()
call s:clear_all_placeholder_highlights()
call kite#snippet#teardown_maps()
call s:teardown_autocmds()
call s:restore_smaps()
call b:kite_stack.empty()
unlet! b:kite_linenr b:kite_line_length b:kite_insertion_end
endfunction
function! s:highlight_group_for_placeholders()
for group in ['Special', 'SpecialKey', 'Underline', 'DiffChange']
if hlexists(group)
return group
endif
endfor
return ''
endfunction
function! s:cursormoved()
if !exists('b:kite_linenr') | return | endif
if b:kite_linenr == line('.') | return | endif
" TODO check whether the cursor is outside the bounds of the completion?
call s:teardown()
endfunction
function! s:insertleave()
" Modes established by experimentation.
if mode(1) !=# 's' && mode(1) !=# ((has('patch-8.1.0225') || has('nvim-0.4.0')) ? 'niI' : 'n')
call s:teardown()
endif
endfunction
function! s:debug_stack()
if b:kite_stack.is_empty()
echom 'stack empty'
return
endif
let i = 0
for level in b:kite_stack.stack
echom 'level' i
echom ' index' level.index
for pholder in level.placeholders
echom ' '.string(pholder)
endfor
let i += 1
endfor
endfunction

View file

@ -0,0 +1,62 @@
" Updates the status of the current buffer.
"
" Optional argument is a timer id (when called by a timer).
function! kite#status#status(...)
if !s:status_in_status_line() | return | endif
let buf = bufnr('')
let msg = 'NOT SET'
" Check kited status (installed / running) every 10 file status checks.
let counter = getbufvar(buf, 'kite_status_counter', 0)
if counter == 0
if !kite#utils#kite_running()
let msg = 'Kite: not running'
if !kite#utils#kite_installed()
let msg = 'Kite: not installed'
endif
endif
endif
call setbufvar(buf, 'kite_status_counter', (counter + 1) % 10)
if wordcount().bytes > kite#max_file_size()
let msg = 'Kite: file too large'
endif
if msg !=# 'NOT SET'
if msg !=# getbufvar(buf, 'kite_status')
call setbufvar(buf, 'kite_status', msg)
redrawstatus
endif
return
endif
let filename = kite#utils#filepath(0)
call kite#client#status(filename, function('kite#status#handler', [buf]))
endfunction
function! kite#status#handler(buffer, response)
call kite#utils#log('kite status status: '.a:response.status.', body: '.a:response.body)
if a:response.status != 200 | return | endif
let json = json_decode(a:response.body)
let msg = ''
let suffix = get(json, 'short', 'FIELD MISSING')
if suffix !=# 'FIELD MISSING'
let msg = join(['Kite: ', suffix], '')
endif
if msg !=# getbufvar(a:buffer, 'kite_status')
call setbufvar(a:buffer, 'kite_status', msg)
redrawstatus
endif
endfunction
function! s:status_in_status_line()
return stridx(&statusline, 'kite#statusline()') != -1
endfunction

View file

@ -0,0 +1,611 @@
" Values for s:os are used in plugin directory structure
" and also metric values.
if has('win64') || has('win32') || has('win32unix')
let s:os = 'windows'
else
let s:os = empty(findfile('/sbin/launchd')) ? 'linux' : 'macos'
endif
function! kite#utils#windows()
return s:os ==# 'windows'
endfunction
function! kite#utils#macos()
return s:os ==# 'macos'
endfunction
let s:separator = !exists('+shellslash') || &shellslash ? '/' : '\'
let s:plugin_dir = expand('<sfile>:p:h:h:h')
let s:doc_dir = s:plugin_dir.s:separator.'doc'
let s:lib_dir = s:plugin_dir.s:separator.'lib'
let s:lib_subdir = s:lib_dir.s:separator.(s:os)
let s:vim_version = ''
let s:plugin_version = ''
function! kite#utils#vim_version()
if !empty(s:vim_version)
return s:vim_version
endif
let s:vim_version = kite#utils#normalise_version(execute('version'))
return s:vim_version
endfunction
function! kite#utils#normalise_version(version)
let lines = split(a:version, '\n')
if lines[0] =~ 'NVIM'
" Or use api_info().version.
return lines[0] " e.g. NVIM v0.2.2
else
let [major, minor] = matchlist(lines[0], '\v(\d)\.(\d+)')[1:2]
let patch_line = match(lines, ': \d')
if patch_line == -1
let patches = '0'
else
let patches = substitute(split(lines[patch_line], ': ')[1], ' ', '', 'g')
endif
return join([major, minor, patches], '.') " e.g. 8.1.1-582
endif
endfunction
function! kite#utils#plugin_version()
if !empty(s:plugin_version)
return s:plugin_version
endif
let s:plugin_version = readfile(s:plugin_dir.s:separator.'VERSION')[0]
return s:plugin_version
endfunction
" From tpope/vim-fugitive
function! s:winshell()
return kite#utils#windows() && &shellcmdflag !~# '^-'
endfunction
function! kite#utils#browse(url)
if kite#utils#windows()
let cmd = 'cmd /c start "" "'.a:url.'"'
else
let cmd = 'open "'.a:url.'"'
endif
silent call system(cmd)
endfunction
" From tpope/vim-fugitive
function! s:shellescape(arg)
if a:arg =~ '^[A-Za-z0-9_/.-]\+$'
return a:arg
elseif s:winshell()
return '"'.substitute(substitute(a:arg, '"', '""', 'g'), '%', '"%"', 'g').'"'
else
return shellescape(a:arg)
endif
endfunction
if kite#utils#windows()
let s:settings_dir = join([$LOCALAPPDATA, 'Kite'], s:separator)
else
let s:settings_dir = expand('~/.kite')
endif
if !isdirectory(s:settings_dir)
call mkdir(s:settings_dir, 'p')
endif
let s:settings_path = s:settings_dir.s:separator.'vim-plugin.json'
" Get the value for the given key.
" If the key has not been set, returns the default value if given
" (i.e. the optional argument) or -1 otherwise.
function! kite#utils#get_setting(key, ...)
let settings = s:settings()
return get(settings, a:key, (a:0 ? a:1 : -1))
endfunction
" Sets the value for the key.
function! kite#utils#set_setting(key, value)
let settings = s:settings()
let settings[a:key] = a:value
let json_str = json_encode(settings)
call writefile([json_str], s:settings_path)
endfunction
function! s:settings()
if filereadable(s:settings_path)
let json_str = join(readfile(s:settings_path), '')
return json_decode(json_str)
else
return {}
endif
endfunction
function! kite#utils#os()
return s:os
endfunction
function! kite#utils#lib(filename)
return s:lib_subdir.s:separator.a:filename
endfunction
function! kite#utils#kite_installed()
return !empty(s:kite_install_path())
endfunction
" Returns the kite installation path including the filename, or an empty string if not installed.
function! s:kite_install_path()
if kite#utils#windows()
let output = kite#async#sync('reg query HKEY_LOCAL_MACHINE\Software\Kite\AppData /v InstallPath /s /reg:64')
let lines = filter(split(output, '\n'), 'v:val =~ "InstallPath"')
if empty(lines)
return ''
endif
return substitute(lines[0], '\v^\s+InstallPath\s+REG_\w+\s+', '', '').s:separator.'kited.exe'
elseif kite#utils#macos()
return kite#async#sync('mdfind ''kMDItemCFBundleIdentifier = "com.kite.Kite" || kMDItemCFBundleIdentifier = "enterprise.kite.Kite"''')
else
let path = exepath('/opt/kite/kited')
if !empty(path)
return path
endif
let path = exepath(expand('~/.local/share/kite/kited'))
if !empty(path)
return path
endif
return ''
endif
endfunction
function! kite#utils#kite_running()
if kite#utils#windows()
let [cmd, process] = ['tasklist /FI "IMAGENAME eq kited.exe"', '^kited.exe']
elseif kite#utils#macos()
let [cmd, process] = ['ps -axco command', '^Kite$']
else
let process_name = empty($KITED_TEST_PORT) ? 'kited' : 'kited-test'
let [cmd, process] = ['ps -axco command', '^'.process_name.'$']
endif
return match(split(kite#async#sync(cmd), '\n'), process) > -1
endfunction
function! kite#utils#launch_kited()
if kite#utils#kite_running()
return
endif
let path = s:kite_install_path()
if empty(path)
return
endif
if kite#utils#windows()
let $KITE_SKIP_ONBOARDING = 1
silent execute "!start" s:shellescape(path)
elseif kite#utils#macos()
call system('open -a '.path.' --args "--plugin-launch"')
else
silent execute '!'.path.' --plugin-launch >/dev/null 2>&1 &'
endif
endfunction
" msg - a list or a string
function! kite#utils#log(msg)
if g:kite_log
if type(a:msg) == v:t_string
let msg = [a:msg]
else
let msg = a:msg
endif
call writefile(msg, 'kite-vim.log', 'a')
endif
endfunction
function! kite#utils#warn(msg)
echohl WarningMsg
echom 'Kite: '.a:msg
echohl None
let v:warningmsg = a:msg
endfunction
function! kite#utils#info(msg)
echohl Question
echom a:msg
echohl None
endfunction
" Returns the absolute path to the current file after resolving symlinks.
"
" url_format - when truthy, return the path in a URL-compatible format.
function! kite#utils#filepath(url_format)
let path = resolve(expand('%:p'))
if a:url_format
let path = substitute(path, '[\/]', ':', 'g')
if kite#utils#windows()
let path = substitute(path, '^\(\a\)::', '\1:', '')
let path = ':windows:'.path
endif
let path = kite#utils#url_encode(path)
endif
return path
endfunction
" Returns a 2-element list of 0-based character indices into the buffer.
"
" When no text is selected, both elements are the cursor position.
"
" When text is selected, the elements are the start (inclusive) and
" end (exclusive) of the selection.
"
" Returns [-1, -1] when not in normal, insert, or visual mode.
function! kite#utils#selected_region_characters()
return s:selected_region('c')
endfunction
" Returns a 2-element list of 0-based byte indices into the buffer.
"
" When no text is selected, both elements are the cursor position.
"
" When text is selected, the elements are the start (inclusive) and
" end (exclusive) of the selection.
"
" Returns [-1, -1] when not in normal, insert, or visual mode.
function! kite#utils#selected_region_bytes()
return s:selected_region('b')
endfunction
" Returns a 2-element list of 0-based indices into the buffer.
"
" When no text is selected, both elements are the cursor position.
"
" When text is selected, the elements are the start (inclusive) and
" end (exclusive) of the selection.
"
" Returns [-1, -1] when not in normal, insert, or visual mode.
"
" param type (String) - 'c' for character indices, 'b' for byte indices
"
" NOTE: the cursor is moved during the function (but finishes where it started).
function! s:selected_region(type)
if mode() ==# 'n' || mode() ==# 'i'
if a:type == 'c'
let offset = kite#utils#character_offset()
else
let offset = kite#utils#byte_offset_start()
endif
return [offset, offset]
endif
if mode() ==? 'v'
let pos_start = getpos('v')
let pos_end = getpos('.')
if (pos_start[1] > pos_end[1]) || (pos_start[1] == pos_end[1] && pos_start[2] > pos_end[2])
let [pos_start, pos_end] = [pos_end, pos_start]
endif
" switch to normal mode
execute "normal! \<Esc>"
call setpos('.', pos_start)
if a:type == 'c'
let offset1 = kite#utils#character_offset()
else
let offset1 = kite#utils#byte_offset_start()
endif
call setpos('.', pos_end)
" end position is exclusive
if a:type == 'c'
let offset2 = kite#utils#character_offset() + 1
else
let offset2 = kite#utils#byte_offset_end() + 1
endif
" restore visual selection
normal! gv
return [offset1, offset2]
endif
return [-1, -1]
endfunction
" Returns the 0-based index of the cursor in the buffer.
"
" Returns -1 when the buffer is empty.
function! kite#utils#cursor_characters()
if mode() ==? 'v'
" switch to normal mode
execute "normal! \<Esc>"
let cursor = kite#utils#character_offset()
" restore visual selection
normal! gv
return cursor
endif
return kite#utils#character_offset()
endfunction
" Returns the 0-based index into the buffer of the cursor position.
" Returns -1 when the buffer is empty.
"
" Does not work in visual mode.
function! kite#utils#character_offset()
" wordcount().cursor_chars is 1-based so we need to subtract 1.
let offset = wordcount().cursor_chars - 1
" In insert mode the cursor isn't really between two characters;
" it is actually on the second character, but that's what we want
" anyway.
" If the cursor is just before (i.e. on) the end of the line, and
" the file has dos line endings, wordcount().cursor_chars will
" regard the cursor as on the second character of the \r\n. In this
" case we want the offset of the first, i.e. the \r.
if col('.') == col('$') && &ff ==# 'dos'
let offset -= 1
endif
return offset
endfunction
" Returns the 0-based index into the buffer of the cursor position.
" If the cursor is on a multibyte character, it reports the character's
" first byte.
function! kite#utils#byte_offset_start()
let offset = line2byte(line('.')) - 1 + col('.') - 1
if offset < 0
let offset = 0
endif
return offset
endfunction
" Returns the 0-based index into the buffer of the cursor position.
" If the cursor is on a multibyte character, it reports the character's
" last byte.
function! kite#utils#byte_offset_end()
let offset = wordcount().cursor_bytes - 1
if offset < 0
let offset = 0
endif
return offset
endfunction
function! kite#utils#buffer_contents()
let line_ending = {"unix": "\n", "dos": "\r\n", "mac": "\r"}[&fileformat]
return join(getline(1, '$'), line_ending).(&eol ? line_ending : '')
endfunction
" Similar to the goto command, but for characters.
" index is 1-based, the start of the file.
function! kite#utils#goto_character(index)
call search('\m\%^\_.\{'.a:index.'}', 'es')
" The search() function above counts a newline as 1 character even if it is
" actually 2. Therefore we need to adjust the cursor position when newlines
" are 2 characters.
if &ff ==# 'dos'
let [_whichwrap, &whichwrap] = [&whichwrap, "h,l"]
let delta = wordcount().cursor_chars - a:index
while delta != 0
" Cannot land on a newline character.
if (delta == -1 || delta == -2) && col('.') == col('$') - 1
break
endif
execute "normal! ".delta.(delta > 0 ? 'h' : 'l')
let delta = wordcount().cursor_chars - a:index
endwhile
let &whichwrap = _whichwrap
endif
endfunction
" Returns the MD5 hash of the buffer contents.
function! kite#utils#buffer_md5()
return s:MD5(kite#utils#buffer_contents())
endfunction
" https://github.com/tpope/vim-unimpaired/blob/3a7759075cca5b0dc29ce81f2747489b6c8e36a7/plugin/unimpaired.vim#L327-L329
function! kite#utils#url_encode(str)
return substitute(a:str,'[^A-Za-z0-9_.~-]','\="%".printf("%02X",char2nr(submatch(0)))','g')
endfunction
" Capitalises the first letter of str.
function! kite#utils#capitalize(str)
return substitute(a:str, '^.', '\u\0', '')
endfunction
function! kite#utils#map_join(list, prop, sep)
return join(map(copy(a:list), {_,v -> v[a:prop]}), a:sep)
endfunction
" Returns a list of lines, each no longer than length.
" The last line may be longer than length if it has no spaces.
" Assumes str is a constructor or function call.
"
" Example: json.dumps
"
" dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, encoding, default, sort_keys, *args, **kwargs)
"
" - becomes when wrapped:
"
" dumps(obj, skipkeys, ensure_ascii, check_circular,
" allow_nan, cls, indent, separators, encoding,
" default, sort_keys, *args, **kwargs)
"
function! kite#utils#wrap(str, length, indent)
let lines = []
let str = a:str
let [prefix; str] = split(a:str, '(\zs')
let str = join(str)
while v:true
let line = prefix.str
if len(line) <= a:length
call add(lines, line)
break
endif
let i = strridx(str[0:a:length-len(prefix)], ' ')
if i == -1
call add(lines, line)
break
endif
let line = prefix . str[0:i-1]
call add(lines, line)
let str = str[i+1:]
let prefix = repeat(' ', a:indent)
endwhile
return lines
endfunction
function! kite#utils#coerce(dict, key, default)
if has_key(a:dict, a:key)
let v = a:dict[a:key]
if type(v) == type(a:default) " check type in case of null
return v
endif
endif
return a:default
endfunction
function! kite#utils#present(dict, key)
return has_key(a:dict, a:key) && !empty(a:dict[a:key])
endfunction
" Returns a string of the given length.
"
" If length is 0 or negative, returns an empty string.
"
" If text is less than length, it is padded with leading spaces so that it is
" right-aligned.
"
" If text is greater than length, it is truncated with an ellipsis.
" If there isn't room for an ellipsis, or room for only an ellipsis, empty spaces are used.
function! kite#utils#ralign(text, length)
if a:length <= 0
return ''
endif
let text_width = strdisplaywidth(a:text)
" The required length
if text_width == a:length
return a:text
endif
" Less than the required length: left-pad
if text_width < a:length
return repeat(' ', a:length-text_width) . a:text
endif
" Greater than the required length: truncate
if kite#utils#windows()
let ellipsis = '...'
else
let ellipsis = '…'
endif
let ellipsis_width = strdisplaywidth(ellipsis)
if ellipsis_width >= a:length
return repeat(' ', a:length)
endif
return a:text[: a:length-ellipsis_width-1] . ellipsis
endfunction
function! kite#utils#truncate(text, length)
let text_width = strdisplaywidth(a:text)
if text_width <= a:length
return a:text
endif
if kite#utils#windows()
let ellipsis = '...'
else
let ellipsis = '…'
endif
let ellipsis_width = strdisplaywidth(ellipsis)
if ellipsis_width >= a:length
return a:text[0] . ellipsis[0: a:length-2]
endif
return a:text[: a:length-ellipsis_width-1] . ellipsis
endfunction
function! s:chomp(str)
return substitute(a:str, '\n$', '', '')
endfunction
function! s:md5(text)
return s:chomp(system('md5', a:text))
endfunction
function! s:md5sum(text)
return split(system('md5sum', a:text), ' ')[0]
endfunction
function! s:md5bin(text)
return s:chomp(system(s:md5_binary, a:text))
endfunction
if executable('md5')
let s:MD5 = function('s:md5')
elseif executable('md5sum')
let s:MD5 = function('s:md5sum')
else
let s:md5_binary = kite#utils#lib('md5Sum.exe')
let s:MD5 = function('s:md5bin')
endif