"
" requires:
"
-" - neovim
+" - neovim or vim
" - curl
" - llama.cpp server instance
" - FIM-compatible model
" sample config:
"
" - Tab - accept the current suggestion
-" - Shift+Tab - accept just the first line of the segguestion
+" - Shift+Tab - accept just the first line of the suggestion
" - Ctrl+F - toggle FIM completion manually
"
" make symlink or copy this file to ~/.config/nvim/autoload/llama.vim
"
" colors (adjust to your liking)
-highlight llama_hl_hint guifg=#ff772f
-highlight llama_hl_info guifg=#77ff2f
+highlight llama_hl_hint guifg=#ff772f ctermfg=202
+highlight llama_hl_info guifg=#77ff2f ctermfg=119
" general parameters:
"
let g:llama_config = get(g:, 'llama_config', s:default_config)
+function! s:get_indent(str)
+ let l:count = 0
+ for i in range(len(a:str))
+ if a:str[i] == "\t"
+ let l:count += &tabstop - 1
+ else
+ break
+ endif
+ endfor
+ return l:count
+endfunction
+
function! s:rand(i0, i1) abort
return a:i0 + rand() % (a:i1 - a:i0 + 1)
endfunction
let s:current_job = v:null
+ let s:ghost_text_nvim = exists('*nvim_buf_get_mark')
+ let s:ghost_text_vim = has('textprop')
+
+ if s:ghost_text_vim
+ let s:hlgroup_hint = 'llama_hl_hint'
+ let s:hlgroup_info = 'llama_hl_info'
+
+ if empty(prop_type_get(s:hlgroup_hint))
+ call prop_type_add(s:hlgroup_hint, {'highlight': s:hlgroup_hint})
+ endif
+ if empty(prop_type_get(s:hlgroup_info))
+ call prop_type_add(s:hlgroup_info, {'highlight': s:hlgroup_info})
+ endif
+ endif
+
augroup llama
autocmd!
autocmd InsertEnter * inoremap <expr> <silent> <C-F> llama#fim_inline(v:false)
\ 't_max_predict_ms': 1
\ })
- let l:curl_command = printf(
- \ "curl --silent --no-buffer --request POST --url %s --header \"Content-Type: application/json\" --data %s",
- \ g:llama_config.endpoint, shellescape(l:request)
- \ )
+ let l:curl_command = [
+ \ "curl",
+ \ "--silent",
+ \ "--no-buffer",
+ \ "--request", "POST",
+ \ "--url", g:llama_config.endpoint,
+ \ "--header", "Content-Type: application/json",
+ \ "--data", l:request
+ \ ]
" no callbacks because we don't need to process the response
- call jobstart(l:curl_command, {})
+ if s:ghost_text_nvim
+ call jobstart(l:curl_command, {})
+ elseif s:ghost_text_vim
+ call job_start(l:curl_command, {})
+ endif
endfunction
" necessary for 'inoremap <expr>'
\ 't_max_predict_ms': g:llama_config.t_max_predict_ms
\ })
- let l:curl_command = printf(
- \ "curl --silent --no-buffer --request POST --url %s --header \"Content-Type: application/json\" --data %s",
- \ g:llama_config.endpoint, shellescape(l:request)
- \ )
+ let l:curl_command = [
+ \ "curl",
+ \ "--silent",
+ \ "--no-buffer",
+ \ "--request", "POST",
+ \ "--url", g:llama_config.endpoint,
+ \ "--header", "Content-Type: application/json",
+ \ "--data", l:request
+ \ ]
if s:current_job != v:null
- call jobstop(s:current_job)
+ if s:ghost_text_nvim
+ call jobstop(s:current_job)
+ elseif s:ghost_text_vim
+ call job_stop(s:current_job)
+ endif
endif
" send the request asynchronously
- let s:current_job = jobstart(l:curl_command, {
- \ 'on_stdout': function('s:fim_on_stdout'),
- \ 'on_exit': function('s:fim_on_exit'),
- \ 'stdout_buffered': v:true,
- \ 'pos_x': s:pos_x,
- \ 'pos_y': s:pos_y,
- \ 'is_auto': a:is_auto
- \ })
+ if s:ghost_text_nvim
+ let s:current_job = jobstart(l:curl_command, {
+ \ 'on_stdout': function('s:fim_on_stdout', [s:pos_x, s:pos_y, a:is_auto]),
+ \ 'on_exit': function('s:fim_on_exit'),
+ \ 'stdout_buffered': v:true
+ \ })
+ elseif s:ghost_text_vim
+ let s:current_job = job_start(l:curl_command, {
+ \ 'out_cb': function('s:fim_on_stdout', [s:pos_x, s:pos_y, a:is_auto]),
+ \ 'exit_cb': function('s:fim_on_exit')
+ \ })
+ endif
" TODO: per-file location
let l:delta_y = abs(s:pos_y - s:pos_y_pick)
" clear the virtual text
let l:bufnr = bufnr('%')
- let l:id_vt_fim = nvim_create_namespace('vt_fim')
-
- call nvim_buf_clear_namespace(l:bufnr, l:id_vt_fim, 0, -1)
+ if s:ghost_text_nvim
+ let l:id_vt_fim = nvim_create_namespace('vt_fim')
+ call nvim_buf_clear_namespace(l:bufnr, l:id_vt_fim, 0, -1)
+ elseif s:ghost_text_vim
+ call prop_remove({'type': s:hlgroup_hint, 'all': v:true})
+ call prop_remove({'type': s:hlgroup_info, 'all': v:true})
+ endif
" remove the mappings
silent! iunmap <buffer> <Tab>
endfunction
" callback that processes the FIM result from the server and displays the suggestion
-function! s:fim_on_stdout(job_id, data, event) dict
- let l:raw = join(a:data, "\n")
+function! s:fim_on_stdout(pos_x, pos_y, is_auto, job_id, data, event = v:null)
+ if s:ghost_text_nvim
+ let l:raw = join(a:data, "\n")
+ elseif s:ghost_text_vim
+ let l:raw = a:data
+ endif
+
if len(l:raw) == 0
return
endif
- if self.pos_x != col('.') - 1 || self.pos_y != line('.')
+ if a:pos_x != col('.') - 1 || a:pos_y != line('.')
return
endif
return
endif
- let s:pos_x = self.pos_x
- let s:pos_y = self.pos_y
+ let s:pos_x = a:pos_x
+ let s:pos_y = a:pos_y
let s:can_accept = v:true
let l:has_info = v:false
if s:can_accept && v:shell_error
- if !self.is_auto
+ if !a:is_auto
call add(s:content, "<| curl error: is the server on? |>")
endif
let s:can_accept = v:false
" display virtual text with the suggestion
let l:bufnr = bufnr('%')
- let l:id_vt_fim = nvim_create_namespace('vt_fim')
+ if s:ghost_text_nvim
+ let l:id_vt_fim = nvim_create_namespace('vt_fim')
+ endif
" construct the info message
if g:llama_config.show_info > 0 && l:has_info
endif
" display the suggestion and append the info to the end of the first line
- call nvim_buf_set_extmark(l:bufnr, l:id_vt_fim, s:pos_y - 1, s:pos_x - 1, {
- \ 'virt_text': [[s:content[0], 'llama_hl_hint'], [l:info, 'llama_hl_info']],
- \ 'virt_text_win_col': virtcol('.') - 1
- \ })
+ if s:ghost_text_nvim
+ call nvim_buf_set_extmark(l:bufnr, l:id_vt_fim, s:pos_y - 1, s:pos_x - 1, {
+ \ 'virt_text': [[s:content[0], 'llama_hl_hint'], [l:info, 'llama_hl_info']],
+ \ 'virt_text_win_col': virtcol('.') - 1
+ \ })
- call nvim_buf_set_extmark(l:bufnr, l:id_vt_fim, s:pos_y - 1, 0, {
- \ 'virt_lines': map(s:content[1:], {idx, val -> [[val, 'llama_hl_hint']]}),
- \ 'virt_text_win_col': virtcol('.')
- \ })
+ call nvim_buf_set_extmark(l:bufnr, l:id_vt_fim, s:pos_y - 1, 0, {
+ \ 'virt_lines': map(s:content[1:], {idx, val -> [[val, 'llama_hl_hint']]}),
+ \ 'virt_text_win_col': virtcol('.')
+ \ })
+ elseif s:ghost_text_vim
+ let l:new_suffix = s:content[0]
+ if !empty(l:new_suffix)
+ call prop_add(s:pos_y, s:pos_x + 1, {
+ \ 'type': s:hlgroup_hint,
+ \ 'text': l:new_suffix
+ \ })
+ endif
+ for line in s:content[1:]
+ call prop_add(s:pos_y, 0, {
+ \ 'type': s:hlgroup_hint,
+ \ 'text': line,
+ \ 'text_padding_left': s:get_indent(line),
+ \ 'text_align': 'below'
+ \ })
+ endfor
+ if !empty(l:info)
+ call prop_add(s:pos_y, 0, {
+ \ 'type': s:hlgroup_info,
+ \ 'text': l:info,
+ \ 'text_padding_left': col('$'),
+ \ 'text_wrap': 'truncate'
+ \ })
+ endif
+ endif
" setup accept shortcuts
inoremap <buffer> <Tab> <C-O>:call llama#fim_accept(v:false)<CR>
let s:hint_shown = v:true
endfunction
-function! s:fim_on_exit(job_id, exit_code, event) dict
+function! s:fim_on_exit(job_id, exit_code, event = v:null)
if a:exit_code != 0
echom "Job failed with exit code: " . a:exit_code
endif