+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#<buffer>')
+ execute 'au BufUnload <buffer> 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