mirror of
https://github.com/prabirshrestha/vim-lsp.git
synced 2025-12-14 20:35:59 +01:00
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:
@@ -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'
|
||||
|
||||
@@ -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 }]
|
||||
|
||||
410
autoload/lsp/internal/semantic.vim
Normal file
410
autoload/lsp/internal/semantic.vim
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user