Semantic Tokens Support (#1275)

* initial naive implementation of semanticTokens

* fix race condition when starting new lsp server

* fix error when closing and re-opening a buffer

* support nvim highlighting

* provide tokenTypes and tokenModifiers client capabilities

* move semantic.vim to autoload/lsp/internal

* refactor semantic tokens to use a callbag

* remove scope tree functions

* refactor decoding of semanticTokens responses

* improve semantic tokens nvim support

* add support for multi-byte characters

* update semantic tokens on TextChangedP

* don't apply semantic highlighting to large files

* replace semantic_highlight server key with custom highlight groups

* rename g:lsp_large_buffer_threshold to g:lsp_max_buffer_size

* fix typo: correct LpEvents to LspEvents

* update semantic highlighting documentation

* change has('popupwin') to exists('autoload/lsp/internal/semantic.vimTextChangedP')

* correct lsp_max_buffer_size usage

* add LspSemanticHighlightGroups helper command

* use explicit scope and reorganise decode_tokens

* use robust comparison + single quoted strings in s:init_highlight

* update semantic highlighting on VimEnter

* support semanticTokens/full/delta requests

* request semanticTokens/full on BufNewFile or BufReadPost

* perform full semantic updates on lsp_buffer_enabled

* support SemanticTokensDelta response to textDocument/semanticTokens/full/delta request

* decide between full or delta semanticTokens request based on state

* use prop_add_list to add semantic tokens

* sort semanticTokens edits before applying

* update semantic.vim to conform to google style guide

* update default semantic highlight groups to better match usage

* fix parsing of SemanticTokensOptions

* mention g:lsp_semantic_enabled in vim-lsp-semantic docs

* document g:lsp_semantic_delay

* add support for semantic token modifiers

* sort semantic tokens modifiers alphabetically

* correctly report semantic tokens capabilities
This commit is contained in:
frankplow
2022-07-01 04:11:47 +01:00
committed by GitHub
parent da80b01831
commit 68c018eb1a
7 changed files with 528 additions and 341 deletions

View File

@@ -64,6 +64,7 @@ function! lsp#enable() abort
call lsp#internal#document_highlight#_enable()
call lsp#internal#diagnostics#_enable()
call lsp#internal#document_code_action#signs#_enable()
call lsp#internal#semantic#_enable()
call lsp#internal#show_message_request#_enable()
call lsp#internal#show_message#_enable()
call lsp#internal#work_done_progress#_enable()
@@ -80,6 +81,7 @@ function! lsp#disable() abort
call lsp#internal#document_highlight#_disable()
call lsp#internal#diagnostics#_disable()
call lsp#internal#document_code_action#signs#_disable()
call lsp#internal#semantic#_disable()
call lsp#internal#show_message_request#_disable()
call lsp#internal#show_message#_disable()
call lsp#internal#work_done_progress#_disable()
@@ -548,8 +550,27 @@ function! lsp#default_get_supported_capabilities(server_info) abort
\ 'references': {
\ 'dynamicRegistration': v:false,
\ },
\ 'semanticHighlightingCapabilities': {
\ 'semanticHighlighting': lsp#ui#vim#semantic#is_enabled()
\ 'semanticTokens': {
\ 'dynamicRegistration': v:false,
\ 'requests': {
\ 'range': v:false,
\ 'full': lsp#internal#semantic#is_enabled()
\ ? {'delta': v:true}
\ : v:false
\
\ },
\ 'tokenTypes': [
\ 'type', 'class', 'enum', 'interface', 'struct',
\ 'typeParameter', 'parameter', 'variable', 'property',
\ 'enumMember', 'event', 'function', 'method', 'macro',
\ 'keyword', 'modifier', 'comment', 'string', 'number',
\ 'regexp', 'operator'
\ ],
\ 'tokenModifiers': [],
\ 'formats': ['relative'],
\ 'overlappingTokenSupport': v:false,
\ 'multilineTokenSupport': v:false,
\ 'serverCancelSupport': v:false
\ },
\ 'publishDiagnostics': {
\ 'relatedInformation': v:true,
@@ -846,14 +867,7 @@ function! s:on_notification(server_name, id, data, event) abort
endif
call lsp#stream(1, l:stream_data) " notify stream before callbacks
if lsp#client#is_server_instantiated_notification(a:data)
if has_key(l:response, 'method')
if l:response['method'] ==# 'textDocument/semanticHighlighting'
call lsp#ui#vim#semantic#handle_semantic(a:server_name, a:data)
endif
" NOTE: this is legacy code, use stream instead of handling notifications here
endif
else
if !lsp#client#is_server_instantiated_notification(a:data)
let l:request = a:data['request']
let l:method = l:request['method']
if l:method ==# 'initialize'

View File

@@ -109,26 +109,8 @@ function! lsp#capabilities#has_call_hierarchy_provider(server_name) abort
return s:has_provider(a:server_name, 'callHierarchyProvider')
endfunction
function! lsp#capabilities#has_semantic_highlight(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if empty(l:capabilities) || type(l:capabilities) != type({}) || !has_key(l:capabilities, 'semanticHighlighting')
return 0
endif
let l:semantic_hl = l:capabilities['semanticHighlighting']
if type(l:semantic_hl) != type({}) || !has_key(l:semantic_hl, 'scopes')
return 0
endif
let l:scopes = l:semantic_hl['scopes']
if type(l:scopes) != type([]) || empty(l:scopes)
return 0
endif
return 1
function! lsp#capabilities#has_semantic_tokens(server_name) abort
return s:has_provider(a:server_name, 'semanticTokensProvider')
endfunction
" [supports_did_save (boolean), { 'includeText': boolean }]

View File

@@ -0,0 +1,410 @@
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({_->getbufvar(bufnr('%'), '&buftype') !~# '^(help\|terminal\|prompt\|popup)$'}),
\ 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 = {}
for l:token in s:decode_tokens(a:result['data'])
let l:highlights = s:add_highlight(l:highlights, a:server, a:buf, l:token)
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 = {}
for l:token in s:decode_tokens(l:localdata)
let l:highlights = s:add_highlight(l:highlights, a:server, a:buf, l:token)
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:IsSemanticTextprop = {textprop -> textprop['type'] =~ '^' . s:textprop_type_prefix . '.*'}
let l:textprop_types = prop_list(1, {'bufnr': a:buf, 'end_lnum': -1})
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}, 1, line('$'))
endfor
elseif s:use_nvim_highlight
call nvim_buf_clear_namespace(a:buf, s:namespace_id, 0, line('$'))
endif
endfunction
function! s:add_highlight(highlights, server, buf, token) abort
let l:legend = lsp#internal#semantic#get_legend(a:server)
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:token['token_idx'], a:token['token_modifiers'])
let a:highlights[l:textprop_name] = get(a:highlights, 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:token['token_idx'], a:token['token_modifiers'])
let a:highlights[l:hl_name] = get(a:highlights, l:hl_name, [])
\ + [[l:startpos[0] - 1, l:startpos[1] - 1, l:endpos[1] - 1]]
endif
return a:highlights
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 . 'Events': 'Identifier',
\ s:hl_group_prefix . 'Function': 'Function',
\ s:hl_group_prefix . 'Method': 'Function',
\ 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'
\ }
function! s:get_hl_group(server, token_idx, token_modifiers) abort
" get highlight group name
let l:legend = lsp#internal#semantic#get_legend(a:server)
let l:Capitalise = {str -> toupper(str[0]) . str[1:]}
let l:token_name = l:Capitalise(l:legend['tokenTypes'][a:token_idx])
let l:token_modifiers = []
for l:modifier_idx in range(len(l:legend['tokenModifiers']))
" float2nr(pow(2, a)) is 1 << a
if and(a:token_modifiers, float2nr(pow(2, l:modifier_idx)))
let l:modifier_name = l: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: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, 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: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

View File

@@ -1,234 +0,0 @@
let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim')
let s:use_nvim_highlight = exists('*nvim_buf_add_highlight') && has('nvim')
let s:textprop_cache = 'vim-lsp-semantic-cache'
if s:use_nvim_highlight
let s:namespace_id = nvim_create_namespace('vim-lsp-semantic')
endif
if !hlexists('LspUnknownScope')
highlight LspUnknownScope gui=NONE cterm=NONE guifg=NONE ctermfg=NONE guibg=NONE ctermbg=NONE
endif
" Global functions {{{1
function! lsp#ui#vim#semantic#is_enabled() abort
return g:lsp_semantic_enabled && (s:use_vim_textprops || s:use_nvim_highlight) ? v:true : v:false
endfunction
function! lsp#ui#vim#semantic#get_scopes(server) abort
if !lsp#capabilities#has_semantic_highlight(a:server)
return []
endif
let l:capabilities = lsp#get_server_capabilities(a:server)
return l:capabilities['semanticHighlighting']['scopes']
endfunction
function! lsp#ui#vim#semantic#handle_semantic(server, data) abort
if !g:lsp_semantic_enabled | return | endif
if lsp#client#is_error(a:data['response'])
call lsp#log('Skipping semantic highlight: response is invalid')
return
endif
let l:uri = a:data['response']['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
call s:init_highlight(a:server, l:bufnr)
for l:info in a:data['response']['params']['lines']
let l:linenr = l:info['line']
let l:tokens = has_key(l:info, 'tokens') ? l:info['tokens'] : ''
call s:add_highlight(a:server, l:bufnr, l:linenr, l:tokens)
endfor
endfunction
" Highlight helper functions {{{1
function! s:init_highlight(server, buf) abort
if !empty(getbufvar(a:buf, 'lsp_did_semantic_setup'))
return
endif
if s:use_vim_textprops
let l:scopes = lsp#ui#vim#semantic#get_scopes(a:server)
for l:scope_idx in range(len(l:scopes))
let l:scope = l:scopes[l:scope_idx]
let l:hl = s:get_hl_name(a:server, l:scope)
silent! call prop_type_add(s:get_textprop_name(a:server, l:scope_idx), {'bufnr': a:buf, 'highlight': l:hl, 'combine': v:true, 'priority': lsp#internal#textprop#priority('semantic')})
endfor
silent! call prop_type_add(s:textprop_cache, {'bufnr': a:buf, 'priority': lsp#internal#textprop#priority('semantic')})
endif
call setbufvar(a:buf, 'lsp_did_semantic_setup', 1)
endfunction
function! s:hash(str) abort
let l:hash = 1
for l:char in split(a:str, '\zs')
let l:hash = (l:hash * 31 + char2nr(l:char)) % 2147483647
endfor
return l:hash
endfunction
function! s:add_highlight(server, buf, line, tokens) abort
" Return quickly if the tokens for this line are already set correctly,
" according to the cached tokens.
" This only works for Vim at the moment, for Neovim, we need extended
" marks.
if s:use_vim_textprops
let l:props = filter(prop_list(a:line + 1, {'bufnr': a:buf}), {idx, prop -> prop['type'] ==# s:textprop_cache})
let l:hash = s:hash(a:tokens)
if !empty(l:props) && l:props[0]['id'] == l:hash
" No changes for this line, so just return.
return
endif
endif
let l:scopes = lsp#ui#vim#semantic#get_scopes(a:server)
let l:highlights = s:tokens_to_hl_info(a:tokens)
if s:use_vim_textprops
" Clear text properties from the previous run
for l:scope_idx in range(len(l:scopes))
call prop_remove({'bufnr': a:buf, 'type': s:get_textprop_name(a:server, l:scope_idx), 'all': v:true}, a:line + 1)
endfor
" Clear cache from previous run
call prop_remove({'bufnr': a:buf, 'type': s:textprop_cache, 'all': v:true}, a:line + 1)
" Add textprop for cache
call prop_add(a:line + 1, 1, {'bufnr': a:buf, 'type': s:textprop_cache, 'id': l:hash})
for l:highlight in l:highlights
try
call prop_add(a:line + 1, l:highlight['char'] + 1, { 'length': l:highlight['length'], 'bufnr': a:buf, 'type': s:get_textprop_name(a:server, l:highlight['scope'])})
catch
call lsp#log('SemanticHighlight: error while adding prop on line ' . (a:line + 1), v:exception)
endtry
endfor
elseif s:use_nvim_highlight
" Clear text properties from the previous run
call nvim_buf_clear_namespace(a:buf, s:namespace_id, a:line, a:line + 1)
for l:highlight in l:highlights
call nvim_buf_add_highlight(a:buf, s:namespace_id, s:get_hl_name(a:server, l:scopes[l:highlight['scope']]), a:line, l:highlight['char'], l:highlight['char'] + l:highlight['length'])
endfor
endif
endfunction
function! s:get_hl_name(server, scope) abort
let l:hl = 'LspUnknownScope'
" Iterate over scopes in the order most general to most specific,
" returning the last scope encountered. This is accomplished by a try
" catch which ensures we always return the last scope even if an error is
" encountered midway.
try
let l:info = lsp#get_server_info(a:server)
let l:hl = l:info['semantic_highlight']
let l:i = 0
while (l:i < len(a:scope)) && has_key(l:hl, a:scope[l:i])
let l:hl = l:hl[a:scope[l:i]]
let l:i += 1
endwhile
catch
endtry
return type(l:hl) == type('') ? l:hl : 'LspUnknownScope'
endfunction
function! s:get_textprop_name(server, scope_index) abort
return 'vim-lsp-semantic-' . a:server . '-' . a:scope_index
endfunction
" Response parsing functions {{{1
" Converts a list of bytes (MSB first) to a Number.
function! s:octets_to_number(octets) abort
let l:ret = 0
for l:octet in a:octets
let l:ret *= 256
let l:ret += l:octet
endfor
return l:ret
endfunction
function! s:tokens_to_hl_info(token) abort
let l:ret = []
let l:octets = lsp#utils#base64_decode(a:token)
for l:i in range(0, len(l:octets) - 1, 8)
let l:char = s:octets_to_number(l:octets[l:i : l:i+3])
let l:length = s:octets_to_number(l:octets[l:i+4 : l:i+5])
let l:scope = s:octets_to_number(l:octets[l:i+6 : l:i+7])
call add(l:ret, { 'char': l:char, 'length': l:length, 'scope': l:scope })
endfor
return l:ret
endfunction
" Display scope tree {{{1
function! lsp#ui#vim#semantic#display_scope_tree(...) abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_semantic_highlight(v:val)')
if len(l:servers) == 0
call lsp#utils#error('Semantic highlighting not supported for ' . &filetype)
return
endif
let l:server = l:servers[0]
let l:info = lsp#get_server_info(l:server)
let l:hl_mapping = get(l:info, 'semantic_highlight', {})
let l:scopes = copy(lsp#ui#vim#semantic#get_scopes(l:server))
" Convert scope array to tree
let l:tree = {}
for l:scope in l:scopes
let l:cur = l:tree
for l:scope_part in l:scope
if !has_key(l:cur, l:scope_part)
let l:cur[l:scope_part] = {}
endif
let l:cur = l:cur[l:scope_part]
endfor
endfor
call s:display_tree(l:hl_mapping, l:tree, 0, a:0 > 0 ? a:1 - 1 : 20)
endfunction
function! s:display_tree(hl_tree, tree, indent, maxindent) abort
for [l:item, l:rest] in sort(items(a:tree))
if has_key(a:hl_tree, l:item) && type(a:hl_tree[l:item]) == type('')
execute 'echohl ' . a:hl_tree[l:item]
endif
echo repeat(' ', 4 * a:indent) . l:item
echohl None
if a:indent < a:maxindent
let l:new_hl_info = get(a:hl_tree, l:item, {})
if type(l:new_hl_info) != type({})
let l:new_hl_info = {}
endif
call s:display_tree(l:new_hl_info, l:rest, a:indent + 1, a:maxindent)
endif
endfor
endfunction
" vim: fdm=marker

View File

@@ -13,7 +13,7 @@ function! lsp#utils#_has_signs() abort
return s:has_signs
endfunction
let s:has_nvim_buf_highlight = exists('*nvim_buf_add_highlight')
let s:has_nvim_buf_highlight = exists('*nvim_buf_add_highlight') && has('nvim')
function! lsp#utils#_has_nvim_buf_highlight() abort
return s:has_nvim_buf_highlight
endfunction
@@ -446,6 +446,11 @@ function! lsp#utils#parse_command_options(params) abort
return l:result
endfunction
function! lsp#utils#is_large_window(winid) abort
let l:buffer_size = line2byte(line('$', a:winid))
return g:lsp_max_buffer_size >= 0 && l:buffer_size >= g:lsp_max_buffer_size
endfunction
" polyfill for the neovim wait function
if exists('*wait')
function! lsp#utils#_wait(timeout, condition, ...) abort