Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions autoload/lsp.vim
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function! lsp#enable() abort
call lsp#ui#vim#completion#_setup()
call lsp#internal#document_highlight#_enable()
call lsp#internal#diagnostics#_enable()
call lsp#internal#linked_editing_range#_enable()
call lsp#internal#show_message_request#_enable()
call lsp#internal#work_done_progress#_enable()
call s:register_events()
Expand Down Expand Up @@ -518,6 +519,9 @@ function! lsp#default_get_supported_capabilities(server_info) abort
\ 'dynamicRegistration': v:false,
\ 'linkSupport' : v:true
\ },
\ 'linkedEditingRange': {
\ 'dynamicRegistration': v:false,
\ },
\ 'rangeFormatting': {
\ 'dynamicRegistration': v:false,
\ },
Expand Down
4 changes: 4 additions & 0 deletions autoload/lsp/capabilities.vim
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function! lsp#capabilities#has_implementation_provider(server_name) abort
return s:has_provider(a:server_name, 'implementationProvider')
endfunction

function! lsp#capabilities#has_linked_editing_range_provider(server_name) abort
return s:has_provider(a:server_name, 'linkedEditingRangeProvider')
endfunction

function! lsp#capabilities#has_code_action_provider(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider')
Expand Down
136 changes: 136 additions & 0 deletions autoload/lsp/internal/linked_editing_range.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
let s:TextEdit = vital#lsp#import('VS.LSP.TextEdit')
let s:TextMark = vital#lsp#import('VS.Vim.Buffer.TextMark')

let s:TEXT_MARK_NAMESPACE = 'lsp#internal#linked_editing_range'

let s:state = {}
let s:state['bufnr'] = -1
let s:state['changenr'] = -1
let s:state['changedtick'] = -1

function! lsp#internal#linked_editing_range#_enable() abort
if !s:TextEdit.is_text_mark_preserved()
return
endif

if !g:lsp_linked_editing_range_enabled | return | endif
let s:Dispose = lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter']),
\ lsp#callbag#flatMap({ -> s:request(v:false) }),
\ lsp#callbag#subscribe({
\ 'next': { x -> s:prepare(x) }
\ })
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertLeave']),
\ lsp#callbag#subscribe({ -> s:clear() })
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['TextChanged', 'TextChangedI', 'TextChangedP']),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think for now it is ok. but TextChangedP is not available in all versions. One easy fix would be to check for TextChangedP support in s:enabled

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will also hopefully encourage folks to migrate to newer versions.

\ lsp#callbag#delay(0),
\ lsp#callbag#subscribe({ -> s:sync() })
\ ),
\ )
endfunction

function! lsp#internal#linked_editing_range#_disable() abort
if exists('s:Dispose')
call s:clear()
call s:Dispose()
unlet s:Dispose
endif
endfunction

function! s:request(sync) abort
let l:server = lsp#get_allowed_servers(&filetype)
let l:server = filter(l:server, 'lsp#capabilities#has_linked_editing_range_provider(v:val)')
let l:server = get(l:server, 0, v:null)
if empty(l:server)
return lsp#callbag#empty()
endif

let l:X = lsp#callbag#pipe(
\ lsp#request(l:server, {
\ 'method': 'textDocument/linkedEditingRange',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ }
\ }),
\ )
if a:sync
return lsp#callbag#of(
\ get(
\ lsp#callbag#pipe(
\ l:X,
\ lsp#callbag#toList()
\ ).wait({ 'sleep': 1, 'timeout': 200 }),
\ 0,
\ v:null
\ )
\ )
endif
return l:X
endfunction

function! s:prepare(x) abort
if empty(get(a:x['response']['result'], 'ranges', {}))
return
endif

call s:TextMark.set(bufnr('%'), s:TEXT_MARK_NAMESPACE, map(a:x['response']['result']['ranges'], { _, range -> {
\ 'range': range,
\ 'highlight': 'Underlined',
\ } }))
let s:state['bufnr'] = bufnr('%')
let s:state['changenr'] = changenr()
let s:state['changedtick'] = b:changedtick
endfunction

function! s:clear() abort
call s:TextMark.clear(bufnr('%'), s:TEXT_MARK_NAMESPACE)
endfunction

function! s:sync() abort
let l:bufnr = bufnr('%')
if s:state['bufnr'] != l:bufnr
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for callbags this is usually not preferred. you can instead use switchMap and takeUntil similar to this https://github.com/prabirshrestha/vim-lsp/blob/master/autoload/lsp/internal/document_highlight.vim

Feel free to change or ignore it for now and I can refactor it later when it is merged.

I usually use RxJS or other Rx libraries does to understand callbag.

High level these are what it means.

  1. switchMap -> when ever you have a next, cancel the existing one and start a new one. Good example for this is autocomplete where if I type he and then type hello I want to cancel he and automatically start hello.
  2. takeUntil -> this is use for cancellation. basically it means when a next is called in takeuntil cancel the stream.

But since you are using wait what you have should be ok.

Some good reading here. https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I learned a lot.

return
endif
if s:state['changedtick'] == b:changedtick
return
endif
if s:state['changenr'] > changenr()
return
endif

" get current mark and related marks.
let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
let l:current_mark = v:null
let l:related_marks = []
for l:mark in s:TextMark.get(l:bufnr, s:TEXT_MARK_NAMESPACE)
if lsp#utils#range#_contains(l:mark['range'], l:position)
let l:current_mark = l:mark
else
let l:related_marks += [l:mark]
endif
endfor

" ignore if current mark is not detected.
if empty(l:current_mark)
return
endif

" apply new text for related marks.
let l:new_text = lsp#utils#range#_get_text(l:bufnr, l:current_mark['range'])
call lsp#utils#text_edit#apply_text_edits(l:bufnr, map(l:related_marks, { _, mark -> {
\ 'range': mark['range'],
\ 'newText': l:new_text
\ } }))

" save state.
let s:state['bufnr'] = l:bufnr
let s:state['changenr'] = changenr()
let s:state['changedtick'] = b:changedtick
endfunction

32 changes: 32 additions & 0 deletions autoload/lsp/utils/range.vim
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,38 @@ function! lsp#utils#range#_get_current_line_range() abort
return l:range
endfunction

" Returns the range contains specified position or not.
function! lsp#utils#range#_contains(range, position) abort
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this can be used to solve this issue now. #888

if !(
\ a:range['start']['line'] <= a:position['line'] && (
\ a:range['start']['line'] == a:position['line'] &&
\ a:range['start']['character'] <= a:position['character']
\ )
\ )
return v:false
endif
if !(
\ a:range['end']['line'] >= a:position['line'] && (
\ a:range['end']['line'] == a:position['line'] &&
\ a:range['end']['character'] >= a:position['character']
\ )
\ )
return v:false
endif
return v:true
endfunction

" Return the range of text for the specified expr.
function! lsp#utils#range#_get_text(expr, range) abort
let l:lines = []
for l:line in range(a:range['start']['line'], a:range['end']['line'])
let l:lines += getbufline(a:expr, l:line + 1)
endfor
let l:lines[-1] = strcharpart(l:lines[-1], 0, a:range['end']['character'])
let l:lines[0] = strcharpart(l:lines[0], a:range['start']['character'], strchars(l:lines[0]))
return join(l:lines, "\n")
endfunction

" Convert a LSP range to one or more vim match positions.
" If the range spans over multiple lines, break it down to multiple
" positions, one for each line.
Expand Down
155 changes: 4 additions & 151 deletions autoload/lsp/utils/text_edit.vim
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
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)
let s:TextEdit = vital#lsp#import('VS.LSP.TextEdit')

if bufnr(l:current_bufname) == bufnr(l:target_bufname)
call cursor(lsp#utils#position#lsp_to_vim('%', l:cursor_position))
endif
function! lsp#utils#text_edit#apply_text_edits(uri, text_edits) abort
return s:TextEdit.apply(lsp#utils#uri_to_path(a:uri), deepcopy(a:text_edits))
endfunction


" @summary Use this to convert textedit to vim list that is compatible with
" quickfix and locllist items
" @param uri = DocumentUri
Expand Down Expand Up @@ -78,141 +69,3 @@ function! s:lsp_text_edit_item_to_vim(uri, text_edit, cache) abort
\ }
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) >= 0
execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path))
else
execute printf('keepalt keepjumps edit! %s', fnameescape(a:path))
endif
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

9 changes: 9 additions & 0 deletions autoload/vital/_lsp.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let s:_plugin_name = expand('<sfile>:t:r')

function! vital#{s:_plugin_name}#new() abort
return vital#{s:_plugin_name[1:]}#new()
endfunction

function! vital#{s:_plugin_name}#function(funcname) abort
silent! return function(a:funcname)
endfunction
Loading