|
| 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