Skip to content

Commit fe50576

Browse files
m-piliaprabirshrestha
authored andcommitted
Highlight references to the symbol under cursor (#363)
* Highlight references to the symbol under cursor Use the `textDocument/documentHighlight` method to retrieve references to the symbol under the cursor, and highlight them with matchaddpos(). Add the commands `LscPreviusReference` and `LscNextReference` to navigate between references within the buffer. Adaption from the implementation of "Highlight references" in vim-lsc: https://github.com/natebosch/vim-lsc/blob/5b587b0c/autoload/lsc/cursor.vim * Fixups + Lapsus: `while` statement where it should be `if`. + Fix alignment in docs. * Update LICENSE-THIRD-PARTY
1 parent 357a074 commit fe50576

File tree

7 files changed

+328
-40
lines changed

7 files changed

+328
-40
lines changed

LICENSE-THIRD-PARTY

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ their own copyright notices and license terms:
33

44
* vim-lsc - https://github.com/natebosch/vim-lsc
55
* autoload/lsc/diff.vim
6+
* autoload/lsc/cursor.vim
67
====================================================================
78

89
Copyright 2017 vim-lsc authors

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ Refer to docs on configuring omnifunc or [asyncomplete.vim](https://github.com/p
6363
|`:LspHover`| Show hover information |
6464
|`:LspImplementation` | Show implementation of interface |
6565
|`:LspNextError`| jump to next error |
66+
|`:LspNextReference`| jump to next reference to the symbol under cursor |
6667
|`:LspPreviousError`| jump to previous error |
68+
|`:LspPreviousReference`| jump to previous reference to the symbol under cursor |
6769
|`:LspReferences`| Find references |
6870
|`:LspRename`| Rename symbol |
6971
|`:LspStatus` | Show the status of the language server |
@@ -117,6 +119,22 @@ To your configuration.
117119

118120
Virtual text will use the same highlight groups as signs feature.
119121

122+
### Highlight references
123+
124+
References to the symbol under the cursor are highlighted by default. To
125+
disable, set in your configuration:
126+
127+
```viml
128+
let g:lsp_highlight_references_enabled = 0
129+
```
130+
131+
To change the style of the highlighting, you can set or link the `lspReference`
132+
highlight group, e.g.:
133+
134+
```viml
135+
highlight lspReference ctermfg=red guifg=red ctermbg=green guibg=green
136+
```
137+
120138
## Debugging
121139

122140
In order to enable file logging set `g:lsp_log_file`.

autoload/lsp.vim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ function! s:register_events() abort
157157
autocmd TextChangedP * call s:on_text_document_did_change()
158158
endif
159159
autocmd CursorMoved * call s:on_cursor_moved()
160+
autocmd BufWinEnter,BufWinLeave,InsertEnter * call lsp#ui#vim#references#clean_references()
161+
autocmd CursorMoved * call lsp#ui#vim#references#highlight(v:false)
160162
augroup END
161163
call s:on_text_document_did_open()
162164
endfunction

autoload/lsp/capabilities.vim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ function! lsp#capabilities#has_type_definition_provider(server_name) abort
6161
return s:has_bool_provider(a:server_name, 'typeDefinitionProvider')
6262
endfunction
6363

64+
function! lsp#capabilities#has_document_highlight_provider(server_name) abort
65+
return s:has_bool_provider(a:server_name, 'documentHighlightProvider')
66+
endfunction
67+
6468
" [supports_did_save (boolean), { 'includeText': boolean }]
6569
function! lsp#capabilities#get_text_document_save_registration_options(server_name) abort
6670
let l:capabilities = lsp#get_server_capabilities(a:server_name)

autoload/lsp/ui/vim/references.vim

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
" Global state
2+
let s:last_req_id = 0
3+
let s:pending = {}
4+
5+
" Highlight group for references
6+
if !hlexists('lspReference')
7+
highlight link lspReference CursorColumn
8+
endif
9+
10+
" Convert a LSP range to one or more vim match positions.
11+
" If the range spans over multiple lines, break it down to multiple
12+
" positions, one for each line.
13+
" Return a list of positions.
14+
function! s:range_to_position(range) abort
15+
let l:start = a:range['start']
16+
let l:end = a:range['end']
17+
let l:position = []
18+
19+
if l:end['line'] == l:start['line']
20+
let l:position = [[
21+
\ l:start['line'] + 1,
22+
\ l:start['character'] + 1,
23+
\ l:end['character'] - l:start['character']
24+
\ ]]
25+
else
26+
" First line
27+
let l:position = [[
28+
\ l:start['line'] + 1,
29+
\ l:start['character'] + 1,
30+
\ 999
31+
\ ]]
32+
33+
" Last line
34+
call add(l:position, [
35+
\ l:end['line'] + 1,
36+
\ 1,
37+
\ l:end['character']
38+
\ ])
39+
40+
" Lines in the middle
41+
let l:middle_lines = map(
42+
\ range(l:start['line'] + 2, end['line']),
43+
\ {_, l -> [l, 0, 999]}
44+
\ )
45+
46+
call extend(l:position, l:middle_lines)
47+
endif
48+
49+
return l:position
50+
endfunction
51+
52+
" Compare two positions
53+
function! s:compare_positions(p1, p2) abort
54+
let l:line_1 = a:p1[0]
55+
let l:line_2 = a:p2[0]
56+
if l:line_1 != l:line_2
57+
return l:line_1 > l:line_2 ? 1 : -1
58+
endif
59+
let l:col_1 = a:p1[1]
60+
let l:col_2 = a:p2[1]
61+
return l:col_1 - l:col_2
62+
endfunction
63+
64+
" If the cursor is over a reference, return its index in
65+
" the array. Otherwise, return -1.
66+
function! s:in_reference(reference_list) abort
67+
let l:line = line('.')
68+
let l:column = col('.')
69+
let l:index = 0
70+
for l:position in a:reference_list
71+
if l:line == l:position[0] &&
72+
\ l:column >= l:position[1] &&
73+
\ l:column < l:position[1] + l:position[2]
74+
return l:index
75+
endif
76+
let l:index += 1
77+
endfor
78+
return -1
79+
endfunction
80+
81+
" Handle response from server.
82+
function! s:handle_references(ctx, data) abort
83+
" Sanity checks
84+
if lsp#client#is_error(a:data['response']) ||
85+
\ !has_key(s:pending, a:ctx['filetype']) ||
86+
\ !s:pending[a:ctx['filetype']]
87+
return
88+
endif
89+
let s:pending[a:ctx['filetype']] = v:false
90+
91+
" More sanity checks
92+
if a:ctx['bufnr'] != bufnr('%') || a:ctx['last_req_id'] != s:last_req_id
93+
return
94+
endif
95+
96+
" Remove existing highlights from the buffer
97+
call lsp#ui#vim#references#clean_references()
98+
99+
" Get references from the response
100+
let l:reference_list = a:data['response']['result']
101+
if empty(l:reference_list)
102+
return
103+
endif
104+
105+
" Convert references to vim positions
106+
let l:position_list = []
107+
for l:reference in l:reference_list
108+
call extend(l:position_list, s:range_to_position(l:reference['range']))
109+
endfor
110+
call sort(l:position_list, function('s:compare_positions'))
111+
112+
" Ignore response if the cursor is not over a reference anymore
113+
if s:in_reference(l:position_list) == -1
114+
" If the cursor has moved: send another request
115+
if a:ctx['curpos'] != getcurpos()
116+
call lsp#ui#vim#references#highlight(v:true)
117+
endif
118+
return
119+
endif
120+
121+
" Store references
122+
let w:lsp_reference_positions = l:position_list
123+
let w:lsp_reference_matches = []
124+
125+
" Apply highlights to the buffer
126+
if g:lsp_highlight_references_enabled
127+
for l:position in l:position_list
128+
let l:match = matchaddpos('lspReference', [l:position], -5)
129+
call add(w:lsp_reference_matches, l:match)
130+
endfor
131+
endif
132+
endfunction
133+
134+
" Highlight references to the symbol under the cursor
135+
function! lsp#ui#vim#references#highlight(force_refresh) abort
136+
" No need to change the highlights if the cursor has not left
137+
" the currently highlighted symbol.
138+
if !a:force_refresh &&
139+
\ exists('w:lsp_reference_positions') &&
140+
\ s:in_reference(w:lsp_reference_positions) != -1
141+
return
142+
endif
143+
144+
" A request for this symbol has already been sent
145+
if has_key(s:pending, &filetype) && s:pending[&filetype]
146+
return
147+
endif
148+
149+
" Check if any server provides document highlight
150+
let l:capability = 'lsp#capabilities#has_document_highlight_provider(v:val)'
151+
let l:servers = filter(lsp#get_whitelisted_servers(), l:capability)
152+
153+
if len(l:servers) == 0
154+
return
155+
endif
156+
157+
" Send a request
158+
let s:pending[&filetype] = v:true
159+
let s:last_req_id += 1
160+
let l:ctx = {
161+
\ 'last_req_id': s:last_req_id,
162+
\ 'curpos': getcurpos(),
163+
\ 'bufnr': bufnr('%'),
164+
\ 'filetype': &filetype,
165+
\ }
166+
call lsp#send_request(l:servers[0], {
167+
\ 'method': 'textDocument/documentHighlight',
168+
\ 'params': {
169+
\ 'textDocument': lsp#get_text_document_identifier(),
170+
\ 'position': lsp#get_position(),
171+
\ },
172+
\ 'on_notification': function('s:handle_references', [l:ctx]),
173+
\ })
174+
endfunction
175+
176+
" Remove all reference highlights from the buffer
177+
function! lsp#ui#vim#references#clean_references() abort
178+
let s:pending[&filetype] = v:false
179+
if exists('w:lsp_reference_matches')
180+
for l:match in w:lsp_reference_matches
181+
silent! call matchdelete(l:match)
182+
endfor
183+
unlet w:lsp_reference_matches
184+
unlet w:lsp_reference_positions
185+
endif
186+
endfunction
187+
188+
" Cyclically move between references by `offset` occurrences.
189+
function! lsp#ui#vim#references#jump(offset) abort
190+
if !exists('w:lsp_reference_positions')
191+
echohl WarningMsg
192+
echom 'References not available'
193+
echohl None
194+
return
195+
endif
196+
197+
" Get index of reference under cursor
198+
let l:index = s:in_reference(w:lsp_reference_positions)
199+
if l:index < 0
200+
return
201+
endif
202+
203+
let l:n = len(w:lsp_reference_positions)
204+
let l:index += a:offset
205+
206+
" Show a message when reaching TOP/BOTTOM of the file
207+
if l:index < 0
208+
echohl WarningMsg
209+
echom 'search hit TOP, continuing at BOTTOM'
210+
echohl None
211+
elseif l:index >= len(w:lsp_reference_positions)
212+
echohl WarningMsg
213+
echom 'search hit BOTTOM, continuing at TOP'
214+
echohl None
215+
endif
216+
217+
" Wrap index
218+
if l:index < 0 || l:index >= len(w:lsp_reference_positions)
219+
let l:index = (l:index % l:n + l:n) % l:n
220+
endif
221+
222+
" Jump
223+
let l:target = w:lsp_reference_positions[l:index][0:1]
224+
silent exec 'normal! ' . l:target[0] . 'G' . l:target[1] . '|'
225+
endfunction

0 commit comments

Comments
 (0)