--- /dev/null
+function! lsp#utils#text_edit#get_range(text_edit) abort
+ if type(a:text_edit) != v:t_dict
+ return v:null
+ endif
+ let l:insert = get(a:text_edit, 'insert', v:null)
+ if type(l:insert) == v:t_dict
+ return l:insert
+ endif
+ return get(a:text_edit, 'range', v:null)
+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_position = lsp#get_position()
+
+ call s:_switch(l:target_bufname)
+ for l:text_edit in s:_normalize(a:text_edits)
+ call s:_apply(bufnr(l:target_bufname), l:text_edit, l:cursor_position)
+ endfor
+ call s:_switch(l:current_bufname)
+
+ if bufnr(l:current_bufname) == bufnr(l:target_bufname)
+ call cursor(lsp#utils#position#lsp_to_vim('%', l:cursor_position))
+ endif
+endfunction
+
+" @summary Use this to convert textedit to vim list that is compatible with
+" quickfix and locllist items
+" @param uri = DocumentUri
+" @param text_edit = TextEdit | TextEdit[]
+" @returns []
+function! lsp#utils#text_edit#_lsp_to_vim_list(uri, text_edit) abort
+ let l:result = []
+ let l:cache = {}
+ if type(a:text_edit) == type([]) " TextEdit[]
+ for l:text_edit in a:text_edit
+ let l:vim_loc = s:lsp_text_edit_item_to_vim(a:uri, l:text_edit, l:cache)
+ if !empty(l:vim_loc)
+ call add(l:result, l:vim_loc)
+ endif
+ endfor
+ else " TextEdit
+ let l:vim_loc = s:lsp_text_edit_item_to_vim(a:uri, a:text_edit, l:cache)
+ if !empty(l:vim_loc)
+ call add(l:result, l:vim_loc)
+ endif
+ endif
+ return l:result
+endfunction
+
+" @param uri = DocumentUri
+" @param text_edit = TextEdit
+" @param cache = {} empty dict
+" @returns {
+" 'filename',
+" 'lnum',
+" 'col',
+" 'text',
+" }
+function! s:lsp_text_edit_item_to_vim(uri, text_edit, cache) abort
+ if !lsp#utils#is_file_uri(a:uri)
+ return v:null
+ endif
+
+ let l:path = lsp#utils#uri_to_path(a:uri)
+ let l:range = a:text_edit['range']
+ let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start'])
+
+ let l:index = l:line - 1
+ if has_key(a:cache, l:path)
+ let l:text = a:cache[l:path][l:index]
+ else
+ let l:contents = getbufline(l:path, 1, '$')
+ if !empty(l:contents)
+ let l:text = get(l:contents, l:index, '')
+ else
+ let l:contents = readfile(l:path)
+ let a:cache[l:path] = l:contents
+ let l:text = get(l:contents, l:index, '')
+ endif
+ endif
+
+ return {
+ \ 'filename': l:path,
+ \ 'lnum': l:line,
+ \ 'col': l:col,
+ \ 'text': l:text
+ \ }
+endfunction
+
+"
+" _apply
+"
+function! s:_apply(bufnr, text_edit, cursor_position) 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
+
+ " save length.
+ let l:new_lines_len = len(l:new_lines)
+ let l:range_len = (a:text_edit['range']['end']['line'] - a:text_edit['range']['start']['line']) + 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
+
+ " fix cursor pos
+ if a:text_edit['range']['end']['line'] < a:cursor_position['line']
+ " fix cursor line
+ let a:cursor_position['line'] += l:new_lines_len - l:range_len
+ elseif a:text_edit['range']['end']['line'] == a:cursor_position['line'] && a:text_edit['range']['end']['character'] <= a:cursor_position['character']
+ " fix cursor line and col
+ let a:cursor_position['line'] += l:new_lines_len - l:range_len
+ let l:end_character = strchars(l:new_lines[-1]) - strchars(l:after_line)
+ let l:end_offset = a:cursor_position['character'] - a:text_edit['range']['end']['character']
+ let a:cursor_position['character'] = l:end_character + l:end_offset
+ endif
+
+ " append or delete lines.
+ if l:new_lines_len > l:range_len
+ call append(a:text_edit['range']['start']['line'], repeat([''], l:new_lines_len - l:range_len))
+ elseif l:new_lines_len < l:range_len
+ let l:offset = l:range_len - l:new_lines_len
+ call s:delete(a:bufnr, a:text_edit['range']['start']['line'] + 1, a:text_edit['range']['start']['line'] + l:offset)
+ endif
+
+ " set lines.
+ call setline(a:text_edit['range']['start']['line'] + 1, l:new_lines)
+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
+
+"
+" _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
+
+"
+" _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
+
+"
+" _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
+
+"
+" _switch
+"
+function! s:_switch(path) abort
+ if bufnr(a:path) == -1
+ execute printf('badd %s', fnameescape(a:path))
+ endif
+ execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path))
+endfunction
+
+"
+" delete
+"
+function! s:delete(bufnr, start, end) abort
+ if exists('*deletebufline')
+ call deletebufline(a:bufnr, a:start, a:end)
+ else
+ let l:foldenable = &foldenable
+ setlocal nofoldenable
+ execute printf('%s,%sdelete _', a:start, a:end)
+ let &foldenable = l:foldenable
+ endif
+endfunction
+