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

View File

@@ -56,6 +56,7 @@ CONTENTS *vim-lsp-contents*
g:lsp_tree_incoming_prefix |g:lsp_tree_incoming_prefix|
g:lsp_format_sync_timeout |g:lsp_format_sync_timeout|
g:lsp_use_event_queue |g:lsp_use_event_queue|
g:lsp_max_buffer_size |g:lsp_max_buffer_size|
g:lsp_document_highlight_enabled |g:lsp_document_highlight_enabled|
g:lsp_document_highlight_delay |g:lsp_document_highlight_delay|
g:lsp_get_supported_capabilities |g:lsp_get_supported_capabilities|
@@ -70,6 +71,7 @@ CONTENTS *vim-lsp-contents*
g:lsp_log_file |g:lsp_log_file|
g:lsp_log_verbose |g:lsp_log_verbose|
g:lsp_semantic_enabled |g:lsp_semantic_enabled|
g:lsp_semantic_delay |g:lsp_semantic_delay|
g:lsp_text_document_did_save_delay |g:lsp_text_document_did_save_delay|
g:lsp_snippet_expand |g:lsp_snippet_expand|
g:lsp_completion_resolve_timeout |g:lsp_completion_resolve_timeout|
@@ -127,7 +129,7 @@ CONTENTS *vim-lsp-contents*
LspImplementation |:LspImplementation|
LspReferences |:LspReferences|
LspRename |:LspRename|
LspSemanticScopes |:LspSemanticScopes|
LspSemanticHighlightGroups |:LspSemanticHighlightGroups|
LspTypeDefinition |:LspTypeDefinition|
LspTypeHierarchy |:LspTypeHierarchy|
LspWorkspaceSymbol |:LspWorkspaceSymbol|
@@ -747,6 +749,22 @@ g:lsp_use_event_queue *g:lsp_use_event_queue*
let g:lsp_use_event_queue = 1
let g:lsp_use_event_queue = 0
g:lsp_max_buffer_size *g:lsp_max_buffer_size*
Type: |Number|
Default: `5000000`
To improve performance, if a buffer is larger than
`g:lsp_max_buffer_size` (measured in bytes), the following features
are disabled:
* Semantic highlighting
This functionality can be disabled by setting `g:lsp_max_buffer_size`
to a negative value.
Example: >
let g:max_buffer_size = 10000000
let g:max_buffer_size = -1
g:lsp_document_highlight_enabled *g:lsp_document_highlight_enabled*
Type: |Number|
Default: `1` for neovim or vim with patch-8.1.1035
@@ -920,6 +938,14 @@ g:lsp_semantic_enabled *g:lsp_semantic_enabled*
Determines whether or not semantic highlighting is enabled globally. Set
to `1` to enable sending requests.
g:lsp_semantic_delay *g:lsp_semantic_delay*
Type: |Number|
Default: `500`
Modifications which occur within |g:lsp_semantic_delay| of one another are
lumped into a single `semanticTokens` request. Sets the maximum rate at
which the semantic highlighting can update.
g:lsp_text_document_did_save_delay *g:lsp_text_document_did_save_delay*
Type: |Number|
Default: `-1`
@@ -1671,13 +1697,9 @@ LspRename *:LspRename*
Rename the symbol.
LspSemanticScopes *:LspSemanticScopes*
LspSemanticHighlightGroups *:LspSemanticHighlightGroups*
Prints the scope tree for the current buffer. Each scope is highlighted with
the highlight group vim-lsp will use for this scope. An optional argument can
be supplied to limit the maximum depth of the tree that will be printed.
Also see |vim-lsp-semantic|.
List the highlight groups provided by the current semantic tokens server.
LspTypeDefinition *:LspTypeDefinition*
@@ -1948,79 +1970,64 @@ Semantic highlighting *vim-lsp-semantic*
To use semantic highlighting, you need Neovim highlights, or Vim with the
|textprop| feature enabled at compile time.
You can check if semantic highlighting is enabled by running: >
To enable semantic highlighting, |g:lsp_semantic_enabled| should be set to `1`
(it is `0` by default). You can check if semantic highlighting is enabled
by running: >
echo lsp#ui#vim#semantic#is_enabled()
<
Before you can use the semantic highlighting feature, you will also need to
link language server semantic scopes to Vim highlight groups. For example, you
have to specify that function calls need to be highlighted using the
|Identifier| Vim group.
To find out the available scopes for your language server, you can open a
buffer of the correct filetype and run :|LspSemanticScopes|. This will display
a tree representation of the available scopes, along with their highlight
group, if any.
vim-lsp provides |highlight| groups for each of the token types supported by
the current LSP server. This includes highlight groups for each of the
standard set of token types:
* `LspSemanticType`
* `LspSemanticClass`
* `LspSemanticEnum`
* `LspSemanticInterface`
* `LspSemanticStruct`
* `LspSemanticTypeParameter`
* `LspSemanticParameter`
* `LspSemanticVariable`
* `LspSemanticProperty`
* `LspSemanticEnumMember`
* `LspSemanticEvents`
* `LspSemanticFunction`
* `LspSemanticMethod`
* `LspSemanticKeyword`
* `LspSemanticModifier`
* `LspSemanticComment`
* `LspSemanticString`
* `LspSemanticNumber`
* `LspSemanticRegexp`
* `LspSemanticOperator`
as well as additional highlight groups for any further types supported by the
server. For example, clangd provides `LspNamespace`.
For example, consider this excerpt from the Eclipse Java language server: >
entity.name.function.java
meta.function-call.java
meta.method.body.java
meta.method.java
meta.class.body.java
meta.class.java
source.java
meta.method.identifier.java
meta.function-call.java
meta.method.java
meta.class.body.java
meta.class.java
source.java
meta.method.java
meta.class.body.java
meta.class.java
source.java
entity.name.type.class.java
meta.class.identifier.java
meta.class.java
source.java
...
<
`entity.name.type.class.java` is a top-level scope.
`entity.name.function.java` is also a top-level scope, which itself contains
two subscopes `meta.function-call.java` and `meta.method.identifier.java`.
The standard set of token types have sensible defaults provided, however
any other types require manual configuration. The types provided by the
current buffer's semantic tokens server can be found by running
|:LspSemanticTokenTypes|.
The mapping from scopes to highlight groups is done in the server
configuration options.
LSP servers may also provide modifiers for each of the tokens. The standard
set is:
* `Declaration`
* `Definition`
* `Readonly`
* `Static`
* `Deprecated`
* `Abstract`
* `Async`
* `Modification`
* `Documentation`
* `DefaultLibrary`
Servers may also provide their own modifiers. The full set of types provided
by the current buffer's semantic tokens server can be found by running
|:LspSemanticTokenModifiers|.
If modifiers are applied to a token, the name of the |highlight| group will
be prepended with each of the modifier names, for example a static default
library function will use the highlight group
`LspSemanticStaticDefaultLibraryFunction`. By default, any modified highlight
groups are linked to their unmodified equivalent.
Suppose we want to highlight classes using |Identifier| and functions using
|Label|. The language server registration for Eclipse LS will look like: >
autocmd User lsp_setup call lsp#register_server({
\ 'name': 'eclipse.jdt.ls',
\ 'cmd': {server_info->[...]},
\ 'allowlist': ['java'],
\ 'semantic_highlight': {
\ 'entity.name.type.class.java': 'Identifier',
\ 'entity.name.function.java': 'Label'
\ }
\ })
<
You can also be more specific by replacing the highlight group string with
another |Dict|, containing subkeys for the subscopes. For example, suppose we
want function calls to still use the |Label| group, but use |Identifier| for
`meta.method.identifier.java`. This can be accomplished as follows: >
autocmd User lsp_setup call lsp#register_server({
\ 'name': 'eclipse.jdt.ls',
\ 'cmd': {server_info->[...]},
\ 'allowlist': ['java'],
\ 'semantic_highlight': {
\ 'entity.name.type.class.java': 'Identifier',
\ 'entity.name.function.java': {
\ 'meta.function-call.java': 'Label',
\ 'meta.method.identifier.java': 'Identifier'
\ }
\ }
\ })
<
==============================================================================
Popup Formatting *vim-lsp-popup-format*

View File

@@ -10,6 +10,7 @@ let g:lsp_log_file = get(g:, 'lsp_log_file', '')
let g:lsp_log_verbose = get(g:, 'lsp_log_verbose', 1)
let g:lsp_debug_servers = get(g:, 'lsp_debug_servers', [])
let g:lsp_format_sync_timeout = get(g:, 'lsp_format_sync_timeout', -1)
let g:lsp_max_buffer_size = get(g:, 'lsp_max_buffer_size', 5000000)
let g:lsp_completion_documentation_enabled = get(g:, 'lsp_completion_documentation_enabled', 1)
let g:lsp_completion_documentation_delay = get(g:, 'lsp_completion_documention_delay', 80)
@@ -63,6 +64,7 @@ let g:lsp_hover_conceal = get(g:, 'lsp_hover_conceal', 1)
let g:lsp_hover_ui = get(g:, 'lsp_hover_ui', '')
let g:lsp_ignorecase = get(g:, 'lsp_ignorecase', &ignorecase)
let g:lsp_semantic_enabled = get(g:, 'lsp_semantic_enabled', 0)
let g:lsp_semantic_delay = get(g:, 'lsp_semantic_delay', 500)
let g:lsp_text_document_did_save_delay = get(g:, 'lsp_text_document_did_save_delay', -1)
let g:lsp_completion_resolve_timeout = get(g:, 'lsp_completion_resolve_timeout', 200)
let g:lsp_tagfunc_source_methods = get(g:, 'lsp_tagfunc_source_methods', ['definition', 'declaration', 'implementation', 'typeDefinition'])
@@ -144,7 +146,8 @@ command! -nargs=? -complete=customlist,lsp#server_complete LspStopServer call ls
command! -nargs=? -complete=customlist,lsp#utils#empty_complete LspSignatureHelp call lsp#ui#vim#signature_help#get_signature_help_under_cursor()
command! LspDocumentFold call lsp#ui#vim#folding#fold(0)
command! LspDocumentFoldSync call lsp#ui#vim#folding#fold(1)
command! -nargs=? LspSemanticScopes call lsp#ui#vim#semantic#display_scope_tree(<args>)
command! -nargs=0 LspSemanticTokenTypes echo lsp#internal#semantic#get_token_types()
command! -nargs=0 LspSemanticTokenModifiers echo lsp#internal#semantic#get_token_modifiers()
nnoremap <silent> <plug>(lsp-call-hierarchy-incoming) :<c-u>call lsp#ui#vim#call_hierarchy_incoming({})<cr>
nnoremap <silent> <plug>(lsp-call-hierarchy-outgoing) :<c-u>call lsp#ui#vim#call_hierarchy_outgoing()<cr>