let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim') let s:use_nvim_highlight = lsp#utils#_has_nvim_buf_highlight() let s:textprop_cache = 'vim-lsp-semantic-cache' if s:use_nvim_highlight let s:namespace_id = nvim_create_namespace('vim-lsp-semantic') endif " Global functions {{{1 function! lsp#internal#semantic#is_enabled() abort return g:lsp_semantic_enabled && (s:use_vim_textprops || s:use_nvim_highlight) ? v:true : v:false endfunction function! lsp#internal#semantic#_enable() abort if !lsp#internal#semantic#is_enabled() | return | endif augroup lsp#internal#semantic autocmd! au User lsp_buffer_enabled call s:on_lsp_buffer_enabled() augroup END let l:events = [['User', 'lsp_buffer_enabled'], 'TextChanged', 'TextChangedI'] if exists('##TextChangedP') call add(l:events, 'TextChangedP') endif let s:Dispose = lsp#callbag#pipe( \ lsp#callbag#fromEvent(l:events), \ lsp#callbag#filter({_->lsp#internal#semantic#is_enabled()}), \ lsp#callbag#debounceTime(g:lsp_semantic_delay), \ lsp#callbag#filter({_->index(['help', 'terminal', 'prompt', 'popup'], getbufvar(bufnr('%'), '&buftype')) ==# -1}), \ lsp#callbag#filter({_->!lsp#utils#is_large_window(win_getid())}), \ lsp#callbag#switchMap({_-> \ lsp#callbag#pipe( \ s:semantic_request(), \ lsp#callbag#materialize(), \ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}), \ lsp#callbag#map({x->x['value']}) \ ) \ }), \ lsp#callbag#subscribe({x->s:handle_semantic_request(x)}) \ ) endfunction function! lsp#internal#semantic#_disable() abort augroup lsp#internal#semantic autocmd! augroup END if exists('s:Dispose') call s:Dispose() unlet s:Dispose endif endfunction function! lsp#internal#semantic#get_legend(server) abort if !lsp#capabilities#has_semantic_tokens(a:server) return {'tokenTypes': [], 'tokenModifiers': []} endif let l:capabilities = lsp#get_server_capabilities(a:server) return l:capabilities['semanticTokensProvider']['legend'] endfunction function! lsp#internal#semantic#get_token_types() abort let l:capability = 'lsp#capabilities#has_semantic_tokens(v:val)' let l:servers = filter(lsp#get_allowed_servers(), l:capability) if empty(l:servers) return [] endif let l:legend = lsp#internal#semantic#get_legend(l:servers[0]) let l:token_types = l:legend['tokenTypes'] call map(l:token_types, {_, type -> toupper(type[0]) . type[1:]}) return l:token_types endfunction function! lsp#internal#semantic#get_token_modifiers() abort let l:capability = 'lsp#capabilities#has_semantic_tokens(v:val)' let l:servers = filter(lsp#get_allowed_servers(), l:capability) if empty(l:servers) return [] endif let l:legend = lsp#internal#semantic#get_legend(l:servers[0]) let l:token_modifiers = l:legend['tokenModifiers'] call map(l:token_modifiers, {_, modifier -> toupper(modifier[0]) . modifier[1:]}) return l:token_modifiers endfunction function! s:on_lsp_buffer_enabled() abort augroup lsp#internal#semantic if !exists('#BufUnload#') execute 'au BufUnload call setbufvar(' . bufnr() . ', ''lsp_semantic_previous_result_id'', '''')' endif augroup END endfunction function! s:supports_full_semantic_request(server) abort if !lsp#capabilities#has_semantic_tokens(a:server) return v:false endif let l:capabilities = lsp#get_server_capabilities(a:server)['semanticTokensProvider'] if !has_key(l:capabilities, 'full') return v:false endif if type(l:capabilities['full']) ==# v:t_dict return v:true endif return l:capabilities['full'] endfunction function! s:supports_delta_semantic_request(server) abort if !lsp#capabilities#has_semantic_tokens(a:server) return v:false endif let l:capabilities = lsp#get_server_capabilities(a:server)['semanticTokensProvider'] if !has_key(l:capabilities, 'full') return v:false endif if type(l:capabilities['full']) !=# v:t_dict return v:false endif if !has_key(l:capabilities['full'], 'delta') return v:false endif return l:capabilities['full']['delta'] endfunction function! s:get_server() abort let l:capability = 's:supports_delta_semantic_request(v:val)' let l:servers = filter(lsp#get_allowed_servers(), l:capability) if empty(l:servers) let l:capability = 's:supports_full_semantic_request(v:val)' let l:servers = filter(lsp#get_allowed_servers(), l:capability) endif if empty(l:servers) return '' endif return l:servers[0] endfunction function! s:semantic_request() abort let l:server = s:get_server() if l:server ==# '' return lsp#callbag#empty() endif if (s:supports_delta_semantic_request(l:server) \ && getbufvar(bufnr(), 'lsp_semantic_previous_result_id') !=# '') return s:delta_semantic_request(l:server) else return s:full_semantic_request(l:server) endif endfunction function! s:full_semantic_request(server) abort return lsp#request(a:server, { \ 'method': 'textDocument/semanticTokens/full', \ 'params': { \ 'textDocument': lsp#get_text_document_identifier() \ }}) endfunction function! s:delta_semantic_request(server) abort return lsp#request(a:server, { \ 'method': 'textDocument/semanticTokens/full/delta', \ 'params': { \ 'textDocument': lsp#get_text_document_identifier(), \ 'previousResultId': getbufvar(bufname(), 'lsp_semantic_previous_result_id', 0) \ }}) endfunction " Highlight helper functions {{{1 function! s:handle_semantic_request(data) abort if lsp#client#is_error(a:data['response']) call lsp#log('Skipping semantic highlight: response is invalid') return endif let l:server = a:data['server_name'] let l:uri = a:data['request']['params']['textDocument']['uri'] let l:path = lsp#utils#uri_to_path(l:uri) let l:bufnr = bufnr(l:path) " Skip if the buffer doesn't exist. This might happen when a buffer is " opened and quickly deleted. if !bufloaded(l:bufnr) | return | endif if has_key(a:data['response']['result'], 'data') call s:handle_semantic_tokens_response(l:server, l:bufnr, a:data['response']['result']) elseif has_key(a:data['response']['result'], 'edits') call s:handle_semantic_tokens_delta_response(l:server, l:bufnr, a:data['response']['result']) else " Don't update previous result ID if we could not update local copy call lsp#log('SemanticHighlight: unsupported semanticTokens method') return endif if has_key(a:data['response']['result'], 'resultId') call setbufvar(l:bufnr, 'lsp_semantic_previous_result_id', a:data['response']['result']['resultId']) endif endfunction function! s:handle_semantic_tokens_response(server, buf, result) abort let l:highlights = {} let l:legend = lsp#internal#semantic#get_legend(a:server) for l:token in s:decode_tokens(a:result['data']) let [l:key, l:value] = s:add_highlight(a:server, l:legend, a:buf, l:token) let l:highlights[l:key] = get(l:highlights, l:key, []) + l:value endfor call s:apply_highlights(a:server, a:buf, l:highlights) call setbufvar(a:buf, 'lsp_semantic_local_data', a:result['data']) endfunction function! s:startpos_compare(edit1, edit2) abort return a:edit1[0] == a:edit2[0] ? 0 : a:edit1[0] > a:edit2[0] ? -1 : 1 endfunction function! s:handle_semantic_tokens_delta_response(server, buf, result) abort " The locations given in the edit are all referenced to the state before " any are applied and sorting is not required from the server, " therefore the edits must be sorted before applying. let l:edits = a:result['edits'] call sort(l:edits, function('s:startpos_compare')) let l:localdata = getbufvar(a:buf, 'lsp_semantic_local_data') for l:edit in l:edits let l:insertdata = get(l:edit, 'data', []) let l:localdata = l:localdata[:l:edit['start'] - 1] \ + l:insertdata \ + l:localdata[l:edit['start'] + l:edit['deleteCount']:] endfor call setbufvar(a:buf, 'lsp_semantic_local_data', l:localdata) let l:highlights = {} let l:legend = lsp#internal#semantic#get_legend(a:server) for l:token in s:decode_tokens(l:localdata) let [l:key, l:value] = s:add_highlight(a:server, l:legend, a:buf, l:token) let l:highlights[l:key] = get(l:highlights, l:key, []) + l:value endfor call s:apply_highlights(a:server, a:buf, l:highlights) endfunction function! s:decode_tokens(data) abort let l:tokens = [] let l:i = 0 let l:line = 0 let l:char = 0 while l:i < len(a:data) let l:line = l:line + a:data[l:i] if a:data[l:i] > 0 let l:char = 0 endif let l:char = l:char + a:data[l:i + 1] call add(l:tokens, { \ 'pos': {'line': l:line, 'character': l:char}, \ 'length': a:data[l:i + 2], \ 'token_idx': a:data[l:i + 3], \ 'token_modifiers': a:data[l:i + 4] \ }) let l:i = l:i + 5 endwhile return l:tokens endfunction function! s:clear_highlights(server, buf) abort if s:use_vim_textprops let l:BeginsWith = {str, prefix -> str[0:len(prefix) - 1] ==# prefix} let l:IsSemanticTextprop = {_, textprop -> l:BeginsWith(textprop, s:textprop_type_prefix)} let l:textprop_types = prop_type_list() call filter(l:textprop_types, l:IsSemanticTextprop) for l:textprop_type in l:textprop_types silent! call prop_remove({'type': l:textprop_type, 'bufnr': a:buf, 'all': v:true}) endfor elseif s:use_nvim_highlight call nvim_buf_clear_namespace(a:buf, s:namespace_id, 0, line('$')) endif endfunction function! s:add_highlight(server, legend, buf, token) abort let l:startpos = lsp#utils#position#lsp_to_vim(a:buf, a:token['pos']) let l:endpos = a:token['pos'] let l:endpos['character'] = l:endpos['character'] + a:token['length'] let l:endpos = lsp#utils#position#lsp_to_vim(a:buf, l:endpos) if s:use_vim_textprops let l:textprop_name = s:get_textprop_type(a:server, a:legend, a:token['token_idx'], a:token['token_modifiers']) return [l:textprop_name, [[l:startpos[0], l:startpos[1], l:endpos[0], l:endpos[1]]]] elseif s:use_nvim_highlight let l:char = a:token['pos']['character'] let l:hl_name = s:get_hl_group(a:server, a:legend, a:token['token_idx'], a:token['token_modifiers']) return [l:hl_name, [[l:startpos[0] - 1, l:startpos[1] - 1, l:endpos[1] - 1]]] endif endfunction function! s:apply_highlights(server, buf, highlights) abort call s:clear_highlights(a:server, a:buf) if s:use_vim_textprops for [l:type, l:prop_list] in items(a:highlights) call prop_add_list({'type': l:type, 'bufnr': a:buf}, l:prop_list) endfor elseif s:use_nvim_highlight call lsp#log(a:highlights) for [l:hl_name, l:instances] in items(a:highlights) for l:instance in l:instances let [l:line, l:startcol, l:endcol] = l:instance try call nvim_buf_add_highlight(a:buf, s:namespace_id, l:hl_name, l:line, l:startcol, l:endcol) catch call lsp#log('SemanticHighlight: error while adding ' . l:hl_name . ' highlight on line ' . l:line) endtry endfor endfor end endfunction let s:hl_group_prefix = 'LspSemantic' let s:default_highlight_groups = { \ s:hl_group_prefix . 'Type': 'Type', \ s:hl_group_prefix . 'Class': 'Type', \ s:hl_group_prefix . 'Enum': 'Type', \ s:hl_group_prefix . 'Interface': 'TypeDef', \ s:hl_group_prefix . 'Struct': 'Type', \ s:hl_group_prefix . 'TypeParameter': 'Type', \ s:hl_group_prefix . 'Parameter': 'Identifier', \ s:hl_group_prefix . 'Variable': 'Identifier', \ s:hl_group_prefix . 'Property': 'Identifier', \ s:hl_group_prefix . 'EnumMember': 'Constant', \ s:hl_group_prefix . 'Event': 'Identifier', \ s:hl_group_prefix . 'Function': 'Function', \ s:hl_group_prefix . 'Method': 'Function', \ s:hl_group_prefix . 'Macro': 'Macro', \ s:hl_group_prefix . 'Keyword': 'Keyword', \ s:hl_group_prefix . 'Modifier': 'Type', \ s:hl_group_prefix . 'Comment': 'Comment', \ s:hl_group_prefix . 'String': 'String', \ s:hl_group_prefix . 'Number': 'Number', \ s:hl_group_prefix . 'Regexp': 'String', \ s:hl_group_prefix . 'Operator': 'Operator', \ s:hl_group_prefix . 'Decorator': 'Macro' \ } function! s:get_hl_group(server, legend, token_idx, token_modifiers) abort " get highlight group name let l:Capitalise = {str -> toupper(str[0]) . str[1:]} let l:token_name = l:Capitalise(a:legend['tokenTypes'][a:token_idx]) let l:token_modifiers = [] for l:modifier_idx in range(len(a:legend['tokenModifiers'])) " float2nr(pow(2, a)) is 1 << a if and(a:token_modifiers, float2nr(pow(2, l:modifier_idx))) let l:modifier_name = a:legend['tokenModifiers'][l:modifier_idx] call add(l:token_modifiers, l:Capitalise(l:modifier_name)) endif endfor call sort(l:token_modifiers) let l:hl_group = s:hl_group_prefix \ . reduce(l:token_modifiers, {acc, val -> acc . val}, '') \ . l:token_name " create the highlight group if it does not already exist if !hlexists(l:hl_group) if has_key(s:default_highlight_groups, l:hl_group) exec 'highlight link' l:hl_group s:default_highlight_groups[l:hl_group] else if a:token_modifiers != 0 let l:base_hl_group = s:get_hl_group(a:server, a:legend, a:token_idx, 0) exec 'highlight link' l:hl_group l:base_hl_group else exec 'highlight link' l:hl_group 'Normal' endif endif endif return l:hl_group endfunction let s:textprop_type_prefix = 'vim-lsp-semantic-' function! s:get_textprop_type(server, legend, token_idx, token_modifiers) abort " get textprop type name let l:textprop_type = s:textprop_type_prefix . a:server . '-' . a:token_idx . '-' . a:token_modifiers " create the textprop type if it does not already exist if prop_type_get(l:textprop_type) ==# {} let l:hl_group = s:get_hl_group(a:server, a:legend, a:token_idx, a:token_modifiers) silent! call prop_type_add(l:textprop_type, { \ 'highlight': l:hl_group, \ 'combine': v:true, \ 'priority': lsp#internal#textprop#priority('semantic')}) endif return l:textprop_type endfunction " vim: fdm=marker