|
|
|
|
@@ -1,260 +1,140 @@
|
|
|
|
|
function! lsp#utils#text_edit#apply_text_edits(uri, text_edits) abort
|
|
|
|
|
" https://microsoft.github.io/language-server-protocol/specification#textedit
|
|
|
|
|
" The order in the array defines the order in which the inserted string
|
|
|
|
|
" appear in the resulting text.
|
|
|
|
|
"
|
|
|
|
|
" The edits must be applied in the reverse order so the early edits will
|
|
|
|
|
" not interfere with the position of later edits, they need to be applied
|
|
|
|
|
" one at the time or put together as a single command.
|
|
|
|
|
"
|
|
|
|
|
" Example: {"range": {"end": {"character": 45, "line": 5}, "start":
|
|
|
|
|
" {"character": 45, "line": 5}}, "newText": "\n"}, {"range": {"end":
|
|
|
|
|
" {"character": 45, "line": 5}, "start": {"character": 45, "line": 5}},
|
|
|
|
|
" "newText": "import javax.ws.rs.Consumes;"}]}}
|
|
|
|
|
"
|
|
|
|
|
" If we apply the \n first we will need adjust the line range of the next
|
|
|
|
|
" command (so the import will be written on the next line) , but if we
|
|
|
|
|
" write the import first and then the \n everything will be fine.
|
|
|
|
|
" If you do not apply a command one at time, you will need to adjust the
|
|
|
|
|
" range columns after which edit. You will get this (only one execution):
|
|
|
|
|
"
|
|
|
|
|
" execute 'keepjumps normal! 6G045laimport javax.ws.rs.Consumes;'" |
|
|
|
|
|
" execute 'keepjumps normal! 6G045la\n'
|
|
|
|
|
"
|
|
|
|
|
" resulting in this:
|
|
|
|
|
" import javax.servlet.http.HttpServletRequest;i
|
|
|
|
|
" mport javax.ws.rs.Consumes;
|
|
|
|
|
"
|
|
|
|
|
" instead of this (multiple executions):
|
|
|
|
|
" execute 'keepjumps normal! 6G045laimport javax.ws.rs.Consumes;'"
|
|
|
|
|
" execute 'keepjumps normal! 6G045li\n'
|
|
|
|
|
"
|
|
|
|
|
" resulting in this:
|
|
|
|
|
" import javax.servlet.http.HttpServletRequest;
|
|
|
|
|
" import javax.ws.rs.Consumes;
|
|
|
|
|
"
|
|
|
|
|
"
|
|
|
|
|
" The sort is also necessary since the LSP specification does not
|
|
|
|
|
" guarantee that text edits are sorted.
|
|
|
|
|
"
|
|
|
|
|
" Example:
|
|
|
|
|
" Initial text: "abcdef"
|
|
|
|
|
" Edits:
|
|
|
|
|
" ((0, 0), (0, 1), "") - remove first character 'a'
|
|
|
|
|
" ((0, 4), (0, 5), "") - remove fifth character 'e'
|
|
|
|
|
" ((0, 2), (0, 3), "") - remove third character 'c'
|
|
|
|
|
if empty(a:text_edits)
|
|
|
|
|
return
|
|
|
|
|
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 l:current_bufname == l:target_bufname
|
|
|
|
|
let l:length = strlen(getline(l:cursor_pos[0]))
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
let l:text_edits = sort(deepcopy(a:text_edits), '<SID>sort_text_edit_desc')
|
|
|
|
|
let l:i = 0
|
|
|
|
|
|
|
|
|
|
while l:i < len(l:text_edits)
|
|
|
|
|
let l:merged_text_edit = s:merge_same_range(l:i, l:text_edits)
|
|
|
|
|
let l:cmd = s:build_cmd(a:uri, l:merged_text_edit['merged'])
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
let l:was_paste = &paste
|
|
|
|
|
let l:was_selection = &selection
|
|
|
|
|
let l:was_virtualedit = &virtualedit
|
|
|
|
|
let l:was_view = winsaveview()
|
|
|
|
|
|
|
|
|
|
set paste
|
|
|
|
|
|
|
|
|
|
let l:start_line = l:merged_text_edit['merged']['range']['start']['line']
|
|
|
|
|
let l:end_line = l:merged_text_edit['merged']['range']['end']['line']
|
|
|
|
|
let l:end_character = l:merged_text_edit['merged']['range']['end']['character']
|
|
|
|
|
if l:start_line < l:end_line && l:end_character <= 0
|
|
|
|
|
" set inclusive if end position was newline character.
|
|
|
|
|
set selection=inclusive
|
|
|
|
|
else
|
|
|
|
|
set selection=exclusive
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
set virtualedit=onemore
|
|
|
|
|
|
|
|
|
|
silent execute l:cmd
|
|
|
|
|
finally
|
|
|
|
|
let &paste = l:was_paste
|
|
|
|
|
let &selection = l:was_selection
|
|
|
|
|
let &virtualedit = l:was_virtualedit
|
|
|
|
|
call winrestview(l:was_view)
|
|
|
|
|
endtry
|
|
|
|
|
|
|
|
|
|
let l:i = l:merged_text_edit['end_index']
|
|
|
|
|
endwhile
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
" Merge the edits on the same range so we do not have to reverse the
|
|
|
|
|
" text_edits that are inserts, also from the specification:
|
|
|
|
|
" If multiple inserts have the same position, the order in the array
|
|
|
|
|
" defines the order in which the inserted strings appear in the
|
|
|
|
|
" resulting text
|
|
|
|
|
function! s:merge_same_range(start_index, text_edits) abort
|
|
|
|
|
let l:i = a:start_index + 1
|
|
|
|
|
let l:merged = deepcopy(a:text_edits[a:start_index])
|
|
|
|
|
|
|
|
|
|
while l:i < len(a:text_edits) &&
|
|
|
|
|
\ s:is_same_range(l:merged['range'], a:text_edits[l:i]['range'])
|
|
|
|
|
|
|
|
|
|
let l:merged['newText'] .= a:text_edits[l:i]['newText']
|
|
|
|
|
let l:i += 1
|
|
|
|
|
endwhile
|
|
|
|
|
|
|
|
|
|
return {'merged': l:merged, 'end_index': l:i}
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
function! s:is_same_range(range1, range2) abort
|
|
|
|
|
return a:range1['start']['line'] == a:range2['start']['line'] &&
|
|
|
|
|
\ a:range1['end']['line'] == a:range2['end']['line'] &&
|
|
|
|
|
\ a:range1['start']['character'] == a:range2['start']['character'] &&
|
|
|
|
|
\ a:range1['end']['character'] == a:range2['end']['character']
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
" https://microsoft.github.io/language-server-protocol/specification#textedit
|
|
|
|
|
function! s:is_insert(range) abort
|
|
|
|
|
return a:range['start']['line'] == a:range['end']['line'] &&
|
|
|
|
|
\ a:range['start']['character'] == a:range['end']['character']
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
" Compares two text edits, based on the starting position of the range.
|
|
|
|
|
" Assumes that edits have non-overlapping ranges.
|
|
|
|
|
"
|
|
|
|
|
" `text_edit1` and `text_edit2` are dictionaries and represent LSP TextEdit type.
|
|
|
|
|
" _apply
|
|
|
|
|
"
|
|
|
|
|
" Returns 0 if both text edits starts at the same position (insert text),
|
|
|
|
|
" positive value if `text_edit1` starts before `text_edit2` and negative value
|
|
|
|
|
" otherwise.
|
|
|
|
|
function! s:sort_text_edit_desc(text_edit1, text_edit2) abort
|
|
|
|
|
if a:text_edit1['range']['start']['line'] != a:text_edit2['range']['start']['line']
|
|
|
|
|
return a:text_edit2['range']['start']['line'] - a:text_edit1['range']['start']['line']
|
|
|
|
|
endif
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
if a:text_edit1['range']['start']['character'] != a:text_edit2['range']['start']['character']
|
|
|
|
|
return a:text_edit2['range']['start']['character'] - a:text_edit1['range']['start']['character']
|
|
|
|
|
endif
|
|
|
|
|
" 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
|
|
|
|
|
|
|
|
|
|
return !s:is_insert(a:text_edit1['range']) ? -1 :
|
|
|
|
|
\ s:is_insert(a:text_edit2['range']) ? 0 : 1
|
|
|
|
|
" 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 <= 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
|
|
|
|
|
let l:buffer_length = len(getbufline(a:bufnr, '^', '$'))
|
|
|
|
|
execute printf('%s,%sdelete _',
|
|
|
|
|
\ l:new_lines_len + a:text_edit.range.start.line + 1,
|
|
|
|
|
\ min([l:buffer_length, l:new_lines_len + a:text_edit.range.end.line + 1])
|
|
|
|
|
\ )
|
|
|
|
|
|
|
|
|
|
return l:cursor_offset
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
function! s:build_cmd(uri, text_edit) abort
|
|
|
|
|
let l:path = lsp#utils#uri_to_path(a:uri)
|
|
|
|
|
let l:buffer = bufnr(l:path)
|
|
|
|
|
let l:cmd = 'keepjumps keepalt ' . (l:buffer !=# -1 ? 'b ' . l:buffer : 'edit ' . l:path)
|
|
|
|
|
let s:text_edit = deepcopy(a:text_edit)
|
|
|
|
|
|
|
|
|
|
let s:text_edit['range'] = s:parse_range(s:text_edit['range'])
|
|
|
|
|
let l:sub_cmd = s:generate_sub_cmd(s:text_edit)
|
|
|
|
|
let l:escaped_sub_cmd = substitute(l:sub_cmd, '''', '''''', 'g')
|
|
|
|
|
let l:cmd = l:cmd . " | execute 'keepjumps normal! " . l:escaped_sub_cmd . "'"
|
|
|
|
|
|
|
|
|
|
call lsp#log('s:build_cmd', l:cmd)
|
|
|
|
|
|
|
|
|
|
return l:cmd
|
|
|
|
|
"
|
|
|
|
|
" _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
|
|
|
|
|
|
|
|
|
|
function! s:generate_sub_cmd(text_edit) abort
|
|
|
|
|
if s:is_insert(a:text_edit['range'])
|
|
|
|
|
return s:generate_sub_cmd_insert(a:text_edit)
|
|
|
|
|
else
|
|
|
|
|
return s:generate_sub_cmd_replace(a:text_edit)
|
|
|
|
|
"
|
|
|
|
|
" _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
|
|
|
|
|
|
|
|
|
|
function! s:generate_sub_cmd_insert(text_edit) abort
|
|
|
|
|
let l:start_line = a:text_edit['range']['start']['line']
|
|
|
|
|
let l:start_character = a:text_edit['range']['start']['character']
|
|
|
|
|
|
|
|
|
|
let l:sub_cmd = s:preprocess_cmd(a:text_edit['range'])
|
|
|
|
|
let l:sub_cmd .= s:generate_move_start_cmd(l:start_line, l:start_character)
|
|
|
|
|
|
|
|
|
|
if l:start_character >= strchars(getline(l:start_line))
|
|
|
|
|
let l:sub_cmd .= "\"=l:merged_text_edit['merged']['newText']\<CR>P"
|
|
|
|
|
else
|
|
|
|
|
let l:sub_cmd .= "\"=l:merged_text_edit['merged']['newText'].'?'\<CR>gPh\"_x"
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
return l:sub_cmd
|
|
|
|
|
"
|
|
|
|
|
" _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
|
|
|
|
|
|
|
|
|
|
function! s:generate_sub_cmd_replace(text_edit) abort
|
|
|
|
|
let l:start_line = a:text_edit['range']['start']['line']
|
|
|
|
|
let l:start_character = a:text_edit['range']['start']['character']
|
|
|
|
|
let l:end_line = a:text_edit['range']['end']['line']
|
|
|
|
|
let l:end_character = a:text_edit['range']['end']['character']
|
|
|
|
|
let l:new_text = a:text_edit['newText']
|
|
|
|
|
|
|
|
|
|
let l:sub_cmd = s:preprocess_cmd(a:text_edit['range'])
|
|
|
|
|
let l:sub_cmd .= s:generate_move_start_cmd(l:start_line, l:start_character) " move to the first position
|
|
|
|
|
|
|
|
|
|
" If start and end position are 0, we are selecting a range of lines.
|
|
|
|
|
" Thus, we can use linewise-visual mode, which avoids some inconsistencies
|
|
|
|
|
" when applying text edits.
|
|
|
|
|
if l:start_character == 0 && l:end_character == 0
|
|
|
|
|
let l:sub_cmd .= 'V'
|
|
|
|
|
else
|
|
|
|
|
let l:sub_cmd .= 'v'
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
let l:sub_cmd .= s:generate_move_end_cmd(l:end_line, l:end_character) " move to the last position
|
|
|
|
|
|
|
|
|
|
if len(l:new_text) == 0
|
|
|
|
|
let l:sub_cmd .= 'x'
|
|
|
|
|
elseif l:start_character == 0 && l:end_character == 0
|
|
|
|
|
let l:sub_cmd .= "\"=l:merged_text_edit['merged']['newText']\<CR>P"
|
|
|
|
|
else
|
|
|
|
|
let l:sub_cmd .= "\"=l:merged_text_edit['merged']['newText'].'?'\<CR>gph\"_x"
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
return l:sub_cmd
|
|
|
|
|
"
|
|
|
|
|
" _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
|
|
|
|
|
|
|
|
|
|
function! s:generate_move_start_cmd(line_pos, character_pos) abort
|
|
|
|
|
let l:result = printf('%dG0', a:line_pos) " move the line and set to the cursor at the beginning
|
|
|
|
|
if a:character_pos > 0
|
|
|
|
|
let l:result .= printf('%dl', a:character_pos) " move right until the character
|
|
|
|
|
endif
|
|
|
|
|
return l:result
|
|
|
|
|
"
|
|
|
|
|
" _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! s:generate_move_end_cmd(line_pos, character_pos) abort
|
|
|
|
|
let l:result = printf('%dG0', a:line_pos) " move the line and set to the cursor at the beginning
|
|
|
|
|
if a:character_pos > 1
|
|
|
|
|
let l:result .= printf('%dl', a:character_pos) " move right until the character
|
|
|
|
|
elseif a:character_pos == 0
|
|
|
|
|
let l:result = printf('%dG$', a:line_pos - 1) " move most right
|
|
|
|
|
endif
|
|
|
|
|
return l:result
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
function! s:preprocess_cmd(range) abort
|
|
|
|
|
" preprocess by opening the folds, this is needed because the line you are
|
|
|
|
|
" going might have a folding
|
|
|
|
|
let l:preprocess = ''
|
|
|
|
|
|
|
|
|
|
if foldlevel(a:range['start']['line']) > 0
|
|
|
|
|
let l:preprocess .= a:range['start']['line']
|
|
|
|
|
let l:preprocess .= 'GzO'
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
if foldlevel(a:range['end']['line']) > 0
|
|
|
|
|
let l:preprocess .= a:range['end']['line']
|
|
|
|
|
let l:preprocess .= 'GzO'
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
return l:preprocess
|
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
|
|
" https://microsoft.github.io/language-server-protocol/specification#text-documents
|
|
|
|
|
" Position in a text document expressed as zero-based line and zero-based
|
|
|
|
|
" character offset, and since we are using the character as a offset position
|
|
|
|
|
" we do not have to fix its position
|
|
|
|
|
function! s:parse_range(range) abort
|
|
|
|
|
let s:range = deepcopy(a:range)
|
|
|
|
|
let s:range['start']['line'] = a:range['start']['line'] + 1
|
|
|
|
|
let s:range['end']['line'] = a:range['end']['line'] + 1
|
|
|
|
|
|
|
|
|
|
return s:range
|
|
|
|
|
endfunction
|
|
|
|
|
|