Skip to content

Commit 9c99f91

Browse files
committed
Implement linkedEditingRange
1 parent cb506f5 commit 9c99f91

File tree

15 files changed

+1256
-151
lines changed

15 files changed

+1256
-151
lines changed

autoload/lsp.vim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function! lsp#enable() abort
6161
call lsp#ui#vim#completion#_setup()
6262
call lsp#internal#document_highlight#_enable()
6363
call lsp#internal#diagnostics#_enable()
64+
call lsp#internal#linked_editing_range#_enable()
6465
call lsp#internal#show_message_request#_enable()
6566
call lsp#internal#work_done_progress#_enable()
6667
call s:register_events()
@@ -518,6 +519,9 @@ function! lsp#default_get_supported_capabilities(server_info) abort
518519
\ 'dynamicRegistration': v:false,
519520
\ 'linkSupport' : v:true
520521
\ },
522+
\ 'linkedEditingRange': {
523+
\ 'dynamicRegistration': v:false,
524+
\ },
521525
\ 'rangeFormatting': {
522526
\ 'dynamicRegistration': v:false,
523527
\ },

autoload/lsp/capabilities.vim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ function! lsp#capabilities#has_implementation_provider(server_name) abort
5353
return s:has_provider(a:server_name, 'implementationProvider')
5454
endfunction
5555

56+
function! lsp#capabilities#has_linked_editing_range_provider(server_name) abort
57+
return s:has_provider(a:server_name, 'linkedEditingRangeProvider')
58+
endfunction
59+
5660
function! lsp#capabilities#has_code_action_provider(server_name) abort
5761
let l:capabilities = lsp#get_server_capabilities(a:server_name)
5862
if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider')
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
let s:TextEdit = vital#lsp#import('VS.LSP.TextEdit')
2+
let s:TextMark = vital#lsp#import('VS.Vim.Buffer.TextMark')
3+
4+
let s:TEXT_MARK_NAMESPACE = 'lsp#internal#linked_editing_range'
5+
6+
let s:state = {}
7+
let s:state['bufnr'] = -1
8+
let s:state['changenr'] = -1
9+
let s:state['changedtick'] = -1
10+
11+
function! lsp#internal#linked_editing_range#_enable() abort
12+
if !s:TextEdit.is_text_mark_preserved()
13+
return
14+
endif
15+
16+
if !g:lsp_linked_editing_range_enabled | return | endif
17+
let s:Dispose = lsp#callbag#merge(
18+
\ lsp#callbag#pipe(
19+
\ lsp#callbag#fromEvent(['InsertEnter']),
20+
\ lsp#callbag#flatMap({ -> s:request(v:false) }),
21+
\ lsp#callbag#subscribe({
22+
\ 'next': { x -> s:prepare(x) }
23+
\ })
24+
\ ),
25+
\ lsp#callbag#pipe(
26+
\ lsp#callbag#fromEvent(['InsertLeave']),
27+
\ lsp#callbag#subscribe({ -> s:clear() })
28+
\ ),
29+
\ lsp#callbag#pipe(
30+
\ lsp#callbag#fromEvent(['TextChanged', 'TextChangedI', 'TextChangedP']),
31+
\ lsp#callbag#delay(0),
32+
\ lsp#callbag#subscribe({ -> s:sync() })
33+
\ ),
34+
\ )
35+
endfunction
36+
37+
function! lsp#internal#linked_editing_range#_disable() abort
38+
if exists('s:Dispose')
39+
call s:clear()
40+
call s:Dispose()
41+
unlet s:Dispose
42+
endif
43+
endfunction
44+
45+
function! s:request(sync) abort
46+
let l:server = lsp#get_allowed_servers(&filetype)
47+
let l:server = filter(l:server, 'lsp#capabilities#has_linked_editing_range_provider(v:val)')
48+
let l:server = get(l:server, 0, v:null)
49+
if empty(l:server)
50+
return lsp#callbag#empty()
51+
endif
52+
53+
let l:X = lsp#callbag#pipe(
54+
\ lsp#request(l:server, {
55+
\ 'method': 'textDocument/linkedEditingRange',
56+
\ 'params': {
57+
\ 'textDocument': lsp#get_text_document_identifier(),
58+
\ 'position': lsp#get_position(),
59+
\ }
60+
\ }),
61+
\ )
62+
if a:sync
63+
return lsp#callbag#of(
64+
\ get(
65+
\ lsp#callbag#pipe(
66+
\ l:X,
67+
\ lsp#callbag#toList()
68+
\ ).wait({ 'sleep': 1, 'timeout': 200 }),
69+
\ 0,
70+
\ v:null
71+
\ )
72+
\ )
73+
endif
74+
return l:X
75+
endfunction
76+
77+
function! s:prepare(x) abort
78+
if empty(get(a:x['response']['result'], 'ranges', {}))
79+
return
80+
endif
81+
82+
call s:TextMark.set(bufnr('%'), s:TEXT_MARK_NAMESPACE, map(a:x['response']['result']['ranges'], { _, range -> {
83+
\ 'range': range,
84+
\ 'highlight': 'Underlined',
85+
\ } }))
86+
let s:state['bufnr'] = bufnr('%')
87+
let s:state['changenr'] = changenr()
88+
let s:state['changedtick'] = b:changedtick
89+
endfunction
90+
91+
function! s:clear() abort
92+
call s:TextMark.clear(bufnr('%'), s:TEXT_MARK_NAMESPACE)
93+
endfunction
94+
95+
function! s:sync() abort
96+
let l:bufnr = bufnr('%')
97+
if s:state['bufnr'] != l:bufnr
98+
return
99+
endif
100+
if s:state['changedtick'] == b:changedtick
101+
return
102+
endif
103+
if s:state['changenr'] > changenr()
104+
return
105+
endif
106+
107+
" get current mark and related marks.
108+
let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
109+
let l:current_mark = v:null
110+
let l:related_marks = []
111+
for l:mark in s:TextMark.get(l:bufnr, s:TEXT_MARK_NAMESPACE)
112+
if lsp#utils#range#_contains(l:mark['range'], l:position)
113+
let l:current_mark = l:mark
114+
else
115+
let l:related_marks += [l:mark]
116+
endif
117+
endfor
118+
119+
" ignore if current mark is not detected.
120+
if empty(l:current_mark)
121+
return
122+
endif
123+
124+
" apply new text for related marks.
125+
let l:new_text = lsp#utils#range#_get_text(l:bufnr, l:current_mark['range'])
126+
call lsp#utils#text_edit#apply_text_edits(l:bufnr, map(l:related_marks, { _, mark -> {
127+
\ 'range': mark['range'],
128+
\ 'newText': l:new_text
129+
\ } }))
130+
131+
" save state.
132+
let s:state['bufnr'] = l:bufnr
133+
let s:state['changenr'] = changenr()
134+
let s:state['changedtick'] = b:changedtick
135+
endfunction
136+

autoload/lsp/utils/range.vim

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,38 @@ function! lsp#utils#range#_get_current_line_range() abort
2929
return l:range
3030
endfunction
3131

32+
" Returns the range contains specified position or not.
33+
function! lsp#utils#range#_contains(range, position) abort
34+
if !(
35+
\ a:range['start']['line'] <= a:position['line'] && (
36+
\ a:range['start']['line'] == a:position['line'] &&
37+
\ a:range['start']['character'] <= a:position['character']
38+
\ )
39+
\ )
40+
return v:false
41+
endif
42+
if !(
43+
\ a:range['end']['line'] >= a:position['line'] && (
44+
\ a:range['end']['line'] == a:position['line'] &&
45+
\ a:range['end']['character'] >= a:position['character']
46+
\ )
47+
\ )
48+
return v:false
49+
endif
50+
return v:true
51+
endfunction
52+
53+
" Return the range of text for the specified expr.
54+
function! lsp#utils#range#_get_text(expr, range) abort
55+
let l:lines = []
56+
for l:line in range(a:range['start']['line'], a:range['end']['line'])
57+
let l:lines += getbufline(a:expr, l:line + 1)
58+
endfor
59+
let l:lines[-1] = strcharpart(l:lines[-1], 0, a:range['end']['character'])
60+
let l:lines[0] = strcharpart(l:lines[0], a:range['start']['character'], strchars(l:lines[0]))
61+
return join(l:lines, "\n")
62+
endfunction
63+
3264
" Convert a LSP range to one or more vim match positions.
3365
" If the range spans over multiple lines, break it down to multiple
3466
" positions, one for each line.

autoload/lsp/utils/text_edit.vim

Lines changed: 4 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
1-
function! lsp#utils#text_edit#apply_text_edits(uri, text_edits) abort
2-
let l:current_bufname = bufname('%')
3-
let l:target_bufname = lsp#utils#uri_to_path(a:uri)
4-
let l:cursor_position = lsp#get_position()
5-
6-
call s:_switch(l:target_bufname)
7-
for l:text_edit in s:_normalize(a:text_edits)
8-
call s:_apply(bufnr(l:target_bufname), l:text_edit, l:cursor_position)
9-
endfor
10-
call s:_switch(l:current_bufname)
1+
let s:TextEdit = vital#lsp#import('VS.LSP.TextEdit')
112

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

7+
178
" @summary Use this to convert textedit to vim list that is compatible with
189
" quickfix and locllist items
1910
" @param uri = DocumentUri
@@ -78,141 +69,3 @@ function! s:lsp_text_edit_item_to_vim(uri, text_edit, cache) abort
7869
\ }
7970
endfunction
8071

81-
"
82-
" _apply
83-
"
84-
function! s:_apply(bufnr, text_edit, cursor_position) abort
85-
" create before/after line.
86-
let l:start_line = getline(a:text_edit['range']['start']['line'] + 1)
87-
let l:end_line = getline(a:text_edit['range']['end']['line'] + 1)
88-
let l:before_line = strcharpart(l:start_line, 0, a:text_edit['range']['start']['character'])
89-
let l:after_line = strcharpart(l:end_line, a:text_edit['range']['end']['character'], strchars(l:end_line) - a:text_edit['range']['end']['character'])
90-
91-
" create new lines.
92-
let l:new_lines = lsp#utils#_split_by_eol(a:text_edit['newText'])
93-
let l:new_lines[0] = l:before_line . l:new_lines[0]
94-
let l:new_lines[-1] = l:new_lines[-1] . l:after_line
95-
96-
" save length.
97-
let l:new_lines_len = len(l:new_lines)
98-
let l:range_len = (a:text_edit['range']['end']['line'] - a:text_edit['range']['start']['line']) + 1
99-
100-
" fixendofline
101-
let l:buffer_length = len(getbufline(a:bufnr, '^', '$'))
102-
let l:should_fixendofline = lsp#utils#buffer#_get_fixendofline(a:bufnr)
103-
let l:should_fixendofline = l:should_fixendofline && l:new_lines[-1] ==# ''
104-
let l:should_fixendofline = l:should_fixendofline && l:buffer_length <= a:text_edit['range']['end']['line']
105-
let l:should_fixendofline = l:should_fixendofline && a:text_edit['range']['end']['character'] == 0
106-
if l:should_fixendofline
107-
call remove(l:new_lines, -1)
108-
endif
109-
110-
" fix cursor pos
111-
if a:text_edit['range']['end']['line'] < a:cursor_position['line']
112-
" fix cursor line
113-
let a:cursor_position['line'] += l:new_lines_len - l:range_len
114-
elseif a:text_edit['range']['end']['line'] == a:cursor_position['line'] && a:text_edit['range']['end']['character'] <= a:cursor_position['character']
115-
" fix cursor line and col
116-
let a:cursor_position['line'] += l:new_lines_len - l:range_len
117-
let l:end_character = strchars(l:new_lines[-1]) - strchars(l:after_line)
118-
let l:end_offset = a:cursor_position['character'] - a:text_edit['range']['end']['character']
119-
let a:cursor_position['character'] = l:end_character + l:end_offset
120-
endif
121-
122-
" append or delete lines.
123-
if l:new_lines_len > l:range_len
124-
call append(a:text_edit['range']['start']['line'], repeat([''], l:new_lines_len - l:range_len))
125-
elseif l:new_lines_len < l:range_len
126-
let l:offset = l:range_len - l:new_lines_len
127-
call s:delete(a:bufnr, a:text_edit['range']['start']['line'] + 1, a:text_edit['range']['start']['line'] + l:offset)
128-
endif
129-
130-
" set lines.
131-
call setline(a:text_edit['range']['start']['line'] + 1, l:new_lines)
132-
endfunction
133-
134-
"
135-
" _normalize
136-
"
137-
function! s:_normalize(text_edits) abort
138-
let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits]
139-
let l:text_edits = filter(copy(l:text_edits), { _, text_edit -> type(text_edit) == type({}) })
140-
let l:text_edits = s:_range(l:text_edits)
141-
let l:text_edits = sort(copy(l:text_edits), function('s:_compare', [], {}))
142-
let l:text_edits = s:_check(l:text_edits)
143-
return reverse(l:text_edits)
144-
endfunction
145-
146-
"
147-
" _range
148-
"
149-
function! s:_range(text_edits) abort
150-
for l:text_edit in a:text_edits
151-
if l:text_edit.range.start.line > l:text_edit.range.end.line || (
152-
\ l:text_edit.range.start.line == l:text_edit.range.end.line &&
153-
\ l:text_edit.range.start.character > l:text_edit.range.end.character
154-
\ )
155-
let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start }
156-
endif
157-
endfor
158-
return a:text_edits
159-
endfunction
160-
161-
"
162-
" _check
163-
"
164-
" LSP Spec says `multiple text edits can not overlap those ranges`.
165-
" This function check it. But does not throw error.
166-
"
167-
function! s:_check(text_edits) abort
168-
if len(a:text_edits) > 1
169-
let l:range = a:text_edits[0].range
170-
for l:text_edit in a:text_edits[1 : -1]
171-
if l:range.end.line > l:text_edit.range.start.line || (
172-
\ l:range.end.line == l:text_edit.range.start.line &&
173-
\ l:range.end.character > l:text_edit.range.start.character
174-
\ )
175-
call lsp#log('text_edit: range overlapped.')
176-
endif
177-
let l:range = l:text_edit.range
178-
endfor
179-
endif
180-
return a:text_edits
181-
endfunction
182-
183-
"
184-
" _compare
185-
"
186-
function! s:_compare(text_edit1, text_edit2) abort
187-
let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line
188-
if l:diff == 0
189-
return a:text_edit1.range.start.character - a:text_edit2.range.start.character
190-
endif
191-
return l:diff
192-
endfunction
193-
194-
"
195-
" _switch
196-
"
197-
function! s:_switch(path) abort
198-
if bufnr(a:path) >= 0
199-
execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path))
200-
else
201-
execute printf('keepalt keepjumps edit! %s', fnameescape(a:path))
202-
endif
203-
endfunction
204-
205-
"
206-
" delete
207-
"
208-
function! s:delete(bufnr, start, end) abort
209-
if exists('*deletebufline')
210-
call deletebufline(a:bufnr, a:start, a:end)
211-
else
212-
let l:foldenable = &foldenable
213-
setlocal nofoldenable
214-
execute printf('%s,%sdelete _', a:start, a:end)
215-
let &foldenable = l:foldenable
216-
endif
217-
endfunction
218-

autoload/vital/_lsp.vim

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
let s:_plugin_name = expand('<sfile>:t:r')
2+
3+
function! vital#{s:_plugin_name}#new() abort
4+
return vital#{s:_plugin_name[1:]}#new()
5+
endfunction
6+
7+
function! vital#{s:_plugin_name}#function(funcname) abort
8+
silent! return function(a:funcname)
9+
endfunction

0 commit comments

Comments
 (0)