DonnieWest / asyncomplete_neovim_lsp

1 stars 0 forks source link

Snippet support #2

Open entropitor opened 4 years ago

entropitor commented 4 years ago

It would be nice if we have snippet support. i.e. that LSP snippets would expand using UltiSnips

I found some code already but I didn't nail every part down yet. Do you think it makes sense to implement that in this plugin?

https://github.com/thomasfaingnaert/vim-lsp-ultisnips/blob/master/autoload/lsp_ultisnips.vim#L7-L9 Applying the snippet: https://github.com/prabirshrestha/vim-lsp/blob/2de8f4d479229eedcb9bf41a878b94fb7e2662ed/autoload/lsp/ui/vim/completion.vim#L57-L117 Clearing the text: https://github.com/prabirshrestha/vim-lsp/blob/2de8f4d479229eedcb9bf41a878b94fb7e2662ed/autoload/lsp/ui/vim/completion.vim#L173-L224 Applying the text edits: https://github.com/prabirshrestha/vim-lsp/blob/2de8f4d479229eedcb9bf41a878b94fb7e2662ed/autoload/lsp/utils/text_edit.vim#L1-L21

This leads to this code already but the s:clear_inserted_text still needs to be fixed + have a way to actually clear the line as it's using vim-lsp code under the hood.

function! s:escape_string(str) abort
    let l:ret = substitute(a:str, '\%x00', '\\n', 'g')
    let l:ret = substitute(l:ret, '"', '\\"', 'g')
    return l:ret
endfunction

function! s:clear_inserted_text(line, done_position, complete_position, completed_item, completion_item) abort
  " Remove commit characters.
  call setline('.', a:line)

  " Create range to remove v:completed_item.
  let l:range = {
        \   'start': {
        \     'line': a:done_position[1] - 1,
        \     'character': lsp#utils#to_char('%', a:done_position[1], a:done_position[2] + a:done_position[3]) - strchars(a:completed_item['word'])
        \   },
        \   'end': {
        \     'line': a:done_position[1] - 1,
        \     'character': lsp#utils#to_char('%', a:done_position[1], a:done_position[2] + a:done_position[3])
        \   }
        \ }

  " Expand remove range to textEdit.
  if has_key(a:completion_item, 'textEdit')
    let l:range = {
    \   'start': {
    \     'line': a:completion_item['textEdit']['range']['start']['line'],
    \     'character': a:completion_item['textEdit']['range']['start']['character'],
    \   },
    \   'end': {
    \     'line': a:completion_item['textEdit']['range']['end']['line'],
    \     'character': a:completion_item['textEdit']['range']['end']['character'] + strchars(a:completed_item['word']) - (a:complete_position['character'] - l:range['start']['character'])
    \   }
    \ }
  endif

  " Remove v:completed_item.word (and textEdit range if need).
  call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{
        \   'range': l:range,
        \   'newText': ''
        \ }])

  " Move to complete start position.
  call cursor(lsp#utils#position#lsp_to_vim('%', l:range['start']))
endfunction
function! s:get_expand_text(completed_item, completion_item) abort
  let l:text = a:completed_item['word']
  if has_key(a:completion_item, 'textEdit') && type(a:completion_item['textEdit']) == v:t_dict
    let l:text = a:completion_item['textEdit']['newText']
  elseif has_key(a:completion_item, 'insertText')
    let l:text = a:completion_item['insertText']
  endif
  return l:text
endfunction

function! s:handle_completion() abort
  if empty(v:completed_item)
    return
  endif

  let l:completion_item = v:completed_item.user_data.nvim.lsp.completion_item
  let l:new_text = s:get_expand_text(v:completed_item, l:completion_item)
  if strlen(l:new_text) > 0
  " call s:clear_inserted_text(
  "       \   l:line,
  "       \   l:done_position,
  "       \   l:complete_position,
  "       \   l:completed_item,
  "       \   l:completion_item,
  "       \ )
    call feedkeys("i\<C-r>=UltiSnips#Anon(\"" . s:escape_string(l:new_text) . "\", \"\")\<CR>", 'i')
  endif

endfunction
autocmd CompleteDonePre * call s:handle_completion()
DonnieWest commented 4 years ago

Yep! It totally makes sense to do that here. Want to submit a PR?

entropitor commented 4 years ago

Yeah, potentially, but I might need some help with the clearing of the text. Do you think it's best to just copy all the code that vim-lsp uses for this?

DonnieWest commented 4 years ago

That works. Be sure to give them credit in the PR and I'll make sure to add credit to the README :)

entropitor commented 4 years ago
function! s:escape_string(str) abort
    let l:ret = substitute(a:str, '\%x00', '\\n', 'g')
    let l:ret = substitute(l:ret, '"', '\\"', 'g')
    return l:ret
endfunction

" This function can be error prone if the caller forgets to use +1 to vim line
" so use lsp#utils#position#lsp_to_vim instead
" Convert a character-index (0-based) to byte-index (1-based)
" This function requires a buffer specifier (expr, see :help bufname()),
" a line number (lnum, 1-based), and a character-index (char, 0-based).
function! s:to_col(expr, lnum, char) abort
    let l:lines = getbufline(a:expr, a:lnum)
    if l:lines == []
        if type(a:expr) != v:t_string || !filereadable(a:expr)
            " invalid a:expr
            return a:char + 1
        endif
        " a:expr is a file that is not yet loaded as a buffer
        let l:lines = readfile(a:expr, '', a:lnum)
    endif
    let l:linestr = l:lines[-1]
    return strlen(strcharpart(l:linestr, 0, a:char)) + 1
endfunction

" Convert a byte-index (1-based) to a character-index (0-based)
" This function requires a buffer specifier (expr, see :help bufname()),
" a line number (lnum, 1-based), and a byte-index (char, 1-based).
function! _lsp_utils_to_char(expr, lnum, col) abort
    let l:lines = getbufline(a:expr, a:lnum)
    if l:lines == []
        if type(a:expr) != v:t_string || !filereadable(a:expr)
            " invalid a:expr
            return a:col - 1
        endif
        " a:expr is a file that is not yet loaded as a buffer
        let l:lines = readfile(a:expr, '', a:lnum)
    endif
    let l:linestr = l:lines[-1]
    return strchars(strpart(l:linestr, 0, a:col - 1))
endfunction

function! s:decode_uri(uri) abort
    let l:ret = substitute(a:uri, '[?#].*', '', '')
    return substitute(l:ret, '%\(\x\x\)', '\=printf("%c", str2nr(submatch(1), 16))', 'g')
endfunction

function! s:urlencode_char(c) abort
    return printf('%%%02X', char2nr(a:c))
endfunction

function! s:get_prefix(path) abort
    return matchstr(a:path, '\(^\w\+::\|^\w\+://\)')
endfunction

function! s:encode_uri(path, start_pos_encode, default_prefix) abort
    let l:prefix = s:get_prefix(a:path)
    let l:path = a:path[len(l:prefix):]
    if len(l:prefix) == 0
        let l:prefix = a:default_prefix
    endif

    let l:result = strpart(a:path, 0, a:start_pos_encode)

    for l:i in range(a:start_pos_encode, len(l:path) - 1)
        " Don't encode '/' here, `path` is expected to be a valid path.
        if l:path[l:i] =~# '^[a-zA-Z0-9_.~/-]$'
            let l:result .= l:path[l:i]
        else
            let l:result .= s:urlencode_char(l:path[l:i])
        endif
    endfor

    return l:prefix . l:result
endfunction

if has('win32') || has('win64')
    function! _lsp_utils_path_to_uri(path) abort
        if empty(a:path)
            return a:path
        else
            " You must not encode the volume information on the path if
            " present
            let l:end_pos_volume = matchstrpos(a:path, '\c[A-Z]:')[2]

            if l:end_pos_volume == -1
                let l:end_pos_volume = 0
            endif

            return s:encode_uri(substitute(a:path, '\', '/', 'g'), l:end_pos_volume, 'file:///')
        endif
    endfunction
else
    function! _lsp_utils_path_to_uri(path) abort
        if empty(a:path)
            return a:path
        else
            return s:encode_uri(a:path, 0, 'file://')
        endif
    endfunction
endif

if has('win32') || has('win64')
    function! _lsp_utils_uri_to_path(uri) abort
        return substitute(s:decode_uri(a:uri[len('file:///'):]), '/', '\\', 'g')
    endfunction
else
    function! _lsp_utils_uri_to_path(uri) abort
        return s:decode_uri(a:uri[len('file://'):])
    endfunction
endif

function! _lsp_utils_get_buffer_uri(...) abort
    return _lsp_utils_path_to_uri(expand((a:0 > 0 ? '#' . a:1 : '%') . ':p'))
endfunction

"
" _check
"
" LSP Spec says `multiple text edits can not overlap those ranges`.
" This function check it. But does not throw error.
"
function! s:_check(text_edits) abort
  if len(a:text_edits) > 1
    let l:range = a:text_edits[0].range
    for l:text_edit in a:text_edits[1 : -1]
      if l:range.end.line > l:text_edit.range.start.line || (
      \   l:range.end.line == l:text_edit.range.start.line &&
      \   l:range.end.character > l:text_edit.range.start.character
      \ )
        call lsp#log('text_edit: range overlapped.')
      endif
      let l:range = l:text_edit.range
    endfor
  endif
  return a:text_edits
endfunction

"
" _normalize
"
function! s:_normalize(text_edits) abort
  let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits]
  let l:text_edits = filter(copy(l:text_edits), { _, text_edit -> type(text_edit) == type({}) })
  let l:text_edits = s:_range(l:text_edits)
  let l:text_edits = sort(copy(l:text_edits), function('s:_compare', [], {}))
  let l:text_edits = s:_check(l:text_edits)
  return reverse(l:text_edits)
endfunction

"
" _compare
"
function! s:_compare(text_edit1, text_edit2) abort
  let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line
  if l:diff == 0
    return a:text_edit1.range.start.character - a:text_edit2.range.start.character
  endif
  return l:diff
endfunction

"
" _range
"
function! s:_range(text_edits) abort
  for l:text_edit in a:text_edits
    if l:text_edit.range.start.line > l:text_edit.range.end.line || (
          \   l:text_edit.range.start.line == l:text_edit.range.end.line &&
          \   l:text_edit.range.start.character > l:text_edit.range.end.character
          \ )
      let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start }
    endif
  endfor
  return a:text_edits
endfunction

"
" _switch
"
function! s:_switch(path) abort
  if bufnr(a:path) >= 0
    execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path))
  else
    execute printf('keepalt keepjumps edit! %s', fnameescape(a:path))
  endif
endfunction

function! _lsp_utils_text_edit_apply_text_edits(uri, text_edits) abort
    let l:current_bufname = bufname('%')
    let l:target_bufname = _lsp_utils_uri_to_path(a:uri)
    let l:cursor_pos = getpos('.')[1 : 3]
    let l:cursor_offset = 0
    let l:topline = line('w0')

    call s:_switch(l:target_bufname)
    for l:text_edit in s:_normalize(a:text_edits)
        let l:cursor_offset += s:_apply(bufnr(l:target_bufname), l:text_edit, l:cursor_pos)
    endfor
    call s:_switch(l:current_bufname)

    if bufnr(l:current_bufname) == bufnr(l:target_bufname)
        let l:length = strlen(getline(l:cursor_pos[0])) + 1
        let l:cursor_pos[2] = max([0, l:cursor_pos[1] + l:cursor_pos[2] - l:length])
        let l:cursor_pos[1] = min([l:length, l:cursor_pos[1] + l:cursor_pos[2]])
        call cursor(l:cursor_pos)
        call winrestview({ 'topline': l:topline + l:cursor_offset })
    endif
endfunction

function! _lsp_utils__split_by_eol(text) abort
    return split(a:text, '\r\n\|\r\|\n', v:true)
endfunction

let s:fixendofline_exists = exists('+fixendofline')
function! _lsp_utils_buffer__get_fixendofline(buf) abort
    let l:eol = getbufvar(a:buf, '&endofline')
    let l:binary = getbufvar(a:buf, '&binary')

    if s:fixendofline_exists
        let l:fixeol = getbufvar(a:buf, '&fixendofline')

        if !l:binary
            " When 'binary' is off and 'fixeol' is on, 'endofline' is not used
            "
            " When 'binary' is off and 'fixeol' is off, 'endofline' is used to
            " remember the presence of a <EOL>
            return l:fixeol || l:eol
        else
            " When 'binary' is on, the value of 'fixeol' doesn't matter
            return l:eol
        endif
    else
        " When 'binary' is off the value of 'endofline' is not used
        "
        " When 'binary' is on 'endofline' is used to remember the presence of
        " a <EOL>
        return !l:binary || l:eol
    endif
endfunction

"
" _apply
"
function! s:_apply(bufnr, text_edit, cursor_pos) abort
  " create before/after line.
  let l:start_line = getline(a:text_edit.range.start.line + 1)
  let l:end_line = getline(a:text_edit.range.end.line + 1)
  let l:before_line = strcharpart(l:start_line, 0, a:text_edit.range.start.character)
  let l:after_line = strcharpart(l:end_line, a:text_edit.range.end.character, strchars(l:end_line) - a:text_edit.range.end.character)

  " create new lines.
  let l:new_lines = _lsp_utils__split_by_eol(a:text_edit.newText)
  let l:new_lines[0] = l:before_line . l:new_lines[0]
  let l:new_lines[-1] = l:new_lines[-1] . l:after_line

  " fixendofline
  let l:buffer_length = len(getbufline(a:bufnr, '^', '$'))
  let l:should_fixendofline = _lsp_utils_buffer__get_fixendofline(a:bufnr)
  let l:should_fixendofline = l:should_fixendofline && l:new_lines[-1] ==# ''
  let l:should_fixendofline = l:should_fixendofline && l:buffer_length <= a:text_edit['range']['end']['line']
  let l:should_fixendofline = l:should_fixendofline && a:text_edit['range']['end']['character'] == 0
  if l:should_fixendofline
      call remove(l:new_lines, -1)
  endif

  let l:new_lines_len = len(l:new_lines)

  " fix cursor pos
  let l:cursor_offset = 0
  if a:text_edit.range.end.line + 1 < a:cursor_pos[0]
    let l:cursor_offset = l:new_lines_len - (a:text_edit.range.end.line - a:text_edit.range.start.line) - 1
    let a:cursor_pos[0] += l:cursor_offset
  endif

  " append new lines.
  call append(a:text_edit.range.start.line, l:new_lines)

  " remove old lines
  execute printf('%s,%sdelete _',
  \   l:new_lines_len + a:text_edit.range.start.line + 1,
  \   min([l:new_lines_len + a:text_edit.range.end.line + 1, line('$')])
  \ )

  return l:cursor_offset
endfunction

" @param expr = see :help bufname()
" @param position = {
"   'line': 1,
"   'character': 1
" }
" @returns [
"   line,
"   col
" ]
function! _lsp_utils_position_lsp_to_vim(expr, position) abort
    let l:line = a:position['line'] + 1
    let l:char = a:position['character']
    let l:col = s:to_col(a:expr, l:line, l:char)
    return [l:line, l:col]
endfunction

function! s:clear_inserted_text(line, done_position, completed_item, completion_item) abort
  " Remove commit characters.
  call setline('.', a:line)

  " Create range to remove v:completed_item.
  let l:range = {
        \   'start': {
        \     'line': a:done_position[1] - 1,
        \     'character': _lsp_utils_to_char('%', a:done_position[1], a:done_position[2] + a:done_position[3]) - strchars(a:completed_item['word'])
        \   },
        \   'end': {
        \     'line': a:done_position[1] - 1,
        \     'character': _lsp_utils_to_char('%', a:done_position[1], a:done_position[2] + a:done_position[3])
        \   }
        \ }

  " Expand remove range to textEdit.
  if has_key(a:completion_item, 'textEdit')
    let l:range = {
    \   'start': {
    \     'line': a:completion_item['textEdit']['range']['start']['line'],
    \     'character': a:completion_item['textEdit']['range']['start']['character'],
    \   },
    \   'end': {
    \     'line': a:completion_item['textEdit']['range']['end']['line'],
    \     'character': a:completion_item['textEdit']['range']['end']['character'] + strchars(a:completed_item['word'])
    \   }
    \ }
  endif

  " Remove v:completed_item.word (and textEdit range if need).
  call _lsp_utils_text_edit_apply_text_edits(_lsp_utils_get_buffer_uri(bufnr('%')), [{
        \   'range': l:range,
        \   'newText': ''
        \ }])

  " Move to complete start position.
  call cursor(_lsp_utils_position_lsp_to_vim('%', l:range['start']))
endfunction

function! s:get_expand_text(completed_item, completion_item) abort
  let l:text = a:completed_item['word']
  if has_key(a:completion_item, 'textEdit') && type(a:completion_item['textEdit']) == v:t_dict
    let l:text = a:completion_item['textEdit']['newText']
  elseif has_key(a:completion_item, 'insertText')
    let l:text = a:completion_item['insertText']
  endif
  return l:text
endfunction

function! s:handle_completion() abort
  if empty(v:completed_item)
    return
  endif

  echo v:completed_item

  let l:completion_item = v:completed_item.user_data.nvim.lsp.completion_item
  let l:new_text = s:get_expand_text(v:completed_item, l:completion_item)
  if strlen(l:new_text) > 0
    let l:line = getline('.')
    call s:clear_inserted_text(
          \   l:line,
          \   getpos('.'),
          \   copy(v:completed_item),
          \   l:completion_item,
          \ )
    call feedkeys("\<C-r>=UltiSnips#Anon(\"" . s:escape_string(l:new_text) . "\", \"\")\<CR>")
  endif

endfunction
autocmd CompleteDone * call s:handle_completion()

This works more or less but it's off because of the completion character typed (so the . might come before the completed text instead of after πŸ˜… ). Not sure how to fix that exactly πŸ˜•

DonnieWest commented 4 years ago

Awesome! Mind sending it as a PR? I want to ensure you get proper credit for this

Also, do you know how of a good LSP server and a sample project I can test this out on? I don't use snippets personally πŸ˜…

entropitor commented 4 years ago

I would, if it was fully working but it's not πŸ˜…

let x = scanner.

would currently expand into

let x = .scanner

(with scanner being autocompleted the moment I press . so something is off but I've no idea how to fix that. I guess we need to know the trigger characters but I've no idea how to get that)

You can test it using https://github.com/vscode-langservers/vscode-json-languageserver for package.json autocompletion for example, although it's not working very well. Rust-analyzer also has snippet support.

DonnieWest commented 4 years ago

With some pretty minor tweaks, this works super well with neosnippet. I've not been able to replicate the autocompleted . though since I haven't found a snippet with it yet

Do you have some example projects or anything I can test that out with?

entropitor commented 4 years ago

@DonnieWest if you use https://rust-analyzer.github.io/manual.html#installation, you should be able to use any rust project (e.g. cargo init --bin test)

Write something like:

    let x= Ok(true);
    x.ok()$0

This is what you get without this change for the autocompletion for x, whit this change, you have the problem of the dot being in the wrong place.

DonnieWest commented 4 years ago

Haven't had the time to look into the insertion issue, but this looks highly promising. It'd be great if Neovim handled this stuff almost entirely and we just needed a little glue code and nothing else to handle LSP snippets

https://github.com/neovim/neovim/commit/5a9226c800d3075821203952da7c38626180680d

entropitor commented 4 years ago

@DonnieWest I agree completely: https://github.com/neovim/neovim/pull/12118#issuecomment-632666575

DonnieWest commented 4 years ago

https://asciinema.org/a/MFgYKxOO4tC0p79u7tuHzZlcS

Seems to work pretty well with the modified Neosnippet support provided below

Neosnippet support

``` function! s:escape_string(str) abort let l:ret = substitute(a:str, '\%x00', '\\n', 'g') let l:ret = substitute(l:ret, '"', '\\"', 'g') return l:ret endfunction function! s:escape_snippet(text) abort " neosnippet.vim expects the tabstops to have curly braces around " them, e.g. ${1} instead of $1, so we add these in now. let l:snippet = substitute(a:text, '\$\(\d\+\)', '${\1}', 'g') " Escape single quotes let l:snippet = substitute(l:snippet, "'", "''", 'g') " Make sure the snippet ends in ${0} if l:snippet !~# "\${0}$" let l:snippet .= "${0}" endif return l:snippet endfunction " This function can be error prone if the caller forgets to use +1 to vim line " so use lsp#utils#position#lsp_to_vim instead " Convert a character-index (0-based) to byte-index (1-based) " This function requires a buffer specifier (expr, see :help bufname()), " a line number (lnum, 1-based), and a character-index (char, 0-based). function! s:to_col(expr, lnum, char) abort let l:lines = getbufline(a:expr, a:lnum) if l:lines == [] if type(a:expr) != v:t_string || !filereadable(a:expr) " invalid a:expr return a:char + 1 endif " a:expr is a file that is not yet loaded as a buffer let l:lines = readfile(a:expr, '', a:lnum) endif let l:linestr = l:lines[-1] return strlen(strcharpart(l:linestr, 0, a:char)) + 1 endfunction " Convert a byte-index (1-based) to a character-index (0-based) " This function requires a buffer specifier (expr, see :help bufname()), " a line number (lnum, 1-based), and a byte-index (char, 1-based). function! _lsp_utils_to_char(expr, lnum, col) abort let l:lines = getbufline(a:expr, a:lnum) if l:lines == [] if type(a:expr) != v:t_string || !filereadable(a:expr) " invalid a:expr return a:col - 1 endif " a:expr is a file that is not yet loaded as a buffer let l:lines = readfile(a:expr, '', a:lnum) endif let l:linestr = l:lines[-1] return strchars(strpart(l:linestr, 0, a:col - 1)) endfunction function! s:decode_uri(uri) abort let l:ret = substitute(a:uri, '[?#].*', '', '') return substitute(l:ret, '%\(\x\x\)', '\=printf("%c", str2nr(submatch(1), 16))', 'g') endfunction function! s:urlencode_char(c) abort return printf('%%%02X', char2nr(a:c)) endfunction function! s:get_prefix(path) abort return matchstr(a:path, '\(^\w\+::\|^\w\+://\)') endfunction function! s:encode_uri(path, start_pos_encode, default_prefix) abort let l:prefix = s:get_prefix(a:path) let l:path = a:path[len(l:prefix):] if len(l:prefix) == 0 let l:prefix = a:default_prefix endif let l:result = strpart(a:path, 0, a:start_pos_encode) for l:i in range(a:start_pos_encode, len(l:path) - 1) " Don't encode '/' here, `path` is expected to be a valid path. if l:path[l:i] =~# '^[a-zA-Z0-9_.~/-]$' let l:result .= l:path[l:i] else let l:result .= s:urlencode_char(l:path[l:i]) endif endfor return l:prefix . l:result endfunction if has('win32') || has('win64') function! _lsp_utils_path_to_uri(path) abort if empty(a:path) return a:path else " You must not encode the volume information on the path if " present let l:end_pos_volume = matchstrpos(a:path, '\c[A-Z]:')[2] if l:end_pos_volume == -1 let l:end_pos_volume = 0 endif return s:encode_uri(substitute(a:path, '\', '/', 'g'), l:end_pos_volume, 'file:///') endif endfunction else function! _lsp_utils_path_to_uri(path) abort if empty(a:path) return a:path else return s:encode_uri(a:path, 0, 'file://') endif endfunction endif if has('win32') || has('win64') function! _lsp_utils_uri_to_path(uri) abort return substitute(s:decode_uri(a:uri[len('file:///'):]), '/', '\\', 'g') endfunction else function! _lsp_utils_uri_to_path(uri) abort return s:decode_uri(a:uri[len('file://'):]) endfunction endif function! _lsp_utils_get_buffer_uri(...) abort return _lsp_utils_path_to_uri(expand((a:0 > 0 ? '#' . a:1 : '%') . ':p')) endfunction " " _check " " LSP Spec says `multiple text edits can not overlap those ranges`. " This function check it. But does not throw error. " function! s:_check(text_edits) abort if len(a:text_edits) > 1 let l:range = a:text_edits[0].range for l:text_edit in a:text_edits[1 : -1] if l:range.end.line > l:text_edit.range.start.line || ( \ l:range.end.line == l:text_edit.range.start.line && \ l:range.end.character > l:text_edit.range.start.character \ ) call lsp#log('text_edit: range overlapped.') endif let l:range = l:text_edit.range endfor endif return a:text_edits endfunction " " _normalize " function! s:_normalize(text_edits) abort let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits] let l:text_edits = filter(copy(l:text_edits), { _, text_edit -> type(text_edit) == type({}) }) let l:text_edits = s:_range(l:text_edits) let l:text_edits = sort(copy(l:text_edits), function('s:_compare', [], {})) let l:text_edits = s:_check(l:text_edits) return reverse(l:text_edits) endfunction " " _compare " function! s:_compare(text_edit1, text_edit2) abort let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line if l:diff == 0 return a:text_edit1.range.start.character - a:text_edit2.range.start.character endif return l:diff endfunction " " _range " function! s:_range(text_edits) abort for l:text_edit in a:text_edits if l:text_edit.range.start.line > l:text_edit.range.end.line || ( \ l:text_edit.range.start.line == l:text_edit.range.end.line && \ l:text_edit.range.start.character > l:text_edit.range.end.character \ ) let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start } endif endfor return a:text_edits endfunction " " _switch " function! s:_switch(path) abort if bufnr(a:path) >= 0 execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path)) else execute printf('keepalt keepjumps edit! %s', fnameescape(a:path)) endif endfunction function! _lsp_utils_text_edit_apply_text_edits(uri, text_edits) abort let l:current_bufname = bufname('%') let l:target_bufname = _lsp_utils_uri_to_path(a:uri) let l:cursor_pos = getpos('.')[1 : 3] let l:cursor_offset = 0 let l:topline = line('w0') call s:_switch(l:target_bufname) for l:text_edit in s:_normalize(a:text_edits) let l:cursor_offset += s:_apply(bufnr(l:target_bufname), l:text_edit, l:cursor_pos) endfor call s:_switch(l:current_bufname) if bufnr(l:current_bufname) == bufnr(l:target_bufname) let l:length = strlen(getline(l:cursor_pos[0])) + 1 let l:cursor_pos[2] = max([0, l:cursor_pos[1] + l:cursor_pos[2] - l:length]) let l:cursor_pos[1] = min([l:length, l:cursor_pos[1] + l:cursor_pos[2]]) call cursor(l:cursor_pos) call winrestview({ 'topline': l:topline + l:cursor_offset }) endif endfunction function! _lsp_utils__split_by_eol(text) abort return split(a:text, '\r\n\|\r\|\n', v:true) endfunction let s:fixendofline_exists = exists('+fixendofline') function! _lsp_utils_buffer__get_fixendofline(buf) abort let l:eol = getbufvar(a:buf, '&endofline') let l:binary = getbufvar(a:buf, '&binary') if s:fixendofline_exists let l:fixeol = getbufvar(a:buf, '&fixendofline') if !l:binary " When 'binary' is off and 'fixeol' is on, 'endofline' is not used " " When 'binary' is off and 'fixeol' is off, 'endofline' is used to " remember the presence of a return l:fixeol || l:eol else " When 'binary' is on, the value of 'fixeol' doesn't matter return l:eol endif else " When 'binary' is off the value of 'endofline' is not used " " When 'binary' is on 'endofline' is used to remember the presence of " a return !l:binary || l:eol endif endfunction " " _apply " function! s:_apply(bufnr, text_edit, cursor_pos) abort " create before/after line. let l:start_line = getline(a:text_edit.range.start.line + 1) let l:end_line = getline(a:text_edit.range.end.line + 1) let l:before_line = strcharpart(l:start_line, 0, a:text_edit.range.start.character) let l:after_line = strcharpart(l:end_line, a:text_edit.range.end.character, strchars(l:end_line) - a:text_edit.range.end.character) " create new lines. let l:new_lines = _lsp_utils__split_by_eol(a:text_edit.newText) let l:new_lines[0] = l:before_line . l:new_lines[0] let l:new_lines[-1] = l:new_lines[-1] . l:after_line " fixendofline let l:buffer_length = len(getbufline(a:bufnr, '^', '$')) let l:should_fixendofline = _lsp_utils_buffer__get_fixendofline(a:bufnr) let l:should_fixendofline = l:should_fixendofline && l:new_lines[-1] ==# '' let l:should_fixendofline = l:should_fixendofline && l:buffer_length <= a:text_edit['range']['end']['line'] let l:should_fixendofline = l:should_fixendofline && a:text_edit['range']['end']['character'] == 0 if l:should_fixendofline call remove(l:new_lines, -1) endif let l:new_lines_len = len(l:new_lines) " fix cursor pos let l:cursor_offset = 0 if a:text_edit.range.end.line + 1 < a:cursor_pos[0] let l:cursor_offset = l:new_lines_len - (a:text_edit.range.end.line - a:text_edit.range.start.line) - 1 let a:cursor_pos[0] += l:cursor_offset endif " append new lines. call append(a:text_edit.range.start.line, l:new_lines) " remove old lines execute printf('%s,%sdelete _', \ l:new_lines_len + a:text_edit.range.start.line + 1, \ min([l:new_lines_len + a:text_edit.range.end.line + 1, line('$')]) \ ) return l:cursor_offset endfunction " @param expr = see :help bufname() " @param position = { " 'line': 1, " 'character': 1 " } " @returns [ " line, " col " ] function! _lsp_utils_position_lsp_to_vim(expr, position) abort let l:line = a:position['line'] + 1 let l:char = a:position['character'] let l:col = s:to_col(a:expr, l:line, l:char) return [l:line, l:col] endfunction function! s:clear_inserted_text(line, done_position, completed_item, completion_item) abort " Remove commit characters. call setline('.', a:line) " Create range to remove v:completed_item. let l:range = { \ 'start': { \ 'line': a:done_position[1] - 1, \ 'character': _lsp_utils_to_char('%', a:done_position[1], a:done_position[2] + a:done_position[3]) - strchars(a:completed_item['word']) \ }, \ 'end': { \ 'line': a:done_position[1] - 1, \ 'character': _lsp_utils_to_char('%', a:done_position[1], a:done_position[2] + a:done_position[3]) \ } \ } " Expand remove range to textEdit. if has_key(a:completion_item, 'textEdit') let l:range = { \ 'start': { \ 'line': a:completion_item['textEdit']['range']['start']['line'], \ 'character': a:completion_item['textEdit']['range']['start']['character'], \ }, \ 'end': { \ 'line': a:completion_item['textEdit']['range']['end']['line'], \ 'character': a:completion_item['textEdit']['range']['end']['character'] + strchars(a:completed_item['word']) \ } \ } endif " Remove v:completed_item.word (and textEdit range if need). call _lsp_utils_text_edit_apply_text_edits(_lsp_utils_get_buffer_uri(bufnr('%')), [{ \ 'range': l:range, \ 'newText': '' \ }]) " Move to complete start position. call cursor(_lsp_utils_position_lsp_to_vim('%', l:range['start'])) endfunction function! s:get_expand_text(completed_item, completion_item) abort let l:text = a:completed_item['word'] if has_key(a:completion_item, 'textEdit') && type(a:completion_item['textEdit']) == v:t_dict let l:text = a:completion_item['textEdit']['newText'] elseif has_key(a:completion_item, 'insertText') let l:text = a:completion_item['insertText'] endif return l:text endfunction function! s:handle_completion() abort if empty(v:completed_item) return endif echo v:completed_item let l:completion_item = v:completed_item.user_data.nvim.lsp.completion_item let l:new_text = s:get_expand_text(v:completed_item, l:completion_item) if strlen(l:new_text) > 0 let l:line = getline('.') call s:clear_inserted_text( \ l:line, \ getpos('.'), \ copy(v:completed_item), \ l:completion_item, \ ) Echos l:new_text call feedkeys("\=neosnippet#anonymous('" . s:escape_snippet(l:new_text) . "')\") endif endfunction autocmd CompleteDone * call s:handle_completion() ```

entropitor commented 4 years ago

Hmm, must be something to do with the difference between ultisnips and neosnippet then :/

DonnieWest commented 4 years ago

@entropitor could you please submit a PR out with your original work? I'd like to add the work I did here to get us neosnippet support and get it merged in. We can use that as a jumping point to also add Ultisnips support.

I'd like to give you proper credit πŸ˜‰