I broke up with neovim....vim is my best friend now

This commit is contained in:
LinlyBoi
2023-04-30 08:14:07 +03:00
parent 0d185449c5
commit 4a4a6b1e81
5245 changed files with 468325 additions and 25 deletions

View File

@@ -0,0 +1,65 @@
function! s:BuildConfigBlock(section, info) abort
let l:block = get(a:info, a:section, '')
if !empty(l:block)
return printf("### %s\n%s\n", a:section, l:block)
endif
return ''
endf
function! health#lsp#check() abort
call health#report_start('server status')
let l:server_status = lsp#collect_server_status()
let l:has_printed = v:false
for l:k in sort(keys(l:server_status))
let l:report = l:server_status[l:k]
let l:status_msg = printf('%s: %s', l:k, l:report.status)
if l:report.status == 'running'
call health#report_ok(l:status_msg)
elseif l:report.status == 'failed'
call health#report_error(l:status_msg, 'See :help g:lsp_log_verbose to debug server failure.')
else
call health#report_warn(l:status_msg)
endif
let l:has_printed = v:true
endfor
if !l:has_printed
call health#report_warn('no servers connected')
endif
for l:k in sort(keys(l:server_status))
call health#report_start(printf('server configuration: %s', l:k))
let l:report = l:server_status[l:k]
let l:msg = "\t\n"
let l:msg .= s:BuildConfigBlock('allowlist', l:report.info)
let l:msg .= s:BuildConfigBlock('blocklist', l:report.info)
let l:cfg = get(l:report.info, 'workspace_config', '')
if !empty(l:cfg)
if get(g:, 'loaded_scriptease', 0)
let l:cfg = scriptease#dump(l:cfg, {'width': &columns-1})
else
let l:cfg = json_encode(l:cfg)
" Add some whitespace to make it readable.
let l:cfg = substitute(l:cfg, '[,{(\[]', "&\n\t", 'g')
let l:cfg = substitute(l:cfg, '":', '& ', 'g')
let l:cfg = substitute(l:cfg, '\v[})\]]+', "\n&", 'g')
let l:cfg = substitute(l:cfg, '\n\s*\n', "\n", 'g')
endif
let l:msg .= printf("### workspace_config\n```json\n%s\n```", l:cfg)
endif
call health#report_info(l:msg)
endfor
call health#report_start('Performance')
if lsp#utils#has_lua() && g:lsp_use_lua
call health#report_ok('Using lua for faster performance.')
else
call health#report_warn('Missing requirements to enable lua for faster performance.')
endif
endf

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
function! s:has_provider(server_name, ...) abort
let l:value = lsp#get_server_capabilities(a:server_name)
for l:provider in a:000
if empty(l:value) || type(l:value) != type({}) || !has_key(l:value, l:provider)
return 0
endif
let l:value = l:value[l:provider]
endfor
return (type(l:value) == type(v:true) && l:value == v:true) || type(l:value) == type({})
endfunction
function! lsp#capabilities#has_declaration_provider(server_name) abort
return s:has_provider(a:server_name, 'declarationProvider')
endfunction
function! lsp#capabilities#has_definition_provider(server_name) abort
return s:has_provider(a:server_name, 'definitionProvider')
endfunction
function! lsp#capabilities#has_references_provider(server_name) abort
return s:has_provider(a:server_name, 'referencesProvider')
endfunction
function! lsp#capabilities#has_hover_provider(server_name) abort
return s:has_provider(a:server_name, 'hoverProvider')
endfunction
function! lsp#capabilities#has_rename_provider(server_name) abort
return s:has_provider(a:server_name, 'renameProvider')
endfunction
function! lsp#capabilities#has_rename_prepare_provider(server_name) abort
return s:has_provider(a:server_name, 'renameProvider', 'prepareProvider')
endfunction
function! lsp#capabilities#has_workspace_folders_change_notifications(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if type(l:capabilities) == type({}) && !empty(l:capabilities)
let l:workspace = get(l:capabilities, 'workspace', {})
if type(l:workspace) == type({}) && !empty(l:workspace)
let l:workspace_folders = get(l:workspace, 'workspaceFolders', {})
if type(l:workspace_folders) == type({}) && !empty(l:workspace_folders)
if get(l:workspace_folders, 'supported', v:false) && get(l:workspace_folders, 'changeNotifications', '') ==# 'workspace/didChangeWorkspaceFolders'
return v:true
endif
endif
endif
endif
return v:false
endfunction
function! lsp#capabilities#has_document_formatting_provider(server_name) abort
return s:has_provider(a:server_name, 'documentFormattingProvider')
endfunction
function! lsp#capabilities#has_document_range_formatting_provider(server_name) abort
return s:has_provider(a:server_name, 'documentRangeFormattingProvider')
endfunction
function! lsp#capabilities#has_document_symbol_provider(server_name) abort
return s:has_provider(a:server_name, 'documentSymbolProvider')
endfunction
function! lsp#capabilities#has_workspace_symbol_provider(server_name) abort
return s:has_provider(a:server_name, 'workspaceSymbolProvider')
endfunction
function! lsp#capabilities#has_implementation_provider(server_name) abort
return s:has_provider(a:server_name, 'implementationProvider')
endfunction
function! lsp#capabilities#has_code_action_provider(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider')
if type(l:capabilities['codeActionProvider']) == type({})
if has_key(l:capabilities['codeActionProvider'], 'codeActionKinds') && type(l:capabilities['codeActionProvider']['codeActionKinds']) == type([])
return len(l:capabilities['codeActionProvider']['codeActionKinds']) != 0
endif
endif
endif
return s:has_provider(a:server_name, 'codeActionProvider')
endfunction
function! lsp#capabilities#has_code_lens_provider(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'codeLensProvider')
return 1
endif
return 0
endfunction
function! lsp#capabilities#has_type_definition_provider(server_name) abort
return s:has_provider(a:server_name, 'typeDefinitionProvider')
endfunction
function! lsp#capabilities#has_type_hierarchy_provider(server_name) abort
return s:has_provider(a:server_name, 'typeHierarchyProvider')
endfunction
function! lsp#capabilities#has_document_highlight_provider(server_name) abort
return s:has_provider(a:server_name, 'documentHighlightProvider')
endfunction
function! lsp#capabilities#has_folding_range_provider(server_name) abort
return s:has_provider(a:server_name, 'foldingRangeProvider')
endfunction
function! lsp#capabilities#has_call_hierarchy_provider(server_name) abort
return s:has_provider(a:server_name, 'callHierarchyProvider')
endfunction
function! lsp#capabilities#has_semantic_tokens(server_name) abort
return s:has_provider(a:server_name, 'semanticTokensProvider')
endfunction
" [supports_did_save (boolean), { 'includeText': boolean }]
function! lsp#capabilities#get_text_document_save_registration_options(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'textDocumentSync')
if type(l:capabilities['textDocumentSync']) == type({})
let l:save_options = get(l:capabilities['textDocumentSync'], 'save', 0)
if type(l:save_options) == type({})
return [1, {'includeText': get(l:save_options, 'includeText', 0)}]
else
return [l:save_options ? 1 : 0, {'includeText': 0 }]
endif
else
return [1, { 'includeText': 0 }]
endif
endif
return [0, { 'includeText': 0 }]
endfunction
" supports_did_change (boolean)
function! lsp#capabilities#get_text_document_change_sync_kind(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'textDocumentSync')
if type(l:capabilities['textDocumentSync']) == type({})
if has_key(l:capabilities['textDocumentSync'], 'change') && type(l:capabilities['textDocumentSync']['change']) == type(1)
let l:val = l:capabilities['textDocumentSync']['change']
return l:val >= 0 && l:val <= 2 ? l:val : 1
else
return 1
endif
elseif type(l:capabilities['textDocumentSync']) == type(1)
return l:capabilities['textDocumentSync']
else
return 1
endif
endif
return 1
endfunction
function! lsp#capabilities#has_signature_help_provider(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'signatureHelpProvider')
return 1
endif
return 0
endfunction
function! lsp#capabilities#get_signature_help_trigger_characters(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'signatureHelpProvider')
let l:trigger_chars = []
if type(l:capabilities['signatureHelpProvider']) == type({})
if has_key(l:capabilities['signatureHelpProvider'], 'triggerCharacters')
let l:trigger_chars = l:capabilities['signatureHelpProvider']['triggerCharacters']
endif
" If retriggerChars exist, just treat them like triggerChars.
if has_key(l:capabilities['signatureHelpProvider'], 'retriggerCharacters')
let l:trigger_chars += l:capabilities['signatureHelpProvider']['retriggerCharacters']
endif
return l:trigger_chars
endif
endif
return []
endfunction
function! lsp#capabilities#get_code_action_kinds(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider')
if type(l:capabilities['codeActionProvider']) == type({})
if has_key(l:capabilities['codeActionProvider'], 'codeActionKinds') && type(l:capabilities['codeActionProvider']['codeActionKinds']) == type([])
return l:capabilities['codeActionProvider']['codeActionKinds']
endif
endif
endif
return []
endfunction
function! lsp#capabilities#has_completion_resolve_provider(server_name) abort
return s:has_provider(a:server_name, 'completionProvider', 'resolveProvider')
endfunction
function! lsp#capabilities#has_inlay_hint_provider(server_name) abort
return s:has_provider(a:server_name, 'inlayHintProvider')
endfunction

View File

@@ -0,0 +1,465 @@
let s:save_cpo = &cpoptions
set cpoptions&vim
let s:clients = {} " { client_id: ctx }
" Vars used by native lsp
let s:jobidseq = 0
function! s:create_context(client_id, opts) abort
if a:client_id <= 0
return {}
endif
let l:ctx = {
\ 'opts': a:opts,
\ 'buffer': '',
\ 'content-length': -1,
\ 'requests': {},
\ 'request_sequence': 0,
\ 'on_notifications': {},
\ }
let s:clients[a:client_id] = l:ctx
return l:ctx
endfunction
function! s:dispose_context(client_id) abort
if a:client_id > 0
if has_key(s:clients, a:client_id)
unlet s:clients[a:client_id]
endif
endif
endfunction
function! s:on_stdout(id, data, event) abort
let l:ctx = get(s:clients, a:id, {})
if empty(l:ctx)
return
endif
let l:ctx['buffer'] .= a:data
while 1
if l:ctx['content-length'] < 0
" wait for all headers to arrive
let l:header_end_index = stridx(l:ctx['buffer'], "\r\n\r\n")
if l:header_end_index < 0
" no headers found
return
endif
let l:headers = l:ctx['buffer'][:l:header_end_index - 1]
let l:ctx['content-length'] = s:get_content_length(l:headers)
if l:ctx['content-length'] < 0
" invalid content-length
call lsp#log('on_stdout', a:id, 'invalid content-length')
call s:lsp_stop(a:id)
return
endif
let l:ctx['buffer'] = l:ctx['buffer'][l:header_end_index + 4:] " 4 = len(\r\n\r\n)
endif
if len(l:ctx['buffer']) < l:ctx['content-length']
" incomplete message, wait for next buffer to arrive
return
endif
" we have full message
let l:response_str = l:ctx['buffer'][:l:ctx['content-length'] - 1]
let l:ctx['content-length'] = -1
try
let l:response = json_decode(l:response_str)
catch
call lsp#log('s:on_stdout json_decode failed', v:exception)
endtry
let l:ctx['buffer'] = l:ctx['buffer'][len(l:response_str):]
if exists('l:response')
" call appropriate callbacks
let l:on_notification_data = { 'response': l:response }
if has_key(l:response, 'method') && has_key(l:response, 'id')
" it is a request from a server
let l:request = l:response
if has_key(l:ctx['opts'], 'on_request')
call l:ctx['opts']['on_request'](a:id, l:request)
endif
elseif has_key(l:response, 'id')
" it is a request->response
if !(type(l:response['id']) == type(0) || type(l:response['id']) == type(''))
" response['id'] can be number | string | null based on the spec
call lsp#log('invalid response id. ignoring message', l:response)
continue
endif
if has_key(l:ctx['requests'], l:response['id'])
let l:on_notification_data['request'] = l:ctx['requests'][l:response['id']]
endif
if has_key(l:ctx['opts'], 'on_notification')
" call client's on_notification first
try
call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
catch
call lsp#log('s:on_stdout client option on_notification() error', v:exception, v:throwpoint)
endtry
endif
if has_key(l:ctx['on_notifications'], l:response['id'])
" call lsp#client#send({ 'on_notification }) second
try
call l:ctx['on_notifications'][l:response['id']](a:id, l:on_notification_data, 'on_notification')
catch
call lsp#log('s:on_stdout client request on_notification() error', v:exception, v:throwpoint)
endtry
unlet l:ctx['on_notifications'][l:response['id']]
endif
if has_key(l:ctx['requests'], l:response['id'])
unlet l:ctx['requests'][l:response['id']]
else
call lsp#log('cannot find the request corresponding to response: ', l:response)
endif
else
" it is a notification
if has_key(l:ctx['opts'], 'on_notification')
try
call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
catch
call lsp#log('s:on_stdout on_notification() error', v:exception, v:throwpoint)
endtry
endif
endif
endif
if empty(l:response_str)
" buffer is empty, wait for next message to arrive
return
endif
endwhile
endfunction
function! s:get_content_length(headers) abort
for l:header in split(a:headers, "\r\n")
let l:kvp = split(l:header, ':')
if len(l:kvp) == 2
if l:kvp[0] =~? '^Content-Length'
return str2nr(l:kvp[1], 10)
endif
endif
endfor
return -1
endfunction
function! s:on_stderr(id, data, event) abort
let l:ctx = get(s:clients, a:id, {})
if empty(l:ctx)
return
endif
if has_key(l:ctx['opts'], 'on_stderr')
try
call l:ctx['opts']['on_stderr'](a:id, a:data, a:event)
catch
call lsp#log('s:on_stderr exception', v:exception, v:throwpoint)
echom v:exception
endtry
endif
endfunction
function! s:on_exit(id, status, event) abort
let l:ctx = get(s:clients, a:id, {})
if empty(l:ctx)
return
endif
if has_key(l:ctx['opts'], 'on_exit')
try
call l:ctx['opts']['on_exit'](a:id, a:status, a:event)
catch
call lsp#log('s:on_exit exception', v:exception, v:throwpoint)
echom v:exception
endtry
endif
call s:dispose_context(a:id)
endfunction
function! s:lsp_start(opts) abort
let l:opts = {
\ 'on_stdout': function('s:on_stdout'),
\ 'on_stderr': function('s:on_stderr'),
\ 'on_exit': function('s:on_exit'),
\ 'normalize': 'string'
\ }
if has_key(a:opts, 'env')
let l:opts.env = a:opts.env
endif
if has_key(a:opts, 'cmd')
let l:client_id = lsp#utils#job#start(a:opts.cmd, l:opts)
elseif has_key(a:opts, 'tcp')
let l:client_id = lsp#utils#job#connect(a:opts.tcp, l:opts)
else
return -1
endif
let l:ctx = s:create_context(l:client_id, a:opts)
let l:ctx['id'] = l:client_id
return l:client_id
endfunction
function! s:lsp_stop(id) abort
call lsp#utils#job#stop(a:id)
endfunction
let s:send_type_request = 1
let s:send_type_notification = 2
let s:send_type_response = 3
function! s:lsp_send(id, opts, type) abort " opts = { id?, method?, result?, params?, on_notification }
let l:ctx = get(s:clients, a:id, {})
if empty(l:ctx) | return -1 | endif
let l:request = { 'jsonrpc': '2.0' }
if (a:type == s:send_type_request)
let l:ctx['request_sequence'] = l:ctx['request_sequence'] + 1
let l:request['id'] = l:ctx['request_sequence']
let l:ctx['requests'][l:request['id']] = l:request
if has_key(a:opts, 'on_notification')
let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification']
endif
endif
if has_key(a:opts, 'id')
let l:request['id'] = a:opts['id']
endif
if has_key(a:opts, 'method')
let l:request['method'] = a:opts['method']
endif
if has_key(a:opts, 'params')
let l:request['params'] = a:opts['params']
endif
if has_key(a:opts, 'result')
let l:request['result'] = a:opts['result']
endif
if has_key(a:opts, 'error')
let l:request['error'] = a:opts['error']
endif
let l:json = json_encode(l:request)
let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json
call lsp#utils#job#send(a:id, l:payload)
if (a:type == s:send_type_request)
let l:id = l:request['id']
if get(a:opts, 'sync', 0) !=# 0
let l:timeout = get(a:opts, 'sync_timeout', -1)
if lsp#utils#_wait(l:timeout, {-> !has_key(l:ctx['requests'], l:request['id'])}, 1) == -1
throw 'lsp#client: timeout'
endif
endif
return l:id
else
return 0
endif
endfunction
function! s:lsp_get_last_request_id(id) abort
return s:clients[a:id]['request_sequence']
endfunction
function! s:lsp_is_error(obj_or_response) abort
let l:vt = type(a:obj_or_response)
if l:vt == type('')
return len(a:obj_or_response) > 0
elseif l:vt == type({})
return has_key(a:obj_or_response, 'error')
endif
return 0
endfunction
function! s:is_server_instantiated_notification(notification) abort
return !has_key(a:notification, 'request')
endfunction
function! s:native_out_cb(cbctx, channel, response) abort
if !has_key(a:cbctx, 'ctx') | return | endif
let l:ctx = a:cbctx['ctx']
if has_key(a:response, 'method') && has_key(a:response, 'id')
" it is a request from a server
let l:request = a:response
if has_key(l:ctx['opts'], 'on_request')
call l:ctx['opts']['on_request'](l:ctx['id'], l:request)
endif
elseif !has_key(a:response, 'id') && has_key(l:ctx['opts'], 'on_notification')
" it is a notification
let l:on_notification_data = { 'response': a:response }
try
call l:ctx['opts']['on_notification'](l:ctx['id'], l:on_notification_data, 'on_notification')
catch
call lsp#log('s:native_notification_callback on_notification() error', v:exception, v:throwpoint)
endtry
endif
endfunction
function! s:native_err_cb(cbctx, channel, response) abort
if !has_key(a:cbctx, 'ctx') | return | endif
let l:ctx = a:cbctx['ctx']
if has_key(l:ctx['opts'], 'on_stderr')
try
call l:ctx['opts']['on_stderr'](l:ctx['id'], a:response, 'stderr')
catch
call lsp#log('s:on_stderr exception', v:exception, v:throwpoint)
echom v:exception
endtry
endif
endfunction
" public apis {{{
function! lsp#client#start(opts) abort
if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
if has_key(a:opts, 'cmd')
let l:cbctx = {}
let l:jobopt = { 'in_mode': 'lsp', 'out_mode': 'lsp', 'noblock': 1,
\ 'out_cb': function('s:native_out_cb', [l:cbctx]),
\ 'err_cb': function('s:native_err_cb', [l:cbctx]),
\ }
if has_key(a:opts, 'cwd') | let l:jobopt['cwd'] = a:opts['cwd'] | endif
if has_key(a:opts, 'env') | let l:jobopt['env'] = a:opts['env'] | endif
let s:jobidseq += 1
let l:jobid = s:jobidseq " jobid == clientid
call lsp#log_verbose('using native lsp client')
let l:job = job_start(a:opts['cmd'], l:jobopt)
if job_status(l:job) !=? 'run' | return -1 | endif
let l:ctx = s:create_context(l:jobid, a:opts)
let l:ctx['id'] = l:jobid
let l:ctx['job'] = l:job
let l:ctx['channel'] = job_getchannel(l:job)
let l:cbctx['ctx'] = l:ctx
return l:jobid
elseif has_key(a:opts, 'tcp')
" add support for tcp
call lsp#log('tcp not supported when using native lsp client')
return -1
endif
endif
return s:lsp_start(a:opts)
endfunction
function! lsp#client#stop(client_id) abort
if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
let l:ctx = get(s:clients, a:client_id, {})
if empty(l:ctx) | return | endif
call job_stop(l:ctx['job'])
else
return s:lsp_stop(a:client_id)
endif
endfunction
function! lsp#client#send_request(client_id, opts) abort
if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
let l:ctx = get(s:clients, a:client_id, {})
if empty(l:ctx) | return -1 | endif
let l:request = {}
" id shouldn't be passed to request as vim will overwrite it. refer to :h language-server-protocol
if has_key(a:opts, 'method') | let l:request['method'] = a:opts['method'] | endif
if has_key(a:opts, 'params') | let l:request['params'] = a:opts['params'] | endif
call ch_sendexpr(l:ctx['channel'], l:request, { 'callback': function('s:on_response_native', [l:ctx, l:request]) })
let l:ctx['requests'][l:request['id']] = l:request
if has_key(a:opts, 'on_notification')
let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification']
endif
let l:ctx['request_sequence'] = l:request['id']
return l:request['id']
else
return s:lsp_send(a:client_id, a:opts, s:send_type_request)
endif
endfunction
function! s:on_response_native(ctx, request, channel, response) abort
" request -> response
let l:on_notification_data = { 'response': a:response, 'request': a:request }
if has_key(a:ctx['opts'], 'on_notification')
" call client's on_notification first
try
call a:ctx['opts']['on_notification'](a:ctx['id'], l:on_notification_data, 'on_notification')
catch
call lsp#log('s:on_response_native client option on_notification() error', v:exception, v:throwpoint)
endtry
endif
if has_key(a:ctx['on_notifications'], a:request['id'])
" call lsp#client#send({ 'on_notification' }) second
try
call a:ctx['on_notifications'][a:request['id']](a:ctx['id'], l:on_notification_data, 'on_notification')
catch
call lsp#log('s:on_response_native client request on_notification() error', v:exception, v:throwpoint, a:request, a:response)
endtry
unlet a:ctx['on_notifications'][a:response['id']]
if has_key(a:ctx['requests'], a:response['id'])
unlet a:ctx['requests'][a:response['id']]
else
call lsp#log('cannot find the request corresponding to response: ', a:response)
endif
endif
endfunction
function! lsp#client#send_notification(client_id, opts) abort
if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
let l:ctx = get(s:clients, a:client_id, {})
if empty(l:ctx) | return -1 | endif
let l:request = {}
if has_key(a:opts, 'method') | let l:request['method'] = a:opts['method'] | endif
if has_key(a:opts, 'params') | let l:request['params'] = a:opts['params'] | endif
call ch_sendexpr(l:ctx['channel'], l:request)
return 0
else
return s:lsp_send(a:client_id, a:opts, s:send_type_notification)
endif
endfunction
function! lsp#client#send_response(client_id, opts) abort
if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
let l:ctx = get(s:clients, a:client_id, {})
if empty(l:ctx) | return -1 | endif
let l:request = {}
if has_key(a:opts, 'id') | let l:request['id'] = a:opts['id'] | endif
if has_key(a:opts, 'result') | let l:request['result'] = a:opts['result'] | endif
if has_key(a:opts, 'error') | let l:request['error'] = a:opts['error'] | endif
call ch_sendexpr(l:ctx['channel'], l:request)
return 0
else
return s:lsp_send(a:client_id, a:opts, s:send_type_response)
endif
endfunction
function! lsp#client#get_last_request_id(client_id) abort
return s:lsp_get_last_request_id(a:client_id)
endfunction
function! lsp#client#is_error(obj_or_response) abort
return s:lsp_is_error(a:obj_or_response)
endfunction
function! lsp#client#error_message(obj_or_response) abort
try
return a:obj_or_response['error']['data']['message']
catch
endtry
try
return a:obj_or_response['error']['message']
catch
endtry
return string(a:obj_or_response)
endfunction
function! lsp#client#is_server_instantiated_notification(notification) abort
return s:is_server_instantiated_notification(a:notification)
endfunction
" }}}
let &cpoptions = s:save_cpo
unlet s:save_cpo
" vim sw=4 ts=4 et

View File

@@ -0,0 +1,207 @@
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
let s:enabled = 0
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
function! lsp#internal#completion#documentation#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_completion_documentation_enabled | return | endif
if !s:FloatingWindow.is_available() | return | endif
if !exists('##CompleteChanged') | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent('CompleteChanged'),
\ lsp#callbag#filter({_->g:lsp_completion_documentation_enabled}),
\ lsp#callbag#map({->copy(v:event)}),
\ lsp#callbag#debounceTime(g:lsp_completion_documentation_delay),
\ lsp#callbag#switchMap({event->
\ lsp#callbag#pipe(
\ s:resolve_completion(event),
\ lsp#callbag#tap({managed_user_data->s:show_floating_window(event, managed_user_data)}),
\ lsp#callbag#takeUntil(lsp#callbag#fromEvent('CompleteDone'))
\ )
\ })
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent('CompleteDone'),
\ lsp#callbag#tap({_->s:close_floating_window(v:false)}),
\ )
\ ),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! s:resolve_completion(event) abort
let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(a:event['completed_item'])
if empty(l:managed_user_data)
return lsp#callbag#of({})
endif
let l:completion_item = l:managed_user_data['completion_item']
if has_key(l:completion_item, 'documentation')
return lsp#callbag#of(l:managed_user_data)
elseif lsp#capabilities#has_completion_resolve_provider(l:managed_user_data['server_name'])
return lsp#callbag#pipe(
\ lsp#request(l:managed_user_data['server_name'], {
\ 'method': 'completionItem/resolve',
\ 'params': l:completion_item,
\ }),
\ lsp#callbag#map({x->{
\ 'server_name': l:managed_user_data['server_name'],
\ 'completion_item': x['response']['result'],
\ 'complete_position': l:managed_user_data['complete_position'],
\ }})
\ )
else
return lsp#callbag#of(l:managed_user_data)
endif
endfunction
function! s:show_floating_window(event, managed_user_data) abort
if empty(a:managed_user_data) || !pumvisible()
call s:close_floating_window(v:true)
return
endif
let l:completion_item = a:managed_user_data['completion_item']
let l:contents = []
" Add detail field if provided.
if type(get(l:completion_item, 'detail', v:null)) == type('')
if !empty(l:completion_item.detail)
let l:detail = s:MarkupContent.normalize({
\ 'language': &filetype,
\ 'value': l:completion_item['detail'],
\ }, {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
let l:contents += [l:detail]
endif
endif
" Add documentation filed if provided.
let l:documentation = s:MarkupContent.normalize(get(l:completion_item, 'documentation', ''), {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
if !empty(l:documentation)
let l:contents += [l:documentation]
endif
" Ignore if contents is empty.
if empty(l:contents)
return s:close_floating_window(v:true)
endif
" Update contents.
let l:doc_win = s:get_doc_win()
call deletebufline(l:doc_win.get_bufnr(), 1, '$')
call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n")))
" Calculate layout.
if g:lsp_float_max_width >= 1
let l:maxwidth = g:lsp_float_max_width
elseif g:lsp_float_max_width == 0
let l:maxwidth = &columns
else
let l:maxwidth = float2nr(&columns * 0.4)
endif
let l:size = l:doc_win.get_size({
\ 'maxwidth': l:maxwidth,
\ 'maxheight': float2nr(&lines * 0.4),
\ })
let l:margin_right = &columns - 1 - (float2nr(a:event.col) + float2nr(a:event.width) + 1 + (a:event.scrollbar ? 1 : 0))
let l:margin_left = float2nr(a:event.col) - 3
if l:size.width < l:margin_right
" do nothing
elseif l:margin_left <= l:margin_right
let l:size.width = l:margin_right
else
let l:size.width = l:margin_left
endif
let l:pos = s:compute_position(a:event, l:size)
if empty(l:pos)
call s:close_floating_window(v:true)
return
endif
" Show popupmenu and apply markdown syntax.
call l:doc_win.open({
\ 'row': l:pos[0] + 1,
\ 'col': l:pos[1] + 1,
\ 'width': l:size.width,
\ 'height': l:size.height,
\ 'border': v:true,
\ 'topline': 1,
\ })
call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() })
endfunction
function! s:close_floating_window(force) abort
" Ignore `CompleteDone` if it occurred by `complete()` because in this case, the popup menu will re-appear immediately.
let l:ctx = {}
function! l:ctx.callback(force) abort
if !pumvisible() || a:force
call s:get_doc_win().close()
endif
endfunction
call timer_start(1, { -> l:ctx.callback(a:force) })
endfunction
function! s:compute_position(event, size) abort
let l:col_if_right = a:event.col + a:event.width + 1 + (a:event.scrollbar ? 1 : 0)
let l:col_if_left = a:event.col - a:size.width - 2
if a:size.width >= (&columns - l:col_if_right)
let l:col = l:col_if_left
else
let l:col = l:col_if_right
endif
if l:col <= 0
return []
endif
if &columns <= l:col + a:size.width
return []
endif
return [a:event.row, l:col]
endfunction
function! s:get_doc_win() abort
if exists('s:doc_win')
return s:doc_win
endif
let s:doc_win = s:FloatingWindow.new({
\ 'on_opened': { -> execute('doautocmd <nomodeline> User lsp_float_opened') },
\ 'on_closed': { -> execute('doautocmd <nomodeline> User lsp_float_closed') }
\ })
call s:doc_win.set_var('&wrap', 1)
call s:doc_win.set_var('&conceallevel', 2)
noautocmd silent let l:bufnr = s:Buffer.create()
call s:doc_win.set_bufnr(l:bufnr)
call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
return s:doc_win
endfunction
function! lsp#internal#completion#documentation#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction

View File

@@ -0,0 +1,20 @@
function! lsp#internal#diagnostics#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_diagnostics_enabled | return | endif
call lsp#internal#diagnostics#state#_enable() " Needs to be the first one to register
call lsp#internal#diagnostics#echo#_enable()
call lsp#internal#diagnostics#highlights#_enable()
call lsp#internal#diagnostics#float#_enable()
call lsp#internal#diagnostics#signs#_enable()
call lsp#internal#diagnostics#virtual_text#_enable()
endfunction
function! lsp#internal#diagnostics#_disable() abort
call lsp#internal#diagnostics#echo#_disable()
call lsp#internal#diagnostics#float#_disable()
call lsp#internal#diagnostics#highlights#_disable()
call lsp#internal#diagnostics#virtual_text#_disable()
call lsp#internal#diagnostics#signs#_disable()
call lsp#internal#diagnostics#state#_disable() " Needs to be the last one to unregister
endfunction

View File

@@ -0,0 +1,40 @@
" options = {
" buffers: '1' " optional string, defaults to current buffer, '*' for all buffers
" }
function! lsp#internal#diagnostics#document_diagnostics_command#do(options) abort
if !g:lsp_diagnostics_enabled
call lsp#utils#error(':LspDocumentDiagnostics g:lsp_diagnostics_enabled must be enabled')
return
endif
let l:buffers = get(a:options, 'buffers', '')
let l:filtered_diagnostics = {}
if l:buffers ==# '*'
let l:filtered_diagnostics = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server()
else
let l:uri = lsp#utils#get_buffer_uri()
if !empty(l:uri)
let l:filtered_diagnostics[l:uri] = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
endif
endif
let l:result = []
for [l:uri, l:value] in items(l:filtered_diagnostics)
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr(lsp#utils#uri_to_path(l:uri)))
for l:diagnostics in values(l:value)
let l:result += lsp#ui#vim#utils#diagnostics_to_loc_list({ 'response': l:diagnostics })
endfor
endif
endfor
if empty(l:result)
call lsp#utils#error('No diagnostics results')
return
else
call setloclist(0, l:result)
echo 'Retrieved diagnostics results'
botright lopen
endif
endfunction

View File

@@ -0,0 +1,41 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
function! lsp#internal#diagnostics#echo#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_diagnostics_echo_cursor | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['CursorMoved']),
\ lsp#callbag#filter({_->g:lsp_diagnostics_echo_cursor}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_echo_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n'}),
\ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#map({_->lsp#internal#diagnostics#under_cursor#get_diagnostic()}),
\ lsp#callbag#subscribe({x->s:echo(x)}),
\ )
endfunction
function! lsp#internal#diagnostics#echo#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
let s:enabled = 0
endfunction
function! s:echo(diagnostic) abort
if !empty(a:diagnostic) && has_key(a:diagnostic, 'message')
call lsp#utils#echo_with_truncation('LSP: '. substitute(a:diagnostic['message'], '\n\+', ' ', 'g'))
let s:displaying_message = 1
elseif get(s:, 'displaying_message', 0)
call lsp#utils#echo_with_truncation('')
let s:displaying_message = 0
endif
endfunction

View File

@@ -0,0 +1,26 @@
" Return first error line or v:null if there are no errors
" available.
" options = {
" 'bufnr': '', " optional
" }
function! lsp#internal#diagnostics#first_line#get_first_error_line(options) abort
let l:bufnr = get(a:options, 'bufnr', bufnr('%'))
if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)
return v:null
endif
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
let l:first_error_line = v:null
for l:diagnostics_response in values(l:diagnostics_by_server)
for l:item in lsp#utils#iteratable(l:diagnostics_response['params']['diagnostics'])
let l:severity = get(l:item, 'severity', 1)
if l:severity ==# 1 && (l:first_error_line ==# v:null || l:first_error_line ># l:item['range']['start']['line'])
let l:first_error_line = l:item['range']['start']['line']
endif
endfor
endfor
return l:first_error_line ==# v:null ? v:null : l:first_error_line + 1
endfunction

View File

@@ -0,0 +1,123 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
function! lsp#internal#diagnostics#float#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#ui#vim#output#float_supported() | return | endif
if !g:lsp_diagnostics_float_cursor | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_float_insert_mode_enabled}),
\ lsp#callbag#tap({_->s:hide_float()}),
\ )
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_float_cursor}),
\ lsp#callbag#tap({_->s:hide_float()}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_float_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n'}),
\ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#map({_->lsp#internal#diagnostics#under_cursor#get_diagnostic()}),
\ lsp#callbag#subscribe({x->s:show_float(x)}),
\ )
endfunction
function! lsp#internal#diagnostics#float#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
let s:enabled = 0
endfunction
function! s:show_float(diagnostic) abort
let l:doc_win = s:get_doc_win()
if !empty(a:diagnostic) && has_key(a:diagnostic, 'message')
" Update contents.
call deletebufline(l:doc_win.get_bufnr(), 1, '$')
call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(a:diagnostic['message']))
" Compute size.
if g:lsp_float_max_width >= 1
let l:maxwidth = g:lsp_float_max_width
elseif g:lsp_float_max_width == 0
let l:maxwidth = &columns
else
let l:maxwidth = float2nr(&columns * 0.4)
endif
let l:size = l:doc_win.get_size({
\ 'maxwidth': l:maxwidth,
\ 'maxheight': float2nr(&lines * 0.4),
\ })
" Compute position.
let l:pos = s:compute_position(l:size)
" Open window.
call l:doc_win.open({
\ 'row': l:pos[0],
\ 'col': l:pos[1],
\ 'width': l:size.width,
\ 'height': l:size.height,
\ 'border': v:true,
\ 'topline': 1,
\ })
else
call s:hide_float()
endif
endfunction
function! s:hide_float() abort
let l:doc_win = s:get_doc_win()
call l:doc_win.close()
endfunction
function! s:get_doc_win() abort
if exists('s:doc_win')
return s:doc_win
endif
let s:doc_win = s:FloatingWindow.new({
\ 'on_opened': { -> execute('doautocmd <nomodeline> User lsp_float_opened') },
\ 'on_closed': { -> execute('doautocmd <nomodeline> User lsp_float_closed') }
\ })
call s:doc_win.set_var('&wrap', 1)
call s:doc_win.set_var('&conceallevel', 2)
noautocmd silent let l:bufnr = s:Buffer.create()
call s:doc_win.set_bufnr(l:bufnr)
call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
return s:doc_win
endfunction
function! s:compute_position(size) abort
let l:pos = screenpos(0, line('.'), col('.'))
if l:pos.row == 0 && l:pos.col == 0
let l:pos = {'curscol': screencol(), 'row': screenrow()}
endif
let l:pos = [l:pos.row + 1, l:pos.curscol + 1]
if l:pos[0] + a:size.height > &lines
let l:pos[0] = l:pos[0] - a:size.height - 3
endif
if l:pos[1] + a:size.width > &columns
let l:pos[1] = l:pos[1] - a:size.width - 3
endif
return l:pos
endfunction

View File

@@ -0,0 +1,207 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:namespace_id = '' " will be set when enabled
let s:severity_sign_names_mapping = {
\ 1: 'LspError',
\ 2: 'LspWarning',
\ 3: 'LspInformation',
\ 4: 'LspHint',
\ }
if !hlexists('LspErrorHighlight')
highlight link LspErrorHighlight Error
endif
if !hlexists('LspWarningHighlight')
highlight link LspWarningHighlight Todo
endif
if !hlexists('LspInformationHighlight')
highlight link LspInformationHighlight Normal
endif
if !hlexists('LspHintHighlight')
highlight link LspHintHighlight Normal
endif
function! lsp#internal#diagnostics#highlights#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#utils#_has_highlights() | return | endif
if !g:lsp_diagnostics_highlights_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
if empty(s:namespace_id)
if has('nvim')
let s:namespace_id = nvim_create_namespace('vim_lsp_diagnostics_highlights')
else
let s:namespace_id = 'vim_lsp_diagnostics_highlights'
for l:severity in keys(s:severity_sign_names_mapping)
let l:hl_group = s:severity_sign_names_mapping[l:severity] . 'Highlight'
call prop_type_add(s:get_prop_type_name(l:severity),
\ {'highlight': l:hl_group, 'combine': v:true, 'priority': lsp#internal#textprop#priority('diagnostics_highlight') })
endfor
endif
endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated'
\ && !lsp#client#is_error(x['response'])}),
\ lsp#callbag#map({x->x['response']['params']}),
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_highlights_insert_mode_enabled}),
\ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}),
\ ),
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_highlights_enabled}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_highlights_delay),
\ lsp#callbag#tap({x->s:clear_highlights(x)}),
\ lsp#callbag#tap({x->s:set_highlights(x)}),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#diagnostics#highlights#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call s:clear_all_highlights()
let s:enabled = 0
endfunction
function! s:get_prop_type_name(severity) abort
return s:namespace_id . '_' . get(s:severity_sign_names_mapping, a:severity, 'LspError')
endfunction
function! s:clear_all_highlights() abort
for l:bufnr in range(1, bufnr('$'))
if bufexists(l:bufnr) && bufloaded(l:bufnr)
if has('nvim')
call nvim_buf_clear_namespace(l:bufnr, s:namespace_id, 0, -1)
else
for l:severity in keys(s:severity_sign_names_mapping)
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_remove({
\ 'type': s:get_prop_type_name(l:severity),
\ 'bufnr': l:bufnr,
\ 'all': v:true })
catch
call lsp#log('diagnostics', 'clear_all_highlights', 'prop_remove', v:exception, v:throwpoint)
endtry
endfor
endif
endif
endfor
endfunction
function! s:clear_highlights(params) abort
" TODO: optimize by looking at params
call s:clear_all_highlights()
endfunction
function! s:set_highlights(params) abort
" TODO: optimize by looking at params
if !g:lsp_diagnostics_highlights_insert_mode_enabled
if mode()[0] ==# 'i' | return | endif
endif
for l:bufnr in range(1, bufnr('$'))
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_highlights(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
endfunction
function! s:place_highlights(server, diagnostics_response, bufnr) abort
" TODO: make diagnostics highlights same across vim and neovim
for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(a:bufnr, l:item['range']['start'])
let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(a:bufnr, l:item['range']['end'])
let l:severity = get(l:item, 'severity', 3)
let l:hl_group = get(s:severity_sign_names_mapping, l:severity, 'LspError') . 'Highlight'
if has('nvim')
for l:line in range(l:start_line, l:end_line)
if l:line == l:start_line
let l:highlight_start_col = l:start_col
else
let l:highlight_start_col = 1
endif
if l:line == l:end_line
let l:highlight_end_col = l:end_col
else
" neovim treats -1 as end of line, special handle it later
" when calling nvim_buf_add_higlight
let l:highlight_end_col = -1
endif
if l:start_line == l:end_line && l:highlight_start_col == l:highlight_end_col
" higlighting same start col and end col on same line
" doesn't work so use -1 for start col
let l:highlight_start_col -= 1
if l:highlight_start_col <= 0
let l:highlight_start_col = 1
endif
endif
call nvim_buf_add_highlight(a:bufnr, s:namespace_id, l:hl_group,
\ l:line - 1, l:highlight_start_col - 1, l:highlight_end_col == -1 ? -1 : l:highlight_end_col)
endfor
else
if l:start_line == l:end_line
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_add(l:start_line, l:start_col, {
\ 'end_col': l:end_col,
\ 'bufnr': a:bufnr,
\ 'type': s:get_prop_type_name(l:severity),
\ })
catch
call lsp#log('diagnostics', 'place_highlights', 'prop_add', v:exception, v:throwpoint)
endtry
else
for l:line in range(l:start_line, l:end_line)
if l:line == l:start_line
let l:highlight_start_col = l:start_col
else
let l:highlight_start_col = 1
endif
if l:line == l:end_line
let l:highlight_end_col = l:end_col
else
let l:highlight_end_col = strlen(getbufline(a:bufnr, l:line, l:line)[0]) + 1
endif
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_add(l:line, l:highlight_start_col, {
\ 'end_col': l:highlight_end_col,
\ 'bufnr': a:bufnr,
\ 'type': s:get_prop_type_name(l:severity),
\ })
catch
call lsp#log('diagnostics', 'place_highlights', 'prop_add', v:exception, v:throwpoint)
endtry
endfor
endif
endif
endfor
endfunction

View File

@@ -0,0 +1,199 @@
function! s:severity_of(diagnostic) abort
return get(a:diagnostic, 'severity', 1)
endfunction
function! lsp#internal#diagnostics#movement#_next_error(...) abort
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 1 })
let l:options = lsp#utils#parse_command_options(a:000)
call s:next_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_next_warning(...) abort
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 2 })
let l:options = lsp#utils#parse_command_options(a:000)
call s:next_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_next_diagnostics(...) abort
let l:options = lsp#utils#parse_command_options(a:000)
call s:next_diagnostic(s:get_all_buffer_diagnostics(), l:options)
endfunction
function! s:next_diagnostic(diagnostics, options) abort
if !len(a:diagnostics)
return
endif
call sort(a:diagnostics, 's:compare_diagnostics')
let l:wrap = 1
if has_key(a:options, 'wrap')
let l:wrap = a:options['wrap']
endif
let l:view = winsaveview()
let l:next_line = 0
let l:next_col = 0
for l:diagnostic in a:diagnostics
let [l:line, l:col] = lsp#utils#position#lsp_to_vim('%', l:diagnostic['range']['start'])
if l:line > l:view['lnum']
\ || (l:line == l:view['lnum'] && l:col > l:view['col'] + 1)
let l:next_line = l:line
let l:next_col = l:col - 1
break
endif
endfor
if l:next_line == 0
if !l:wrap
return
endif
" Wrap to start
let [l:next_line, l:next_col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[0]['range']['start'])
let l:next_col -= 1
endif
let l:view['lnum'] = l:next_line
let l:view['col'] = l:next_col
let l:view['topline'] = 1
let l:height = winheight(0)
let l:totalnum = line('$')
if l:totalnum > l:height
let l:half = l:height / 2
if l:totalnum - l:half < l:view['lnum']
let l:view['topline'] = l:totalnum - l:height + 1
else
let l:view['topline'] = l:view['lnum'] - l:half
endif
endif
call winrestview(l:view)
endfunction
function! lsp#internal#diagnostics#movement#_previous_error(...) abort
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 1 })
let l:options = lsp#utils#parse_command_options(a:000)
call s:previous_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_previous_warning(...) abort
let l:options = lsp#utils#parse_command_options(a:000)
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 2 })
call s:previous_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_previous_diagnostics(...) abort
let l:options = lsp#utils#parse_command_options(a:000)
call s:previous_diagnostic(s:get_all_buffer_diagnostics(), l:options)
endfunction
function! s:previous_diagnostic(diagnostics, options) abort
if !len(a:diagnostics)
return
endif
call sort(a:diagnostics, 's:compare_diagnostics')
let l:wrap = 1
if has_key(a:options, 'wrap')
let l:wrap = a:options['wrap']
endif
let l:view = winsaveview()
let l:next_line = 0
let l:next_col = 0
let l:index = len(a:diagnostics) - 1
while l:index >= 0
let [l:line, l:col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[l:index]['range']['start'])
if l:line < l:view['lnum']
\ || (l:line == l:view['lnum'] && l:col < l:view['col'])
let l:next_line = l:line
let l:next_col = l:col - 1
break
endif
let l:index = l:index - 1
endwhile
if l:next_line == 0
if !l:wrap
return
endif
" Wrap to end
let [l:next_line, l:next_col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[-1]['range']['start'])
let l:next_col -= 1
endif
let l:view['lnum'] = l:next_line
let l:view['col'] = l:next_col
let l:view['topline'] = 1
let l:height = winheight(0)
let l:totalnum = line('$')
if l:totalnum > l:height
let l:half = l:height / 2
if l:totalnum - l:half < l:view['lnum']
let l:view['topline'] = l:totalnum - l:height + 1
else
let l:view['topline'] = l:view['lnum'] - l:half
endif
endif
call winrestview(l:view)
endfunction
function! s:get_diagnostics(uri) abort
if has_key(s:diagnostics, a:uri)
return [1, s:diagnostics[a:uri]]
else
if s:is_win
" vim in windows always uses upper case for drive letter, so use lowercase in case lang server uses lowercase
" https://github.com/theia-ide/typescript-language-server/issues/23
let l:uri = substitute(a:uri, '^' . a:uri[:8], tolower(a:uri[:8]), '')
if has_key(s:diagnostics, l:uri)
return [1, s:diagnostics[l:uri]]
endif
endif
endif
return [0, {}]
endfunction
" Get diagnostics for the current buffer URI from all servers
function! s:get_all_buffer_diagnostics(...) abort
let l:server = get(a:000, 0, '')
let l:bufnr = bufnr('%')
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)
return []
endif
let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
let l:diagnostics = []
if empty(l:server)
for l:item in values(l:diagnostics_by_server)
let l:diagnostics += lsp#utils#iteratable(l:item['params']['diagnostics'])
endfor
else
if has_key(l:diagnostics_by_server, l:server)
let l:diagnostics = lsp#utils#iteratable(l:diagnostics_by_server[l:server]['params']['diagnostics'])
endif
endif
return l:diagnostics
endfunction
function! s:compare_diagnostics(d1, d2) abort
let l:range1 = a:d1['range']
let l:line1 = l:range1['start']['line'] + 1
let l:col1 = l:range1['start']['character'] + 1
let l:range2 = a:d2['range']
let l:line2 = l:range2['start']['line'] + 1
let l:col2 = l:range2['start']['character'] + 1
if l:line1 == l:line2
return l:col1 == l:col2 ? 0 : l:col1 > l:col2 ? 1 : -1
else
return l:line1 > l:line2 ? 1 : -1
endif
endfunction
" vim sw=4 ts=4 et

View File

@@ -0,0 +1,151 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:sign_group = 'vim_lsp'
let s:severity_sign_names_mapping = {
\ 1: 'LspError',
\ 2: 'LspWarning',
\ 3: 'LspInformation',
\ 4: 'LspHint',
\ }
if !hlexists('LspErrorText')
highlight link LspErrorText Error
endif
if !hlexists('LspWarningText')
highlight link LspWarningText Todo
endif
if !hlexists('LspInformationText')
highlight link LspInformationText Normal
endif
if !hlexists('LspHintText')
highlight link LspHintText Normal
endif
function! lsp#internal#diagnostics#signs#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#utils#_has_signs() | return | endif
if !g:lsp_diagnostics_signs_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
call s:define_sign('LspError', 'E>', g:lsp_diagnostics_signs_error)
call s:define_sign('LspWarning', 'W>', g:lsp_diagnostics_signs_warning)
call s:define_sign('LspInformation', 'I>', g:lsp_diagnostics_signs_information)
call s:define_sign('LspHint', 'H>', g:lsp_diagnostics_signs_hint)
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated'
\ && !lsp#client#is_error(x['response'])}),
\ lsp#callbag#map({x->x['response']['params']}),
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_signs_insert_mode_enabled}),
\ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}),
\ ),
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_signs_enabled}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_signs_delay),
\ lsp#callbag#tap({x->s:clear_signs(x)}),
\ lsp#callbag#tap({x->s:set_signs(x)}),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#diagnostics#signs#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call s:clear_all_signs()
call s:undefine_signs()
let s:enabled = 0
endfunction
" Set default sign text to handle case when user provides empty dict
function! s:define_sign(sign_name, sign_default_text, sign_options) abort
let l:options = {
\ 'text': get(a:sign_options, 'text', a:sign_default_text),
\ 'texthl': a:sign_name . 'Text',
\ 'linehl': a:sign_name . 'Line',
\ }
let l:sign_icon = get(a:sign_options, 'icon', '')
if !empty(l:sign_icon)
let l:options['icon'] = l:sign_icon
endif
call sign_define(a:sign_name, l:options)
endfunction
function! s:undefine_signs() abort
call sign_undefine('LspError')
call sign_undefine('LspWarning')
call sign_undefine('LspInformation')
call sign_undefine('LspHint')
endfunction
function! s:clear_all_signs() abort
call sign_unplace(s:sign_group)
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:clear_signs(params) abort
" TODO: optimize by looking at params
call s:clear_all_signs()
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:set_signs(params) abort
" TODO: optimize by looking at params
if !g:lsp_diagnostics_signs_insert_mode_enabled
if mode()[0] ==# 'i' | return | endif
endif
for l:bufnr in range(1, bufnr('$'))
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_signs(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
endfunction
function! s:place_signs(server, diagnostics_response, bufnr) abort
for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])
" Some language servers report an unexpected EOF one line past the end
" key 'linecount' may be missing.
if has_key(getbufinfo(a:bufnr)[0], 'linecount')
if l:line == getbufinfo(a:bufnr)[0].linecount + 1
let l:line = l:line - 1
endif
endif
if has_key(l:item, 'severity') && !empty(l:item['severity'])
let l:sign_name = get(s:severity_sign_names_mapping, l:item['severity'], 'LspError')
let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, l:sign_name, g:lsp_diagnostics_signs_priority)
let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map,
\ a:server . '_' . l:sign_name, l:sign_priority)
" pass 0 and let vim generate sign id
let l:sign_id = sign_place(0, s:sign_group, l:sign_name, a:bufnr,
\{ 'lnum': l:line, 'priority': l:sign_priority })
endif
endfor
endfunction

View File

@@ -0,0 +1,173 @@
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
"
" Refer to https://github.com/microsoft/language-server-protocol/pull/1019 on normalization of urls.
" {
" 'normalized_uri': {
" 'server_name': {
" 'method': 'textDocument/publishDiagnostics',
" 'params': {
" 'uri': 'uri', " this uri is not normalized and is exactly what server returns
" 'dignostics': [ " array can never be null but can be empty
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
" { range, message, severity?, code?, codeDesciption?, source?, tags?, relatedInformation?, data? }
" ]
" }
" }
" }
" Note: Do not remove when buffer unloads or doesn't exist since some server
" may send diagnsotics information regardless of textDocument/didOpen.
" buffer state is removed when server exits.
" TODO: reset buffer state when server initializes. ignoring for now for perf.
let s:diagnostics_state = {}
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:diagnostic_kinds = {
\ 1: 'error',
\ 2: 'warning',
\ 3: 'information',
\ 4: 'hint',
\ }
function! lsp#internal#diagnostics#state#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_diagnostics_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
call lsp#internal#diagnostics#state#_reset()
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && get(x['response'], 'method', '') ==# 'textDocument/publishDiagnostics'}),
\ lsp#callbag#tap({x->s:on_text_documentation_publish_diagnostics(x['server'], x['response'])}),
\ ),
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && get(x['response'], 'method', '') ==# '$/vimlsp/lsp_server_exit' }),
\ lsp#callbag#tap({x->s:on_exit(x['response'])}),
\ ),
\ ),
\ lsp#callbag#subscribe(),
\ )
call s:notify_diagnostics_update()
endfunction
function! lsp#internal#diagnostics#state#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call lsp#internal#diagnostics#state#_reset()
call s:notify_diagnostics_update()
let s:enabled = 0
endfunction
function! lsp#internal#diagnostics#state#_reset() abort
let s:diagnostics_state = {}
let s:diagnostics_disabled_buffers = {}
endfunction
" callers should always treat the return value as immutable
" @return {
" 'servername': response
" }
function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri) abort
return get(s:diagnostics_state, lsp#utils#normalize_uri(a:uri), {})
endfunction
" callers should always treat the return value as immutable.
" callers should treat uri as normalized via lsp#utils#normalize_uri
" @return {
" 'normalized_uri': {
" 'servername': response
" }
" }
function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server() abort
return s:diagnostics_state
endfunction
function! s:on_text_documentation_publish_diagnostics(server, response) abort
if lsp#client#is_error(a:response) | return | endif
let l:normalized_uri = lsp#utils#normalize_uri(a:response['params']['uri'])
if !has_key(s:diagnostics_state, l:normalized_uri)
let s:diagnostics_state[l:normalized_uri] = {}
endif
let s:diagnostics_state[l:normalized_uri][a:server] = a:response
call s:notify_diagnostics_update(a:server, l:normalized_uri)
endfunction
function! s:on_exit(response) abort
let l:server = a:response['params']['server']
let l:notify = 0
for [l:key, l:value] in items(s:diagnostics_state)
if has_key(l:value, l:server)
let l:notify = 1
call remove(l:value, l:server)
endif
endfor
if l:notify | call s:notify_diagnostics_update(l:server) | endif
endfunction
function! lsp#internal#diagnostics#state#_force_notify_buffer(buffer) abort
" TODO: optimize buffer only
call s:notify_diagnostics_update()
endfunction
" call s:notify_diagnostics_update()
" call s:notify_diagnostics_update('server')
" call s:notify_diagnostics_update('server', 'uri')
function! s:notify_diagnostics_update(...) abort
let l:data = { 'server': '$vimlsp', 'response': { 'method': '$/vimlsp/lsp_diagnostics_updated', 'params': {} } }
" if a:0 > 0 | let l:data['response']['params']['server'] = a:1 | endif
" if a:0 > 1 | let l:data['response']['params']['uri'] = a:2 | endif
call lsp#stream(1, l:data)
doautocmd <nomodeline> User lsp_diagnostics_updated
endfunction
function! lsp#internal#diagnostics#state#_enable_for_buffer(bufnr) abort
if getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) == 0
call setbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1)
call s:notify_diagnostics_update()
endif
endfunction
function! lsp#internal#diagnostics#state#_disable_for_buffer(bufnr) abort
if getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) != 0
call setbufvar(a:bufnr, 'lsp_diagnostics_enabled', 0)
call s:notify_diagnostics_update()
endif
endfunction
function! lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr) abort
return getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) == 1
endfunction
" Return dict with diagnostic counts for the specified buffer
" { 'error': 1, 'warning': 0, 'information': 0, 'hint': 0 }
function! lsp#internal#diagnostics#state#_get_diagnostics_count_for_buffer(bufnr) abort
let l:counts = {
\ 'error': 0,
\ 'warning': 0,
\ 'information': 0,
\ 'hint': 0,
\ }
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(a:bufnr)
let l:uri = lsp#utils#get_buffer_uri(a:bufnr)
for [l:_, l:response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
for l:diagnostic in lsp#utils#iteratable(l:response['params']['diagnostics'])
let l:key = get(s:diagnostic_kinds, get(l:diagnostic, 'severity', 1) , 'error')
let l:counts[l:key] += 1
endfor
endfor
end
return l:counts
endfunction

View File

@@ -0,0 +1,48 @@
" Returns a diagnostic object, or empty dictionary if no diagnostics are
" available.
" options = {
" 'server': '', " optional
" }
function! lsp#internal#diagnostics#under_cursor#get_diagnostic(...) abort
let l:options = get(a:000, 0, {})
let l:server = get(l:options, 'server', '')
let l:bufnr = bufnr('%')
if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)
return {}
endif
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
let l:diagnostics = []
if empty(l:server)
for l:item in values(l:diagnostics_by_server)
let l:diagnostics += lsp#utils#iteratable(l:item['params']['diagnostics'])
endfor
else
if has_key(l:diagnostics_by_server, l:server)
let l:diagnostics = lsp#utils#iteratable(l:diagnostics_by_server[l:server]['params']['diagnostics'])
endif
endif
let l:line = line('.')
let l:col = col('.')
let l:closest_diagnostic = {}
let l:closest_distance = -1
for l:diagnostic in l:diagnostics
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim('%', l:diagnostic['range']['start'])
if l:line == l:start_line
let l:distance = abs(l:start_col - l:col)
if l:closest_distance < 0 || l:distance < l:closest_distance
let l:closest_diagnostic = l:diagnostic
let l:closest_distance = l:distance
endif
endif
endfor
return l:closest_diagnostic
endfunction

View File

@@ -0,0 +1,191 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:namespace_id = '' " will be set when enabled
let s:severity_sign_names_mapping = {
\ 1: 'LspError',
\ 2: 'LspWarning',
\ 3: 'LspInformation',
\ 4: 'LspHint',
\ }
if !hlexists('LspErrorVirtualText')
if !hlexists('LspErrorText')
highlight link LspErrorVirtualText Error
else
highlight link LspErrorVirtualText LspErrorText
endif
endif
if !hlexists('LspWarningVirtualText')
if !hlexists('LspWarningText')
highlight link LspWarningVirtualText Todo
else
highlight link LspWarningVirtualText LspWarningText
endif
endif
if !hlexists('LspInformationVirtualText')
if !hlexists('LspInformationText')
highlight link LspInformationVirtualText Normal
else
highlight link LspInformationVirtualText LspInformationText
endif
endif
if !hlexists('LspHintVirtualText')
if !hlexists('LspHintText')
highlight link LspHintVirtualText Normal
else
highlight link LspHintVirtualText LspHintText
endif
endif
function! lsp#internal#diagnostics#virtual_text#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#utils#_has_nvim_virtual_text() && !lsp#utils#_has_vim_virtual_text() | return | endif
if !g:lsp_diagnostics_virtual_text_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
if has('nvim')
if empty(s:namespace_id)
let s:namespace_id = nvim_create_namespace('vim_lsp_diagnostic_virtual_text')
endif
else
if index(prop_type_list(), 'vim_lsp_LspError_virtual_text') ==# -1
call prop_type_add('vim_lsp_LspError_virtual_text', { 'highlight': 'LspErrorVirtualText' })
call prop_type_add('vim_lsp_LspWarning_virtual_text', { 'highlight': 'LspWarningVirtualText' })
call prop_type_add('vim_lsp_LspInformation_virtual_text', { 'highlight': 'LspInformationVirtualText' })
call prop_type_add('vim_lsp_LspHint_virtual_text', { 'highlight': 'LspHintVirtualText' })
endif
endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated'
\ && !lsp#client#is_error(x['response'])}),
\ lsp#callbag#map({x->x['response']['params']}),
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_virtual_text_insert_mode_enabled}),
\ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}),
\ ),
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_virtual_text_enabled}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_virtual_text_delay),
\ lsp#callbag#tap({x->s:clear_virtual_text(x)}),
\ lsp#callbag#tap({x->s:set_virtual_text(x)}),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#diagnostics#virtual_text#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call s:clear_all_virtual_text()
let s:enabled = 0
endfunction
function! s:clear_all_virtual_text() abort
if has('nvim')
for l:bufnr in nvim_list_bufs()
if bufexists(l:bufnr) && bufloaded(l:bufnr)
call nvim_buf_clear_namespace(l:bufnr, s:namespace_id, 0, -1)
endif
endfor
else
let l:types = ['vim_lsp_LspError_virtual_text', 'vim_lsp_LspWarning_virtual_text', 'vim_lsp_LspInformation_virtual_text', 'vim_lsp_LspHint_virtual_text']
for l:bufnr in map(copy(getbufinfo()), 'v:val.bufnr')
if lsp#utils#_has_prop_remove_types()
call prop_remove({'types': l:types, 'bufnr': l:bufnr, 'all': v:true})
else
for l:type in l:types
call prop_remove({'type': l:type, 'bufnr': l:bufnr, 'all': v:true})
endfor
endif
endfor
endif
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:clear_virtual_text(params) abort
" TODO: optimize by looking at params
call s:clear_all_virtual_text()
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:set_virtual_text(params) abort
" TODO: optimize by looking at params
if !g:lsp_diagnostics_virtual_text_insert_mode_enabled
if mode()[0] ==# 'i' | return | endif
endif
if has('nvim')
for l:bufnr in nvim_list_bufs()
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_virtual_text(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
else
for l:bufnr in map(copy(getbufinfo()), 'v:val.bufnr')
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_virtual_text(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
endif
endfunction
function! s:place_virtual_text(server, diagnostics_response, bufnr) abort
for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])
let l:name = get(s:severity_sign_names_mapping, get(l:item, 'severity', 3), 'LspError')
let l:text = g:lsp_diagnostics_virtual_text_prefix . l:item['message']
" Some language servers report an unexpected EOF one line past the end
if l:line == getbufinfo(a:bufnr)[0].linecount + 1
let l:line = l:line - 1
endif
if has('nvim')
let l:hl_name = l:name . 'VirtualText'
" need to do -1 for virtual text
call nvim_buf_set_virtual_text(a:bufnr, s:namespace_id, l:line - 1,
\ [[l:text, l:hl_name]], {})
else
" it's an error to add virtual text on lines that don't exist
" anymore due to async processing, just skip such diagnostics
if l:line <= getbufinfo(a:bufnr)[0].linecount
let l:type = 'vim_lsp_' . l:name . '_virtual_text'
call prop_remove({'all': v:true, 'type': l:type, 'bufnr': a:bufnr}, l:line)
call prop_add(
\ l:line, 0,
\ {
\ 'type': l:type, 'text': l:text, 'bufnr': a:bufnr,
\ 'text_align': g:lsp_diagnostics_virtual_text_align,
\ 'text_padding_left': g:lsp_diagnostics_virtual_text_padding_left,
\ 'text_wrap': g:lsp_diagnostics_virtual_text_wrap,
\ })
endif
endif
endfor
endfunction

View File

@@ -0,0 +1,131 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:sign_group = 'vim_lsp_document_code_action_signs'
if !hlexists('LspCodeActionText')
highlight link LspCodeActionText Normal
endif
function! lsp#internal#document_code_action#signs#_enable() abort
if !lsp#utils#_has_signs() | return | endif
" don't even bother registering if the feature is disabled
if !g:lsp_document_code_action_signs_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
call s:define_sign('LspCodeAction', 'A>', g:lsp_document_code_action_signs_hint)
" Note:
" - update CodeAction signs when CusorMoved or CursorHold
" - clear signs when InsertEnter or BufLeave
" - debounce code action requests
" - automatically switch to latest code action request via switchMap()
" - cancel code action request via takeUntil() when BufLeave
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']),
\ lsp#callbag#tap({_ -> s:clear_signs() }),
\ )
\ ),
\ lsp#callbag#filter({_ -> g:lsp_document_code_action_signs_enabled }),
\ lsp#callbag#debounceTime(g:lsp_document_code_action_signs_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n' && getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:send_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']}),
\ lsp#callbag#takeUntil(
\ lsp#callbag#fromEvent('BufLeave')
\ )
\ )
\ }),
\ lsp#callbag#subscribe({x->s:set_signs(x)}),
\)
endfunction
function! lsp#internal#document_code_action#signs#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:send_request() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
if empty(l:servers)
return lsp#callbag#empty()
endif
let l:range = lsp#utils#range#_get_current_line_range()
return lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#request(server, {
\ 'method': 'textDocument/codeAction',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': l:range,
\ 'context': {
\ 'diagnostics': [],
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite'],
\ }
\ }
\ })
\ }),
\ lsp#callbag#filter({x-> !lsp#client#is_error(x['response']) && !empty(x['response']['result'])}),
\ lsp#callbag#take(1),
\ )
endfunction
function! s:clear_signs() abort
call sign_unplace(s:sign_group)
endfunction
function! s:set_signs(data) abort
call s:clear_signs()
if lsp#client#is_error(a:data['response']) | return | endif
if empty(a:data['response']['result'])
return
endif
let l:bufnr = bufnr(lsp#utils#uri_to_path(a:data['request']['params']['textDocument']['uri']))
call s:place_signs(a:data, l:bufnr)
endfunction
" Set default sign text to handle case when user provides empty dict
function! s:define_sign(sign_name, sign_default_text, sign_options) abort
let l:options = {
\ 'text': get(a:sign_options, 'text', a:sign_default_text),
\ 'texthl': a:sign_name . 'Text',
\ 'linehl': a:sign_name . 'Line',
\ }
let l:sign_icon = get(a:sign_options, 'icon', '')
if !empty(l:sign_icon)
let l:options['icon'] = l:sign_icon
endif
call sign_define(a:sign_name, l:options)
endfunction
function! s:place_signs(data, bufnr) abort
if !bufexists(a:bufnr) || !bufloaded(a:bufnr)
return
endif
let l:sign_priority = g:lsp_document_code_action_signs_priority
let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, a:data['request']['params']['range']['start'])
let l:sign_id = sign_place(0, s:sign_group, 'LspCodeAction', a:bufnr,
\ { 'lnum': l:line, 'priority': l:sign_priority })
endfunction

View File

@@ -0,0 +1,86 @@
" options - {
" bufnr: bufnr('%') " required
" server - 'server_name' " optional
" sync: 0 " optional, defaults to 0 (async)
" }
function! lsp#internal#document_formatting#format(options) abort
let l:mode = mode()
if l:mode =~# '[vV]' || l:mode ==# "\<C-V>"
return lsp#internal#document_range_formatting#format(a:options)
endif
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_formatting_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(a:options['bufnr'], '&filetype')
call lsp#utils#error('textDocument/formatting not supported for ' . l:filetype)
return
endif
" TODO: ask user to select server for formatting if there are multiple servers
let l:server = l:servers[0]
redraw | echo 'Formatting Document ...'
call lsp#_new_command()
let l:request = {
\ 'method': 'textDocument/formatting',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(a:options['bufnr']),
\ 'options': {
\ 'tabSize': lsp#utils#buffer#get_indent_size(a:options['bufnr']),
\ 'insertSpaces': getbufvar(a:options['bufnr'], '&expandtab') ? v:true : v:false,
\ }
\ },
\ 'bufnr': a:options['bufnr'],
\ }
if get(a:options, 'sync', 0) == 1
try
let l:x = lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#toList(),
\ ).wait({ 'sleep': get(a:options, 'sleep', 1), 'timeout': get(a:options, 'timeout', g:lsp_format_sync_timeout) })
call s:format_next(l:x[0])
call s:format_complete()
catch
call s:format_error(v:exception . ' ' . v:throwpoint)
endtry
else
return lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe({
\ 'next':{x->s:format_next(x)},
\ 'error': {x->s:format_error(e)},
\ 'complete': {->s:format_complete()},
\ }),
\ )
endif
endfunction
function! s:format_next(x) abort
if lsp#client#is_error(a:x['response']) | return | endif
call lsp#utils#text_edit#apply_text_edits(a:x['request']['params']['textDocument']['uri'], get(a:x['response'], 'result', ''))
endfunction
function! s:format_error(e) abort
call lsp#log('Formatting Document Failed', a:e)
call lsp#utils#error('Formatting Document Failed.' . (type(a:e) == type('') ? a:e : ''))
endfunction
function! s:format_complete() abort
redraw | echo 'Formatting Document complete'
endfunction

View File

@@ -0,0 +1,238 @@
let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim')
let s:prop_id = 11
function! lsp#internal#document_highlight#_enable() abort
" don't event bother registering if the feature is disabled
if !g:lsp_document_highlight_enabled | return | endif
" Highlight group for references
if !hlexists('lspReference')
highlight link lspReference CursorColumn
endif
" Note:
" - update highlight references when CusorMoved or CursorHold
" - clear highlights when InsertEnter or BufLeave
" - debounce highlight requests
" - automatically switch to latest highlight request via switchMap()
" - cancel highlight request via takeUntil() when BufLeave
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']),
\ lsp#callbag#tap({_ -> s:clear_highlights() }),
\ )
\ ),
\ lsp#callbag#filter({_ -> g:lsp_document_highlight_enabled }),
\ lsp#callbag#debounceTime(g:lsp_document_highlight_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n' && getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:send_highlight_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']}),
\ lsp#callbag#takeUntil(
\ lsp#callbag#fromEvent('BufLeave')
\ )
\ )
\ }),
\ lsp#callbag#filter({_->mode() is# 'n'}),
\ lsp#callbag#subscribe({x->s:set_highlights(x)}),
\)
endfunction
function! lsp#internal#document_highlight#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:send_highlight_request() abort
let l:capability = 'lsp#capabilities#has_document_highlight_provider(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
return lsp#callbag#empty()
endif
return lsp#request(l:servers[0], {
\ 'method': 'textDocument/documentHighlight',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ },
\ })
endfunction
function! s:set_highlights(data) abort
let l:bufnr = bufnr('%')
call s:clear_highlights()
if mode() !=# 'n' | return | endif
if lsp#client#is_error(a:data['response']) | return | endif
" Get references from the response
let l:reference_list = a:data['response']['result']
if empty(l:reference_list)
return
endif
" Convert references to vim positions
let l:position_list = []
for l:reference in l:reference_list
call extend(l:position_list, lsp#utils#range#lsp_to_vim(l:bufnr, l:reference['range']))
endfor
call sort(l:position_list, function('s:compare_positions'))
" Ignore response if the cursor is not over a reference anymore
if s:in_reference(l:position_list) == -1 | return | endif
" Store references
if s:use_vim_textprops
let b:lsp_reference_positions = l:position_list
let b:lsp_reference_matches = []
else
let w:lsp_reference_positions = l:position_list
let w:lsp_reference_matches = []
endif
" Apply highlights to the buffer
call s:init_reference_highlight(l:bufnr)
if s:use_vim_textprops
for l:position in l:position_list
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_add(l:position[0], l:position[1], {
\ 'id': s:prop_id,
\ 'bufnr': l:bufnr,
\ 'length': l:position[2],
\ 'type': 'vim-lsp-reference-highlight'})
call add(b:lsp_reference_matches, l:position[0])
catch
call lsp#log('document_highlight', 'set_highlights', v:exception, v:throwpoint)
endtry
endfor
else
for l:position in l:position_list
let l:match = matchaddpos('lspReference', [l:position], -5)
call add(w:lsp_reference_matches, l:match)
endfor
endif
endfunction
function! s:clear_highlights() abort
if s:use_vim_textprops
if exists('b:lsp_reference_matches')
let l:bufnr = bufnr('%')
for l:line in b:lsp_reference_matches
silent! call prop_remove(
\ {'id': s:prop_id,
\ 'bufnr': l:bufnr,
\ 'all': v:true}, l:line)
endfor
unlet b:lsp_reference_matches
unlet b:lsp_reference_positions
endif
else
if exists('w:lsp_reference_matches')
for l:match in w:lsp_reference_matches
silent! call matchdelete(l:match)
endfor
unlet w:lsp_reference_matches
unlet w:lsp_reference_positions
endif
endif
endfunction
" Compare two positions
function! s:compare_positions(p1, p2) abort
let l:line_1 = a:p1[0]
let l:line_2 = a:p2[0]
if l:line_1 != l:line_2
return l:line_1 > l:line_2 ? 1 : -1
endif
let l:col_1 = a:p1[1]
let l:col_2 = a:p2[1]
return l:col_1 - l:col_2
endfunction
" If the cursor is over a reference, return its index in
" the array. Otherwise, return -1.
function! s:in_reference(reference_list) abort
let l:line = line('.')
let l:column = col('.')
let l:index = 0
for l:position in a:reference_list
if l:line == l:position[0] &&
\ l:column >= l:position[1] &&
\ l:column < l:position[1] + l:position[2]
return l:index
endif
let l:index += 1
endfor
return -1
endfunction
function! s:init_reference_highlight(buf) abort
if s:use_vim_textprops
let l:props = {
\ 'bufnr': a:buf,
\ 'highlight': 'lspReference',
\ 'combine': v:true,
\ 'priority': lsp#internal#textprop#priority('document_highlight')
\ }
if prop_type_get('vim-lsp-reference-highlight', { 'bufnr': a:buf }) == {}
call prop_type_add('vim-lsp-reference-highlight', l:props)
endif
endif
endfunction
" Cyclically move between references by `offset` occurrences.
function! lsp#internal#document_highlight#jump(offset) abort
if s:use_vim_textprops && !exists('b:lsp_reference_positions') ||
\ !s:use_vim_textprops && !exists('w:lsp_reference_positions')
echohl WarningMsg
echom 'References not available'
echohl None
return
endif
" Get index of reference under cursor
let l:index = s:use_vim_textprops ? s:in_reference(b:lsp_reference_positions) : s:in_reference(w:lsp_reference_positions)
if l:index < 0
return
endif
let l:n = s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions)
let l:index += a:offset
" Show a message when reaching TOP/BOTTOM of the file
if l:index < 0
echohl WarningMsg
echom 'search hit TOP, continuing at BOTTOM'
echohl None
elseif l:index >= (s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions))
echohl WarningMsg
echom 'search hit BOTTOM, continuing at TOP'
echohl None
endif
" Wrap index
if l:index < 0 || l:index >= (s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions))
let l:index = (l:index % l:n + l:n) % l:n
endif
" Jump
let l:target = (s:use_vim_textprops ? b:lsp_reference_positions : w:lsp_reference_positions)[l:index][0:1]
normal! m`
call cursor(l:target[0], l:target[1])
endfunction

View File

@@ -0,0 +1,278 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
" options - {
" server - 'server_name' " optional
" ui - 'float' | 'preview'
" }
function! lsp#internal#document_hover#under_cursor#do(options) abort
let l:bufnr = bufnr('%')
let l:ui = get(a:options, 'ui', g:lsp_hover_ui)
if empty(l:ui)
let l:ui = s:FloatingWindow.is_available() ? 'float' : 'preview'
endif
if l:ui ==# 'float'
let l:doc_win = s:get_doc_win()
if l:doc_win.is_visible()
if bufnr('%') ==# l:doc_win.get_bufnr()
call s:close_floating_window()
else
call l:doc_win.enter()
inoremap <buffer><silent> <Plug>(lsp-float-close) <ESC>:<C-u>call <SID>close_floating_window()<CR>
nnoremap <buffer><silent> <Plug>(lsp-float-close) :<C-u>call <SID>close_floating_window()<CR>
execute('doautocmd <nomodeline> User lsp_float_focused')
if !hasmapto('<Plug>(lsp-float-close)')
imap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
nmap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
endif
endif
return
endif
endif
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_hover_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(l:bufnr, '&filetype')
call lsp#utils#error('textDocument/hover not supported for ' . l:filetype)
return
endif
redraw | echo 'Retrieving hover ...'
call lsp#_new_command()
" TODO: ask user to select server for formatting if there are multiple servers
let l:request = {
\ 'method': 'textDocument/hover',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ },
\ }
call lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#request(server, l:request)
\ }),
\ lsp#callbag#tap({x->s:show_hover(l:ui, x['server_name'], x['request'], x['response'])}),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#document_hover#under_cursor#getpreviewwinid() abort
if exists('s:doc_win')
return s:doc_win.get_winid()
endif
return v:null
endfunction
function! s:show_hover(ui, server_name, request, response) abort
if !has_key(a:response, 'result') || empty(a:response['result']) ||
\ empty(a:response['result']['contents'])
call lsp#utils#error('No hover information found in server - ' . a:server_name)
return
endif
echo ''
if s:FloatingWindow.is_available() && a:ui ==? 'float'
call s:show_floating_window(a:server_name, a:request, a:response)
else
call s:show_preview_window(a:server_name, a:request, a:response)
endif
endfunction
function! s:show_preview_window(server_name, request, response) abort
let l:contents = s:get_contents(a:response['result']['contents'])
" Ignore if contents is empty.
if empty(l:contents)
call lsp#utils#error('Empty contents for LspHover')
return
endif
let l:lines = lsp#utils#_split_by_eol(join(l:contents, "\n\n"))
let l:view = winsaveview()
let l:alternate=@#
silent! pclose
sp LspHoverPreview
execute 'resize '.min([len(l:lines), &previewheight])
set previewwindow
setlocal conceallevel=2
setlocal bufhidden=hide
setlocal nobuflisted
setlocal buftype=nofile
setlocal noswapfile
%d _
call setline(1, l:lines)
call s:Window.do(win_getid(), {->s:Markdown.apply()})
execute "normal \<c-w>p"
call winrestview(l:view)
let @#=l:alternate
endfunction
function! s:show_floating_window(server_name, request, response) abort
call s:close_floating_window()
let l:contents = s:get_contents(a:response['result']['contents'])
" Ignore if contents is empty.
if empty(l:contents)
return s:close_floating_window()
endif
" Update contents.
let l:doc_win = s:get_doc_win()
silent! call deletebufline(l:doc_win.get_bufnr(), 1, '$')
call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n")))
" Calculate layout.
if g:lsp_float_max_width >= 1
let l:maxwidth = g:lsp_float_max_width
elseif g:lsp_float_max_width == 0
let l:maxwidth = &columns
else
let l:maxwidth = float2nr(&columns * 0.4)
endif
let l:size = l:doc_win.get_size({
\ 'maxwidth': l:maxwidth,
\ 'maxheight': float2nr(&lines * 0.4),
\ })
let l:pos = s:compute_position(l:size)
if empty(l:pos)
call s:close_floating_window()
return
endif
execute printf('augroup vim_lsp_hover_close_on_move_%d', bufnr('%'))
autocmd!
execute printf('autocmd InsertEnter,BufLeave,CursorMoved <buffer> call s:close_floating_window_on_move(%s)', getcurpos())
augroup END
" Show popupmenu and apply markdown syntax.
call l:doc_win.open({
\ 'row': l:pos[0],
\ 'col': l:pos[1],
\ 'width': l:size.width,
\ 'height': l:size.height,
\ 'border': v:true,
\ })
call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() })
" Format contents to fit window
call setbufvar(l:doc_win.get_bufnr(), '&textwidth', l:size.width)
call s:Window.do(l:doc_win.get_winid(), { -> s:format_window() })
endfunction
function! s:format_window() abort
global/^/normal! gqgq
endfunction
function! s:get_contents(contents) abort
if type(a:contents) == type('')
return [a:contents]
elseif type(a:contents) == type([])
let l:result = []
for l:content in a:contents
let l:result += s:get_contents(l:content)
endfor
return l:result
elseif type(a:contents) == type({})
if has_key(a:contents, 'value')
if has_key(a:contents, 'kind')
if a:contents['kind'] ==? 'markdown'
let l:detail = s:MarkupContent.normalize(a:contents['value'], {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
return [l:detail]
else
return [a:contents['value']]
endif
elseif has_key(a:contents, 'language')
let l:detail = s:MarkupContent.normalize(a:contents, {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
return [l:detail]
else
return ''
endif
else
return ''
endif
else
return ''
endif
endfunction
function! s:close_floating_window() abort
call s:get_doc_win().close()
endfunction
function! s:close_floating_window_on_move(curpos) abort
if a:curpos != getcurpos() | call s:close_floating_window() | endif
endf
function! s:on_opened() abort
inoremap <buffer><silent> <Plug>(lsp-float-close) <ESC>:<C-u>call <SID>close_floating_window()<CR>
nnoremap <buffer><silent> <Plug>(lsp-float-close) :<C-u>call <SID>close_floating_window()<CR>
execute('doautocmd <nomodeline> User lsp_float_opened')
if !hasmapto('<Plug>(lsp-float-close)')
imap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
nmap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
endif
endfunction
function! s:on_closed() abort
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction
function! s:get_doc_win() abort
if exists('s:doc_win')
return s:doc_win
endif
let s:doc_win = s:FloatingWindow.new({
\ 'on_opened': function('s:on_opened'),
\ 'on_closed': function('s:on_closed')
\ })
call s:doc_win.set_var('&wrap', 1)
call s:doc_win.set_var('&conceallevel', 2)
call s:doc_win.set_bufnr(s:Buffer.create())
call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
return s:doc_win
endfunction
function! s:compute_position(size) abort
let l:pos = screenpos(0, line('.'), col('.'))
if l:pos.row == 0 && l:pos.col == 0
" workaround for float position
let l:pos = {'curscol': wincol(), 'row': winline()}
endif
let l:pos = [l:pos.row + 1, l:pos.curscol + 1]
if l:pos[0] + a:size.height > &lines
let l:pos[0] = l:pos[0] - a:size.height - 3
endif
if l:pos[1] + a:size.width > &columns
let l:pos[1] = l:pos[1] - a:size.width - 3
endif
return l:pos
endfunction

View File

@@ -0,0 +1,125 @@
" options - {
" bufnr: bufnr('%') " required
" type: '' " optional: defaults to visualmode(). overridden by opfunc
" server - 'server_name' " optional
" sync: 0 " optional, defaults to 0 (async)
" }
function! lsp#internal#document_range_formatting#format(options) abort
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_range_formatting_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(a:options['bufnr'], '&filetype')
call lsp#utils#error('textDocument/rangeFormatting not supported for ' . l:filetype)
return
endif
" TODO: ask user to select server for formatting if there are multiple servers
let l:server = l:servers[0]
redraw | echo 'Formatting Document Range ...'
call lsp#_new_command()
let [l:start_lnum, l:start_col, l:end_lnum, l:end_col] = s:get_selection_pos(get(a:options, 'type', visualmode()))
let l:start_char = lsp#utils#to_char('%', l:start_lnum, l:start_col)
let l:end_char = lsp#utils#to_char('%', l:end_lnum, l:end_col)
let l:request = {
\ 'method': 'textDocument/rangeFormatting',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(a:options['bufnr']),
\ 'range': {
\ 'start': { 'line': l:start_lnum - 1, 'character': l:start_char },
\ 'end': { 'line': l:end_lnum - 1, 'character': l:end_char },
\ },
\ 'options': {
\ 'tabSize': lsp#utils#buffer#get_indent_size(a:options['bufnr']),
\ 'insertSpaces': getbufvar(a:options['bufnr'], '&expandtab') ? v:true : v:false,
\ }
\ },
\ 'bufnr': a:options['bufnr'],
\ }
if get(a:options, 'sync', 0) == 1
try
let l:x = lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#toList(),
\ ).wait({ 'sleep': get(a:options, 'sleep', 1), 'timeout': get(a:options, 'timeout', g:lsp_format_sync_timeout) })
call s:format_next(l:x[0])
call s:format_complete()
catch
call s:format_error(v:exception . ' ' . v:throwpoint)
endtry
else
return lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe({
\ 'next':{x->s:format_next(x)},
\ 'error': {x->s:format_error(e)},
\ 'complete': {->s:format_complete()},
\ }),
\ )
endif
endfunction
function! s:format_next(x) abort
if lsp#client#is_error(a:x['response']) | return | endif
call lsp#utils#text_edit#apply_text_edits(a:x['request']['params']['textDocument']['uri'], a:x['response']['result'])
endfunction
function! s:format_error(e) abort
call lsp#log('Formatting Document Range Failed', a:e)
call lsp#utils#error('Formatting Document Range Failed.' . (type(a:e) == type('') ? a:e : ''))
endfunction
function! s:format_complete() abort
redraw | echo 'Formatting Document Range complete'
endfunction
function! s:get_selection_pos(type) abort
" TODO: support bufnr
if a:type ==? 'v'
let l:start_pos = getpos("'<")[1:2]
let l:end_pos = getpos("'>")[1:2]
" fix end_pos column (see :h getpos() and :h 'selection')
let l:end_line = getline(l:end_pos[0])
let l:offset = (&selection ==# 'inclusive' ? 1 : 2)
let l:end_pos[1] = len(l:end_line[:l:end_pos[1]-l:offset])
" edge case: single character selected with selection=exclusive
if l:start_pos[0] == l:end_pos[0] && l:start_pos[1] > l:end_pos[1]
let l:end_pos[1] = l:start_pos[1]
endif
elseif a:type ==? 'line'
let l:start_pos = [line("'["), 1]
let l:end_lnum = line("']")
let l:end_pos = [line("']"), len(getline(l:end_lnum))]
elseif a:type ==? 'char'
let l:start_pos = getpos("'[")[1:2]
let l:end_pos = getpos("']")[1:2]
else
let l:start_pos = [0, 0]
let l:end_pos = [0, 0]
endif
return l:start_pos + l:end_pos
endfunction
function! lsp#internal#document_range_formatting#opfunc(type) abort
call lsp#internal#document_range_formatting#format({
\ 'type': a:type,
\ 'bufnr': bufnr('%'),
\ })
endfunction

View File

@@ -0,0 +1,76 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol
" options - {
" bufnr: bufnr('%') " optional
" server - 'server_name' " optional
" }
function! lsp#internal#document_symbol#search#do(options) abort
let l:bufnr = get(a:options, 'bufnr', bufnr('%'))
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(l:bufnr, '&filetype')
call lsp#utils#error('textDocument/documentSymbol not supported for ' . l:filetype)
return
endif
redraw | echo 'Retrieving document symbols ...'
call lsp#internal#ui#quickpick#open({
\ 'items': [],
\ 'busy': 1,
\ 'input': '',
\ 'key': 'text',
\ 'on_accept': function('s:on_accept'),
\ 'on_close': function('s:on_close'),
\ })
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#callbag#pipe(
\ lsp#request(server, {
\ 'method': 'textDocument/documentSymbol',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(l:bufnr),
\ },
\ }),
\ lsp#callbag#map({x->{'server': server, 'request': x['request'], 'response': x['response']}}),
\ )
\ }),
\ lsp#callbag#scan({acc, curr->add(acc, curr)}, []),
\ lsp#callbag#tap({x->s:update_ui_items(x)}),
\ lsp#callbag#subscribe({
\ 'complete':{->lsp#internal#ui#quickpick#busy(0)},
\ 'error':{e->s:on_error(e)},
\ }),
\ )
endfunction
function! s:update_ui_items(x) abort
let l:items = []
for l:i in a:x
let l:items += lsp#ui#vim#utils#symbols_to_loc_list(l:i['server'], l:i)
endfor
call lsp#internal#ui#quickpick#items(l:items)
endfunction
function! s:on_accept(data, ...) abort
call lsp#internal#ui#quickpick#close()
call lsp#utils#location#_open_vim_list_item(a:data['items'][0], '')
endfunction
function! s:on_close(...) abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#internal#ui#quickpick#close()
call lsp#log('LspDocumentSymbolSearch error', a:e)
endfunction

View File

@@ -0,0 +1,127 @@
let s:use_vim_textprops = lsp#utils#_has_vim_virtual_text() && !has('nvim')
function! s:set_inlay_hints(data) abort
let l:bufnr = bufnr('%')
call s:clear_inlay_hints()
if mode() !=# 'n' | return | endif
if lsp#client#is_error(a:data['response']) | return | endif
" Get hints from the response
let l:hints = a:data['response']['result']
if empty(l:hints)
return
endif
let l:not_curline = s:has_inlay_hints_mode('!curline')
for l:hint in l:hints
if l:not_curline && l:hint.position.line+1 ==# line('.')
continue
endif
let l:label = ''
if type(l:hint.label) ==# v:t_list
let l:label = join(map(copy(l:hint.label), {_,v -> v.value}), '')
else
let l:label = l:hint.label
endif
let l:text = (get(l:hint, 'paddingLeft', v:false) ? ' ' : '') . l:label . (get(l:hint, 'paddingRight', v:false) ? ' ' : '')
if !has_key(l:hint, 'kind') || l:hint.kind ==# 1
call prop_add(l:hint.position.line+1, l:hint.position.character+1, {'type': 'vim_lsp_inlay_hint_type', 'text': l:text, 'bufnr': l:bufnr})
elseif l:hint.kind ==# 2
call prop_add(l:hint.position.line+1, l:hint.position.character+1, {'type': 'vim_lsp_inlay_hint_parameter', 'text': l:text, 'bufnr': l:bufnr})
endif
endfor
endfunction
function! s:init_inlay_hints() abort
if index(prop_type_list(), 'vim_lsp_inlay_hint_type') ==# -1
call prop_type_add('vim_lsp_inlay_hint_type', { 'highlight': 'lspInlayHintsType' })
call prop_type_add('vim_lsp_inlay_hint_parameter', { 'highlight': 'lspInlayHintsParameter' })
endif
endfunction
function! lsp#internal#inlay_hints#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:clear_inlay_hints() abort
let l:bufnr = bufnr('%')
call prop_remove({'type': 'vim_lsp_inlay_hint_type', 'bufnr': l:bufnr, 'all': v:true})
call prop_remove({'type': 'vim_lsp_inlay_hint_parameter', 'bufnr': l:bufnr, 'all': v:true})
endfunction
function! s:has_inlay_hints_mode(value) abort
let l:m = get(g:, 'lsp_inlay_hints_mode', {})
if type(l:m) != v:t_dict | return v:false | endif
if mode() ==# 'i'
let l:a = get(l:m, 'insert', [])
elseif mode() ==# 'n'
let l:a = get(l:m, 'normal', [])
else
return v:false
endif
if type(l:a) != v:t_list | return v:false | endif
return index(l:a, a:value) != -1 ? v:true : v:false
endfunction
function! s:send_inlay_hints_request() abort
let l:capability = 'lsp#capabilities#has_inlay_hint_provider(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
return lsp#callbag#empty()
endif
if s:has_inlay_hints_mode('curline')
let l:range = lsp#utils#range#get_range_curline()
else
let l:range = lsp#utils#range#get_range()
endif
return lsp#request(l:servers[0], {
\ 'method': 'textDocument/inlayHint',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': l:range,
\ },
\ })
endfunction
function! lsp#internal#inlay_hints#_enable() abort
if !s:use_vim_textprops | return | endif
if !g:lsp_inlay_hints_enabled | return | endif
if !hlexists('lspInlayHintsType')
highlight link lspInlayHintsType Label
endif
if !hlexists('lspInlayHintsParameter')
highlight link lspInlayHintsParameter Todo
endif
call s:init_inlay_hints()
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']),
\ lsp#callbag#tap({_ -> s:clear_inlay_hints() }),
\ )
\ ),
\ lsp#callbag#filter({_ -> g:lsp_inlay_hints_enabled }),
\ lsp#callbag#debounceTime(g:lsp_inlay_hints_delay),
\ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !~# '^(help\|terminal\|prompt\|popup)$'}),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:send_inlay_hints_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']})
\ )
\ }),
\ lsp#callbag#subscribe({x->s:set_inlay_hints(x)}),
\)
endfunction

View File

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

View File

@@ -0,0 +1,74 @@
let s:ErrorType = 1
let s:WarningType = 2
let s:InfoType = 3
let s:LogType = 4
function! lsp#internal#show_message#_enable() abort
if g:lsp_show_message_log_level ==# 'none' | return | endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->
\ g:lsp_show_message_log_level !=# 'none' &&
\ has_key(x, 'response') && has_key(x['response'], 'method')
\ && x['response']['method'] ==# 'window/showMessage'
\ }),
\ lsp#callbag#tap({x->s:handle_show_message(x['server'], x['response']['params'])}),
\ lsp#callbag#subscribe({ 'error': function('s:on_error') }),
\ )
endfunction
function! lsp#internal#show_message#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#log('lsp#internal#show_message error', a:e)
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:handle_show_message(server, params) abort
let l:level = s:name_to_level(g:lsp_show_message_log_level)
let l:type = a:params['type']
if l:level < l:type
return
endif
let l:message = a:params['message']
try
if l:type == s:ErrorType
echohl ErrorMsg
elseif l:type == s:WarningType
echohl WarningMsg
endif
echom printf('%s: %s: %s', a:server, s:type_to_name(l:type), l:message)
finally
echohl None
endtry
endfunction
function! s:name_to_level(name) abort
if a:name ==# 'none'
return 0
elseif a:name ==# 'error'
return s:ErrorType
elseif a:name ==# 'warn' || a:name ==# 'warning'
return s:WarningType
elseif a:name ==# 'info'
return s:InfoType
elseif a:name ==# 'log'
return s:LogType
else
return 0
endif
endfunction
function! s:type_to_name(type) abort
return get(['unknown', 'error', 'warning', 'info', 'log'], a:type, 'unknown')
endfunction

View File

@@ -0,0 +1,58 @@
function! lsp#internal#show_message_request#_enable() abort
if !g:lsp_show_message_request_enabled | return | endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->
\ g:lsp_show_message_request_enabled &&
\ has_key(x, 'request') && !has_key(x, 'response') &&
\ has_key(x['request'], 'method') && x['request']['method'] ==# 'window/showMessageRequest'
\ }),
\ lsp#callbag#map({x->s:show_message_request(x['server'], x['request'])}),
\ lsp#callbag#map({x->s:send_message_response(x['server'], x['request'], x['action'])}),
\ lsp#callbag#flatten(),
\ lsp#callbag#materialize(),
\ lsp#callbag#subscribe({ 'error': function('s:on_error') }),
\ )
endfunction
function! lsp#internal#show_message_request#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#log('lsp#internal#show_message_request error', a:e)
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:show_message_request(server_name, request) abort
let l:params = a:request['params']
let l:selected_action = v:null
if has_key(l:params, 'actions') && !empty(l:params['actions'])
let l:options = map(copy(l:params['actions']), {i, action ->
\ printf('%d - [%s] %s', i + 1, a:server_name, action['title'])
\ })
let l:index = inputlist([l:params['message']] + l:options)
if l:index > 0 && l:index <= len(l:index)
let l:selected_action = l:params['actions'][l:index - 1]
endif
else
echom l:params['message']
endif
return { 'server': a:server_name, 'request': a:request, 'action': l:selected_action }
endfunction
function! s:send_message_response(server_name, request, action) abort
return lsp#request(a:server_name, {
\ 'id': a:request['id'],
\ 'result': a:action
\})
endfunction

View File

@@ -0,0 +1,13 @@
" TODO: currently, quickpick is generated via vim-quickpick, 'quickpick' is
" not used.
let s:priorities = {
\ 'quickpick': 1,
\ 'folding': 2,
\ 'semantic': 3,
\ 'diagnostics_highlight': 4,
\ 'document_highlight': 5,
\}
function! lsp#internal#textprop#priority(name) abort
return get(s:priorities, a:name, 0)
endfunction

View File

@@ -0,0 +1,91 @@
function! lsp#internal#type_hierarchy#show() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_type_hierarchy_provider(v:val)')
let l:command_id = lsp#_new_command()
if len(l:servers) == 0
return lsp#utils#error('Retrieving type hierarchy not supported for ' . &filetype)
endif
let l:ctx = { 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id }
" direction 0 children, 1 parent, 2 both
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/typeHierarchy',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ 'direction': 2,
\ 'resolve': 1,
\ },
\ 'on_notification': function('s:handle_type_hierarchy', [l:ctx, l:server, 'type hierarchy']),
\ })
endfor
echo 'Retrieving type hierarchy ...'
endfunction
function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_command_id}
if a:ctx['last_command_id'] != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
if empty(a:data['response']['result'])
echo 'No type hierarchy found'
return
endif
" Create new buffer in a split
let l:position = 'topleft'
let l:orientation = 'new'
exec l:position . ' ' . 10 . l:orientation
let l:provider = {
\ 'root': a:data['response']['result'],
\ 'root_state': 'expanded',
\ 'bufnr': bufnr('%'),
\ 'getChildren': function('s:get_children_for_tree_hierarchy'),
\ 'getParent': function('s:get_parent_for_tree_hierarchy'),
\ 'getTreeItem': function('s:get_treeitem_for_tree_hierarchy'),
\ }
call lsp#utils#tree#new(l:provider)
echo 'Retrieved type hierarchy'
endfunction
function! s:hierarchyitem_to_treeitem(hierarchyitem) abort
return {
\ 'id': a:hierarchyitem,
\ 'label': a:hierarchyitem['name'],
\ 'command': function('s:hierarchy_treeitem_command', [a:hierarchyitem]),
\ 'collapsibleState': has_key(a:hierarchyitem, 'parents') && !empty(a:hierarchyitem['parents']) ? 'expanded' : 'none',
\ }
endfunction
function! s:hierarchy_treeitem_command(hierarchyitem) abort
bwipeout
call lsp#utils#tagstack#_update()
call lsp#utils#location#_open_lsp_location(a:hierarchyitem)
endfunction
function! s:get_children_for_tree_hierarchy(Callback, ...) dict abort
if a:0 == 0
call a:Callback('success', [l:self['root']])
return
else
call a:Callback('success', a:1['parents'])
endif
endfunction
function! s:get_parent_for_tree_hierarchy(...) dict abort
" TODO
endfunction
function! s:get_treeitem_for_tree_hierarchy(Callback, object) dict abort
call a:Callback('success', s:hierarchyitem_to_treeitem(a:object))
endfunction

View File

@@ -0,0 +1,39 @@
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:Window = vital#lsp#import('VS.Vim.Window')
function! lsp#internal#ui#popupmenu#open(opt) abort
let l:Callback = remove(a:opt, 'callback')
let l:items = remove(a:opt, 'items')
let l:items_with_shortcuts= map(l:items, {
\ idx, item -> ((idx < 9) ? '['.(idx+1).'] ' : '').item
\ })
function! Filter(id, key) abort closure
if a:key >= 1 && a:key <= len(l:items)
call popup_close(a:id, a:key)
elseif a:key ==# "\<C-j>"
call win_execute(a:id, 'normal! j')
elseif a:key ==# "\<C-k>"
call win_execute(a:id, 'normal! k')
else
return popup_filter_menu(a:id, a:key)
endif
return v:true
endfunction
let l:popup_opt = extend({
\ 'callback': funcref('s:callback', [l:Callback]),
\ 'filter': funcref('Filter'),
\ }, a:opt)
let l:winid = popup_menu(l:items_with_shortcuts, l:popup_opt)
call s:Window.do(l:winid, { -> s:Markdown.apply() })
execute('doautocmd <nomodeline> User lsp_float_opened')
endfunction
function! s:callback(callback, id, selected) abort
call a:callback(a:id, a:selected)
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction

View File

@@ -0,0 +1,461 @@
" https://github.com/prabirshrestha/quickpick.vim#968f00787c1a118228aee869351e754bec555298
" :QuickpickEmbed path=autoload/lsp/internal/ui/quickpick.vim namespace=lsp#internal#ui#quickpick prefix=lsp-quickpick
let s:has_timer = exists('*timer_start') && exists('*timer_stop')
let s:has_matchfuzzy = exists('*matchfuzzy')
let s:has_matchfuzzypos = exists('*matchfuzzypos')
let s:has_proptype = exists('*prop_type_add') && exists('*prop_type_delete')
"
" is_floating
"
if has('nvim')
function! s:is_floating(winid) abort
if !s:win_exists(a:winid)
return 0
endif
let l:config = nvim_win_get_config(a:winid)
return empty(l:config) || !empty(get(l:config, 'relative', ''))
endfunction
else
function! s:is_floating(winid) abort
return s:win_exists(a:winid) && win_id2win(a:winid) == 0
endfunction
endif
function! s:win_exists(winid) abort
return winheight(a:winid) != -1
endfunction
function! lsp#internal#ui#quickpick#open(opt) abort
call lsp#internal#ui#quickpick#close() " hide existing picker if exists
" when key is empty, item is a string else it is a dict
" fitems is filtered items and is the item that is filtered
let s:state = extend({
\ 'items': [],
\ 'highlights': [],
\ 'fitems': [],
\ 'key': '',
\ 'busy': 0,
\ 'busyframes': ['-', '\', '|', '/'],
\ 'filetype': 'lsp-quickpick',
\ 'promptfiletype': 'lsp-quickpick-filter',
\ 'input': '',
\ 'maxheight': 10,
\ 'debounce': 250,
\ 'filter': 1,
\ }, a:opt)
let s:inputecharpre = 0
let s:state['busyframe'] = 0
let s:state['bufnr'] = bufnr('%')
let s:state['winid'] = win_getid()
let s:state['wininfo'] = getwininfo()
" create result buffer
exe printf('keepalt botright 3new %s', s:state['filetype'])
let s:state['resultsbufnr'] = bufnr('%')
let s:state['resultswinid'] = win_getid()
if s:has_proptype
call prop_type_add('highlight', { 'highlight': 'Directory', 'bufnr': s:state['resultsbufnr'] })
endif
" create prompt buffer
exe printf('keepalt botright 1new %s', s:state['promptfiletype'])
let s:state['promptbufnr'] = bufnr('%')
let s:state['promptwinid'] = win_getid()
call win_gotoid(s:state['resultswinid'])
call s:set_buffer_options()
setlocal cursorline
call s:update_items()
exec printf('setlocal filetype=' . s:state['filetype'])
call s:notify('open', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'] , 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['resultswinid'] })
call win_gotoid(s:state['promptwinid'])
call s:set_buffer_options()
call setline(1, s:state['input'])
" map keys
inoremap <buffer><silent> <Plug>(lsp-quickpick-accept) <ESC>:<C-u>call <SID>on_accept()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-accept) :<C-u>call <SID>on_accept()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-close) <ESC>:<C-u>call lsp#internal#ui#quickpick#close()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-close) :<C-u>call lsp#internal#ui#quickpick#close()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-cancel) <ESC>:<C-u>call <SID>on_cancel()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-cancel) :<C-u>call <SID>on_cancel()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-move-next) <C-o>:<C-u>call <SID>on_move_next()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-move-next) :<C-u>call <SID>on_move_next()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-move-previous) <C-o>:<C-u>call <SID>on_move_previous()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-move-previous) :<C-u>call <SID>on_move_previous()<CR>
exec printf('setlocal filetype=' . s:state['promptfiletype'])
if !hasmapto('<Plug>(lsp-quickpick-accept)')
imap <buffer><cr> <Plug>(lsp-quickpick-accept)
nmap <buffer><cr> <Plug>(lsp-quickpick-accept)
endif
if !hasmapto('<Plug>(lsp-quickpick-cancel)')
imap <silent> <buffer> <C-c> <Plug>(lsp-quickpick-cancel)
map <silent> <buffer> <C-c> <Plug>(lsp-quickpick-cancel)
imap <silent> <buffer> <Esc> <Plug>(lsp-quickpick-cancel)
map <silent> <buffer> <Esc> <Plug>(lsp-quickpick-cancel)
endif
if !hasmapto('<Plug>(lsp-quickpick-move-next)')
imap <silent> <buffer> <C-n> <Plug>(lsp-quickpick-move-next)
nmap <silent> <buffer> <C-n> <Plug>(lsp-quickpick-move-next)
imap <silent> <buffer> <C-j> <Plug>(lsp-quickpick-move-next)
nmap <silent> <buffer> <C-j> <Plug>(lsp-quickpick-move-next)
endif
if !hasmapto('<Plug>(lsp-quickpick-move-previous)')
imap <silent> <buffer> <C-p> <Plug>(lsp-quickpick-move-previous)
nmap <silent> <buffer> <C-p> <Plug>(lsp-quickpick-move-previous)
imap <silent> <buffer> <C-k> <Plug>(lsp-quickpick-move-previous)
nmap <silent> <buffer> <C-k> <Plug>(lsp-quickpick-move-previous)
endif
call cursor(line('$'), 0)
call feedkeys('i', 'n')
augroup lsp#internal#ui#quickpick
autocmd!
autocmd InsertCharPre <buffer> call s:on_insertcharpre()
autocmd TextChangedI <buffer> call s:on_inputchanged()
autocmd InsertEnter <buffer> call s:on_insertenter()
autocmd InsertLeave <buffer> call s:on_insertleave()
if exists('##TextChangedP')
autocmd TextChangedP <buffer> call s:on_inputchanged()
endif
augroup END
call s:notify_items()
call s:notify_selection()
call lsp#internal#ui#quickpick#busy(s:state['busy'])
endfunction
function! s:set_buffer_options() abort
" set buffer options
abc <buffer>
setlocal bufhidden=unload " unload buf when no longer displayed
setlocal buftype=nofile " buffer is not related to any file<Paste>
setlocal noswapfile " don't create swap file
setlocal nowrap " don't soft-wrap
setlocal nonumber " don't show line numbers
setlocal nolist " don't use list mode (visible tabs etc)
setlocal foldcolumn=0 " don't show a fold column at side
setlocal foldlevel=99 " don't fold anything
setlocal nospell " spell checking off
setlocal nobuflisted " don't show up in the buffer list
setlocal textwidth=0 " don't hardwarp (break long lines)
setlocal nocursorline " highlight the line cursor is off
setlocal nocursorcolumn " disable cursor column
setlocal noundofile " don't enable undo
setlocal winfixheight
if exists('+colorcolumn') | setlocal colorcolumn=0 | endif
if exists('+relativenumber') | setlocal norelativenumber | endif
setlocal signcolumn=yes " for prompt
endfunction
function! lsp#internal#ui#quickpick#close() abort
if !exists('s:state')
return
endif
call lsp#internal#ui#quickpick#busy(0)
call win_gotoid(s:state['winid'])
call s:notify('close', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'], 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['winid'] })
augroup lsp#internal#ui#quickpick
autocmd!
augroup END
exe 'silent! bunload! ' . s:state['promptbufnr']
exe 'silent! bunload! ' . s:state['resultsbufnr']
call s:restore_windows()
let s:inputecharpre = 0
unlet s:state
endfunction
function! s:restore_windows() abort
let [l:tabnr, l:_] = win_id2tabwin(s:state['winid'])
if l:tabnr == 0
return
endif
let l:Resizable = {_, info ->
\ info.tabnr == l:tabnr &&
\ s:win_exists(info.winid) &&
\ !s:is_floating(info.winid)
\ }
let l:wins_to_resize = sort(filter(s:state['wininfo'], l:Resizable), {l, r -> l.winnr - r.winnr})
let l:open_winids_to_resize = map(filter(getwininfo(), l:Resizable), {_, info -> info.winid})
let l:resize_cmd = ''
for l:info in l:wins_to_resize
if index(l:open_winids_to_resize, l:info.winid) == -1
return
endif
let l:resize_cmd .= printf('%dresize %d | vert %dresize %d |', l:info.winnr, l:info.height, l:info.winnr, l:info.width)
endfor
" winrestcmd repeats :resize commands twice after patch-8.2.2631.
" To simulate this behavior, execute the :resize commands twice.
" see https://github.com/vim/vim/issues/7988
exe 'silent! ' . l:resize_cmd . l:resize_cmd
endfunction
function! lsp#internal#ui#quickpick#items(items) abort
let s:state['items'] = a:items
call s:update_items()
call s:notify_items()
call s:notify_selection()
endfunction
function! lsp#internal#ui#quickpick#busy(busy) abort
let s:state['busy'] = a:busy
if a:busy
if !has_key(s:state, 'busytimer')
let s:state['busyframe'] = 0
let s:state['busytimer'] = timer_start(60, function('s:busy_tick'), { 'repeat': -1 })
endif
else
if has_key(s:state, 'busytimer')
call timer_stop(s:state['busytimer'])
call remove(s:state, 'busytimer')
redraw
echohl None
echo ''
endif
endif
endfunction
function! lsp#internal#ui#quickpick#results_winid() abort
if exists('s:state')
return s:state['resultswinid']
else
return 0
endif
endfunction
function! s:busy_tick(...) abort
let s:state['busyframe'] = s:state['busyframe'] + 1
if s:state['busyframe'] >= len(s:state['busyframes'])
let s:state['busyframe'] = 0
endif
redraw
echohl Question | echon s:state['busyframes'][s:state['busyframe']]
echohl None
endfunction
function! s:update_items() abort
call s:win_execute(s:state['resultswinid'], 'silent! %delete _')
let s:state['highlights'] = []
if s:state['filter'] " if filter is enabled
if empty(s:trim(s:state['input']))
let s:state['fitems'] = s:state['items']
else
if empty(s:state['key']) " item is string
if s:has_matchfuzzypos
let l:matchfuzzyresult = matchfuzzypos(s:state['items'], s:state['input'])
let l:fitems = l:matchfuzzyresult[0]
let l:highlights = l:matchfuzzyresult[1]
let s:state['fitems'] = l:fitems
let s:state['highlights'] = l:highlights
elseif s:has_matchfuzzy
let s:state['fitems'] = matchfuzzy(s:state['items'], s:state['input'])
else
let s:state['fitems'] = filter(copy(s:state['items']), 'stridx(toupper(v:val), toupper(s:state["input"])) >= 0')
endif
else " item is dict
if s:has_matchfuzzypos
" vim requires matchfuzzypos to have highlights.
" matchfuzzy only patch doesn't support dict search
let l:matchfuzzyresult = matchfuzzypos(s:state['items'], s:state['input'], { 'key': s:state['key'] })
let l:fitems = l:matchfuzzyresult[0]
let l:highlights = l:matchfuzzyresult[1]
let s:state['fitems'] = l:fitems
let s:state['highlights'] = l:highlights
else
let s:state['fitems'] = filter(copy(s:state['items']), 'stridx(toupper(v:val[s:state["key"]]), toupper(s:state["input"])) >= 0')
endif
endif
endif
else " if filter is disabled
let s:state['fitems'] = s:state['items']
endif
if empty(s:state['key']) " item is string
let l:lines = s:state['fitems']
else " item is dict
let l:lines = map(copy(s:state['fitems']), 'v:val[s:state["key"]]')
endif
call setbufline(s:state['resultsbufnr'], 1, l:lines)
if s:has_proptype && !empty(s:state['highlights'])
let l:i = 0
for l:line in s:state['highlights']
for l:pos in l:line
let l:cs = split(getbufline(s:state['resultsbufnr'], l:i + 1)[0], '\zs')
let l:mpos = strlen(join(l:cs[: l:pos - 1], ''))
let l:len = strlen(l:cs[l:pos])
call prop_add(l:i + 1, l:mpos + 1, { 'length': l:len, 'type': 'highlight', 'bufnr': s:state['resultsbufnr'] })
endfor
let l:i += 1
endfor
endif
call s:win_execute(s:state['resultswinid'], printf('resize %d', min([len(s:state['fitems']), s:state['maxheight']])))
call s:win_execute(s:state['promptwinid'], 'resize 1')
endfunction
function! s:on_accept() abort
if win_gotoid(s:state['resultswinid'])
let l:index = line('.') - 1 " line is 1 index, list is 0 index
let l:fitems = s:state['fitems']
if l:index < 0 || len(l:fitems) <= l:index
let l:items = []
else
let l:items = [l:fitems[l:index]]
endif
call win_gotoid(s:state['winid'])
call s:notify('accept', { 'items': l:items })
end
endfunction
function! s:on_cancel() abort
call win_gotoid(s:state['winid'])
call s:notify('cancel', {})
call lsp#internal#ui#quickpick#close()
endfunction
function! s:on_move_next() abort
let l:col = col('.')
call s:win_execute(s:state['resultswinid'], 'normal! j')
call s:notify_selection()
endfunction
function! s:on_move_previous() abort
let l:col = col('.')
call s:win_execute(s:state['resultswinid'], 'normal! k')
call s:notify_selection()
endfunction
function! s:notify_items() abort
" items could be huge, so don't send the items as part of data
call s:notify('items', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'], 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['resultswinid'] })
endfunction
function! s:notify_selection() abort
let l:original_winid = win_getid()
call win_gotoid(s:state['resultswinid'])
let l:index = line('.') - 1 " line is 1 based, list is 0 based
if l:index < 0 || ((l:index + 1) > len(s:state['fitems']))
let l:items = []
else
let l:items = [s:state['fitems'][l:index]]
endif
let l:data = {
\ 'bufnr': s:state['bufnr'],
\ 'winid': s:state['winid'],
\ 'resultsbufnr': s:state['resultsbufnr'],
\ 'resultswinid': s:state['resultswinid'],
\ 'items': l:items,
\ }
noautocmd call win_gotoid(s:state['winid'])
call s:notify('selection', l:data)
noautocmd call win_gotoid(l:original_winid)
endfunction
function! s:on_inputchanged() abort
if s:inputecharpre
if s:has_timer && s:state['debounce'] > 0
call s:debounce_onchange()
else
call s:notify_onchange()
endif
endif
endfunction
function! s:on_insertcharpre() abort
let s:inputecharpre = 1
endfunction
function! s:on_insertenter() abort
let s:inputecharpre = 0
endfunction
function! s:on_insertleave() abort
if s:has_timer && has_key(s:state, 'debounce_onchange_timer')
call timer_stop(s:state['debounce_onchange_timer'])
call remove(s:state, 'debounce_onchange_timer')
endif
endfunction
function! s:debounce_onchange() abort
if has_key(s:state, 'debounce_onchange_timer')
call timer_stop(s:state['debounce_onchange_timer'])
call remove(s:state, 'debounce_onchange_timer')
endif
let s:state['debounce_onchange_timer'] = timer_start(s:state['debounce'], function('s:notify_onchange'))
endfunction
function! s:notify_onchange(...) abort
let s:state['input'] = getbufline(s:state['promptbufnr'], 1)[0]
call s:notify('change', { 'input': s:state['input'] })
if s:state['filter']
call s:update_items()
call s:notify_selection()
endif
endfunction
function! s:notify(name, data) abort
if has_key(s:state, 'on_event') | call s:state['on_event'](a:data, a:name) | endif
if has_key(s:state, 'on_' . a:name) | call s:state['on_' . a:name](a:data, a:name) | endif
endfunction
if exists('*win_execute')
function! s:win_execute(win_id, cmd) abort
call win_execute(a:win_id, a:cmd)
endfunction
else
function! s:win_execute(winid, cmd) abort
let l:original_winid = win_getid()
if l:original_winid == a:winid
exec a:cmd
else
if win_gotoid(a:winid)
exec a:cmd
call win_gotoid(l:original_winid)
end
endif
endfunction
endif
if exists('*trim')
function! s:trim(str) abort
return trim(a:str)
endfunction
else
function! s:trim(str) abort
return substitute(a:str, '^\s*\|\s*$', '', 'g')
endfunction
endif
" vim: set sw=2 ts=2 sts=2 et tw=78 foldmarker={{{,}}} foldmethod=marker spell:

View File

@@ -0,0 +1,70 @@
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
let s:progress_ui = []
let s:enabled = 0
function! lsp#internal#work_done_progress#_enable() abort
if !g:lsp_work_done_progress_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:progress_ui = []
let s:Dispose = lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'response') && has_key(x['response'], 'method')
\ && x['response']['method'] ==# '$/progress' && has_key(x['response'], 'params')
\ && has_key(x['response']['params'], 'value') && type(x['response']['params']['value']) == type({})}),
\ lsp#callbag#subscribe({'next': {x->s:handle_work_done_progress(x['server'], x['response'])}})
\ )
endfunction
function! s:handle_work_done_progress(server, response) abort
let l:value = a:response['params']['value']
let l:token = a:response['params']['token']
let l:new = {
\ 'server': a:server,
\ 'token': l:token,
\ 'title': '',
\ 'message': '',
\ }
if l:value['kind'] ==# 'end'
let l:new['message'] = ''
let l:new['percentage'] = 100
call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server})
elseif l:value['kind'] ==# 'begin'
let l:new['title'] = l:value['title']
call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server})
call insert(s:progress_ui, l:new)
elseif l:value['kind'] ==# 'report'
let l:new['message'] = get(l:value, 'message', '')
if has_key(l:value, 'percentage')
" l:value['percentage'] is uinteger in specification.
" But some implementation return float. (e.g. clangd11)
" So we round it.
let l:new['percentage'] = float2nr(l:value['percentage'] + 0.5)
endif
let l:idx = match(s:progress_ui, l:token)
let l:new['title'] = s:progress_ui[l:idx]['title']
call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server})
call insert(s:progress_ui, l:new)
endif
doautocmd <nomodeline> User lsp_progress_updated
endfunction
function! lsp#internal#work_done_progress#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
let s:enabled = 0
let s:progress_ui = []
endfunction
function! lsp#internal#work_done_progress#get_progress() abort
return s:progress_ui
endfunction

View File

@@ -0,0 +1,94 @@
" https://microsoft.github.io/language-server-protocol/specification#workspace_symbol
" options - {
" bufnr: bufnr('%') " optional
" server - 'server_name' " optional
" query: '' " optional
" }
function! lsp#internal#workspace_symbol#search#do(options) abort
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)')
endif
if len(l:servers) == 0
echom 'textDocument/workspaceSymbol not supported'
call lsp#utils#error('textDocument/workspaceSymbol not supported')
return
endif
redraw | echo 'Retrieving workspace symbols ...'
let l:TextChangeSubject = lsp#callbag#makeSubject()
" use callbag debounce instead of quickpick debounce
call lsp#internal#ui#quickpick#open({
\ 'items': [],
\ 'input': get(a:options, 'query', ''),
\ 'key': 'text',
\ 'debounce': 0,
\ 'on_change': function('s:on_change', [l:TextChangeSubject]),
\ 'on_accept': function('s:on_accept'),
\ 'on_close': function('s:on_close'),
\ })
let s:Dispose = lsp#callbag#pipe(
\ l:TextChangeSubject,
\ lsp#callbag#debounceTime(250),
\ lsp#callbag#distinctUntilChanged(),
\ lsp#callbag#switchMap({query->
\ lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#tap({_->lsp#internal#ui#quickpick#busy(1)}),
\ lsp#callbag#flatMap({server->
\ lsp#callbag#pipe(
\ lsp#request(server, {
\ 'method': 'workspace/symbol',
\ 'params': {
\ 'query': query
\ }
\ }),
\ lsp#callbag#map({x->{'server': server, 'request': x['request'], 'response': x['response']}}),
\ )
\ }),
\ lsp#callbag#scan({acc, curr->add(acc, curr)}, []),
\ lsp#callbag#tap({x->s:update_ui_items(x)}),
\ lsp#callbag#tap({'complete': {->lsp#internal#ui#quickpick#busy(0)}}),
\ )
\ }),
\ lsp#callbag#subscribe({
\ 'error': {e->s:on_error(e)},
\ }),
\ )
" Notify empty query. Some servers may not return results when query is empty
call l:TextChangeSubject(1, '')
endfunction
function! s:on_change(TextChangeSubject, data, ...) abort
call a:TextChangeSubject(1, a:data['input'])
endfunction
function! s:update_ui_items(x) abort
let l:items = []
for l:i in a:x
let l:items += lsp#ui#vim#utils#symbols_to_loc_list(l:i['server'], l:i)
endfor
call lsp#internal#ui#quickpick#items(l:items)
endfunction
function! s:on_accept(data, name) abort
call lsp#internal#ui#quickpick#close()
call lsp#utils#location#_open_vim_list_item(a:data['items'][0], '')
endfunction
function! s:on_close(...) abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#internal#ui#quickpick#close()
call lsp#log('LspWorkspaceSymbolSearch error', a:e)
endfunction

View File

@@ -0,0 +1,453 @@
" vint: -ProhibitUnusedVariable
" constants {{{
let s:t_dict = type({})
let s:default_completion_item_kinds = {
\ '1': 'text',
\ '2': 'method',
\ '3': 'function',
\ '4': 'constructor',
\ '5': 'field',
\ '6': 'variable',
\ '7': 'class',
\ '8': 'interface',
\ '9': 'module',
\ '10': 'property',
\ '11': 'unit',
\ '12': 'value',
\ '13': 'enum',
\ '14': 'keyword',
\ '15': 'snippet',
\ '16': 'color',
\ '17': 'file',
\ '18': 'reference',
\ '19': 'folder',
\ '20': 'enum member',
\ '21': 'constant',
\ '22': 'struct',
\ '23': 'event',
\ '24': 'operator',
\ '25': 'type parameter',
\ }
let s:completion_item_kinds = {}
let s:completion_status_success = 'success'
let s:completion_status_failed = 'failed'
let s:completion_status_pending = 'pending'
let s:is_user_data_support = has('patch-8.0.1493')
let s:managed_user_data_key_base = 0
let s:managed_user_data_map = {}
" }}}
" completion state
let s:completion = {'counter': 0, 'status': '', 'matches': []}
function! lsp#omni#complete(findstart, base) abort
let l:info = s:find_complete_servers()
if empty(l:info['server_names'])
return a:findstart ? -1 : []
endif
if a:findstart
return col('.')
else
if !g:lsp_async_completion
let s:completion['status'] = s:completion_status_pending
endif
let l:left = strpart(getline('.'), 0, col('.')-1)
" Initialize the default startcol. It will be updated if the completion items has textEdit.
let s:completion['startcol'] = s:get_startcol(l:left, l:info['server_names'])
" The `l:info` variable will be filled with completion results after request was finished.
call s:send_completion_request(l:info)
if g:lsp_async_completion
" If g:lsp_async_completion == v:true, the `s:display_completions` " will be called by `s:send_completion_request`.
redraw
return exists('v:none') ? v:none : []
else
" Wait for finished the textDocument/completion request and then call `s:display_completions` explicitly.
call lsp#utils#_wait(-1, {-> s:completion['status'] isnot# s:completion_status_pending || complete_check()}, 10)
call timer_start(0, { timer -> s:display_completions(timer, l:info) })
return exists('v:none') ? v:none : []
endif
endif
endfunction
function! s:get_filter_label(item) abort
return lsp#utils#_trim(a:item['word'])
endfunction
function! s:prefix_filter(item, last_typed_word) abort
let l:label = s:get_filter_label(a:item)
if g:lsp_ignorecase
return stridx(tolower(l:label), tolower(a:last_typed_word)) == 0
else
return stridx(l:label, a:last_typed_word) == 0
endif
endfunction
function! s:contains_filter(item, last_typed_word) abort
let l:label = s:get_filter_label(a:item)
if g:lsp_ignorecase
return stridx(tolower(l:label), tolower(a:last_typed_word)) >= 0
else
return stridx(l:label, a:last_typed_word) >= 0
endif
endfunction
let s:pair = {
\ '"': '"',
\ '''': '''',
\ '{': '}',
\ '(': ')',
\ '[': ']',
\}
function! s:display_completions(timer, info) abort
" TODO: Allow multiple servers
let l:server_name = a:info['server_names'][0]
let l:server_info = lsp#get_server_info(l:server_name)
let l:current_line = strpart(getline('.'), 0, col('.') - 1)
let l:last_typed_word = strpart(l:current_line, s:completion['startcol'] - 1)
let l:filter = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'filter') ? l:server_info['config']['filter'] : { 'name': 'prefix' }
if l:filter['name'] ==? 'prefix'
let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:prefix_filter(item, l:last_typed_word)})
if has_key(s:pair, l:last_typed_word[0])
let [l:lhs, l:rhs] = [l:last_typed_word[0], s:pair[l:last_typed_word[0]]]
for l:item in s:completion['matches']
let l:str = l:item['word']
if len(l:str) > 1 && l:str[0] ==# l:lhs && l:str[-1:] ==# l:rhs
let l:item['word'] = l:str[:-2]
endif
endfor
endif
elseif l:filter['name'] ==? 'contains'
let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:contains_filter(item, l:last_typed_word)})
endif
let s:completion['status'] = ''
if mode() is# 'i'
call complete(s:completion['startcol'], s:completion['matches'])
endif
endfunction
function! s:handle_omnicompletion(server_name, complete_counter, info, data) abort
if s:completion['counter'] != a:complete_counter
" ignore old completion results
return
endif
if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result')
let s:completion['status'] = s:completion_status_failed
return
endif
let l:result = s:get_completion_result(a:server_name, a:data)
let s:completion['matches'] = l:result['items']
let s:completion['startcol'] = min([l:result['startcol'], s:completion['startcol']])
let s:completion['status'] = s:completion_status_success
if g:lsp_async_completion
call s:display_completions(0, a:info)
endif
endfunction
function! lsp#omni#get_kind_text(completion_item, ...) abort
let l:server = get(a:, 1, '')
if empty(l:server) " server name
let l:completion_item_kinds = s:default_completion_item_kinds
else
if !has_key(s:completion_item_kinds, l:server)
let l:server_info = lsp#get_server_info(l:server)
if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'completion_item_kinds')
let s:completion_item_kinds[l:server] = extend(copy(s:default_completion_item_kinds), l:server_info['config']['completion_item_kinds'])
else
let s:completion_item_kinds[l:server] = s:default_completion_item_kinds
endif
endif
let l:completion_item_kinds = s:completion_item_kinds[l:server]
endif
return has_key(a:completion_item, 'kind') && has_key(l:completion_item_kinds, a:completion_item['kind'])
\ ? l:completion_item_kinds[a:completion_item['kind']] : ''
endfunction
function! s:get_kind_text_mappings(server) abort
let l:server_name = a:server['name']
if has_key(s:completion_item_kinds, l:server_name)
return s:completion_item_kinds[l:server_name]
else
if has_key(a:server, 'config') && has_key(a:server['config'], 'completion_item_kinds')
let s:completion_item_kinds[l:server_name] = extend(copy(s:default_completion_item_kinds), a:server['config']['completion_item_kinds'])
else
let s:completion_item_kinds[l:server_name] = s:default_completion_item_kinds
endif
return s:completion_item_kinds[l:server_name]
endif
endfunction
" auxiliary functions {{{
function! s:find_complete_servers() abort
let l:server_names = []
for l:server_name in lsp#get_allowed_servers()
let l:init_capabilities = lsp#get_server_capabilities(l:server_name)
if has_key(l:init_capabilities, 'completionProvider')
" TODO: support triggerCharacters
call add(l:server_names, l:server_name)
endif
endfor
return { 'server_names': l:server_names }
endfunction
function! s:send_completion_request(info) abort
let s:completion['counter'] = s:completion['counter'] + 1
let l:server_name = a:info['server_names'][0]
" TODO: support multiple servers
call lsp#send_request(l:server_name, {
\ 'method': 'textDocument/completion',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ 'context': { 'triggerKind': 1 },
\ },
\ 'on_notification': function('s:handle_omnicompletion', [l:server_name, s:completion['counter'], a:info]),
\ })
endfunction
function! s:get_completion_result(server_name, data) abort
let l:result = a:data['response']['result']
let l:options = {
\ 'server': lsp#get_server_info(a:server_name),
\ 'position': lsp#get_position(),
\ 'response': a:data['response'],
\ }
return lsp#omni#get_vim_completion_items(l:options)
endfunction
function! s:sort_by_sorttext(i1, i2) abort
let l:text1 = get(a:i1, 'sortText')
let l:text2 = get(a:i2, 'sortText')
" sortText is possibly empty string
let l:text1 = !empty(l:text1) ? l:text1 : a:i1['label']
let l:text2 = !empty(l:text2) ? l:text2 : a:i2['label']
if g:lsp_ignorecase
return l:text1 ==? l:text2 ? 0 : l:text1 >? l:text2 ? 1 : -1
else
return l:text1 ==# l:text2 ? 0 : l:text1 ># l:text2 ? 1 : -1
endif
endfunction
" Create vim's completed items from LSP response.
"
" options = {
" server: {}, " needs to be server_info and not server_name
" position: lsp#get_position(),
" response: {}, " needs to be the entire lsp response. errors need to be
" handled before calling the fuction
" }
"
" * The returned` startcol` may be the same as the cursor position, in which case you need to decide which one to use.
"
" @return { 'items': v:completed_item[], 'incomplete': v:t_bool, 'startcol': number }
"
function! lsp#omni#get_vim_completion_items(options) abort
let l:server = a:options['server']
let l:server_name = l:server['name']
let l:kind_text_mappings = s:get_kind_text_mappings(l:server)
let l:complete_position = a:options['position']
let l:current_line = getline('.')
let l:default_startcol = s:get_startcol(strcharpart(l:current_line, 0, l:complete_position['character']), [l:server_name])
let l:default_start_character = strchars(strpart(l:current_line, 0, l:default_startcol - 1))
let l:refresh_pattern = s:get_refresh_pattern([l:server_name])
let l:result = a:options['response']['result']
if type(l:result) == type([])
let l:items = l:result
let l:incomplete = 0
elseif type(l:result) == type({})
let l:items = l:result['items']
let l:incomplete = l:result['isIncomplete']
else
let l:items = []
let l:incomplete = 0
endif
let l:sort = has_key(l:server, 'config') && has_key(l:server['config'], 'sort') ? l:server['config']['sort'] : v:null
if len(l:items) > 0 && type(l:sort) == s:t_dict && len(l:items) <= l:sort['max']
" If first item contains sortText, maybe we can use sortText
call sort(l:items, function('s:sort_by_sorttext'))
endif
let l:start_character = l:complete_position['character']
let l:start_characters = [] " The mapping of item specific start_character.
let l:vim_complete_items = []
for l:completion_item in l:items
let l:expandable = get(l:completion_item, 'insertTextFormat', 1) == 2
let l:vim_complete_item = {
\ 'kind': get(l:kind_text_mappings, get(l:completion_item, 'kind', '') , ''),
\ 'dup': 1,
\ 'empty': 1,
\ 'icase': 1,
\ }
let l:range = lsp#utils#text_edit#get_range(get(l:completion_item, 'textEdit', {}))
if has_key(l:completion_item, 'textEdit') && type(l:completion_item['textEdit']) == s:t_dict && !empty(l:range) && has_key(l:completion_item['textEdit'], 'newText')
let l:text_edit_new_text = l:completion_item['textEdit']['newText']
if has_key(l:completion_item, 'filterText') && !empty(l:completion_item['filterText']) && matchstr(l:text_edit_new_text, '^' . l:refresh_pattern) ==# ''
" Use filterText as word.
let l:vim_complete_item['word'] = l:completion_item['filterText']
else
" Use textEdit.newText as word.
let l:vim_complete_item['word'] = l:text_edit_new_text
endif
" Fix overlapped text if needed.
let l:item_start_character = l:range['start']['character']
if l:item_start_character < l:default_start_character
" Add already typed word. The typescript-language-server returns `[Symbol]` item for the line of `Hoo.|`. So we should add `.` (`.[Symbol]`) .
let l:overlap_text = strcharpart(l:current_line, l:item_start_character, l:default_start_character - l:item_start_character)
if stridx(l:vim_complete_item['word'], l:overlap_text) != 0
let l:vim_complete_item['word'] = l:overlap_text . l:vim_complete_item['word']
endif
endif
let l:start_character = min([l:item_start_character, l:start_character])
let l:start_characters += [l:item_start_character]
elseif has_key(l:completion_item, 'insertText') && !empty(l:completion_item['insertText'])
let l:vim_complete_item['word'] = l:completion_item['insertText']
let l:start_characters += [l:default_start_character]
else
let l:vim_complete_item['word'] = l:completion_item['label']
let l:start_characters += [l:default_start_character]
endif
if l:expandable
let l:vim_complete_item['word'] = lsp#utils#make_valid_word(substitute(l:vim_complete_item['word'], '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g'))
let l:vim_complete_item['abbr'] = l:completion_item['label'] . '~'
else
let l:vim_complete_item['abbr'] = l:completion_item['label']
endif
if s:is_user_data_support
let l:vim_complete_item['user_data'] = s:create_user_data(l:completion_item, l:server_name, l:complete_position, l:start_characters[len(l:start_characters) - 1])
endif
let l:vim_complete_items += [l:vim_complete_item]
endfor
" Add the additional text for startcol correction.
if l:start_character != l:default_start_character
for l:i in range(len(l:start_characters))
let l:item_start_character = l:start_characters[l:i]
if l:start_character < l:item_start_character
let l:item = l:vim_complete_items[l:i]
let l:item['word'] = strcharpart(l:current_line, l:start_character, l:item_start_character - l:start_character) . l:item['word']
endif
endfor
endif
let l:startcol = lsp#utils#position#lsp_character_to_vim('%', { 'line': l:complete_position['line'], 'character': l:start_character })
return { 'items': l:vim_complete_items, 'incomplete': l:incomplete, 'startcol': l:startcol }
endfunction
"
" Clear internal user_data map.
"
" This function should call at `CompleteDone` only if not empty `v:completed_item`.
"
function! lsp#omni#_clear_managed_user_data_map() abort
let s:managed_user_data_key_base = 0
let s:managed_user_data_map = {}
endfunction
"
" create item's user_data.
"
function! s:create_user_data(completion_item, server_name, complete_position, start_character) abort
let l:user_data_key = s:create_user_data_key(s:managed_user_data_key_base)
let s:managed_user_data_map[l:user_data_key] = {
\ 'complete_position': a:complete_position,
\ 'server_name': a:server_name,
\ 'completion_item': a:completion_item,
\ 'start_character': a:start_character,
\ }
let s:managed_user_data_key_base += 1
return l:user_data_key
endfunction
function! lsp#omni#get_managed_user_data_from_completed_item(completed_item) abort
" the item has no user_data.
if !has_key(a:completed_item, 'user_data')
return {}
endif
let l:user_data_string = get(a:completed_item, 'user_data', '')
if type(l:user_data_string) != type('')
return {}
endif
" Check managed user_data.
if has_key(s:managed_user_data_map, l:user_data_string)
return s:managed_user_data_map[l:user_data_string]
endif
" Check json.
if stridx(l:user_data_string, '"vim-lsp/key"') != -1
try
let l:user_data = json_decode(l:user_data_string)
if has_key(l:user_data, 'vim-lsp/key')
let l:user_data_key = s:create_user_data_key(l:user_data['vim-lsp/key'])
if has_key(s:managed_user_data_map, l:user_data_key)
return s:managed_user_data_map[l:user_data_key]
endif
endif
catch /.*/
endtry
endif
return {}
endfunction
function! lsp#omni#get_completion_item_kinds() abort
return map(keys(s:default_completion_item_kinds), {idx, key -> str2nr(key)})
endfunction
function! s:create_user_data_key(base) abort
return '{"vim-lsp/key":"' . a:base . '"}'
endfunction
function! s:get_startcol(left, server_names) abort
" Initialize the default startcol. It will be updated if the completion items has textEdit.
let l:startcol = 1 + matchstrpos(a:left, s:get_refresh_pattern(a:server_names))[1]
return l:startcol == 0 ? strlen(a:left) + 1 : l:startcol
endfunction
function! s:get_refresh_pattern(server_names) abort
for l:server_name in a:server_names
let l:server_info = lsp#get_server_info(l:server_name)
if has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'refresh_pattern')
return l:server_info['config']['refresh_pattern']
endif
endfor
return '\(\k\+$\)'
endfunction
" }}}

View File

@@ -0,0 +1,157 @@
let s:tag_kind_priority = ['definition', 'declaration', 'implementation', 'type definition']
function! s:not_supported(what) abort
call lsp#log(a:what . ' not supported for ' . &filetype)
endfunction
function! s:location_to_tag(loc) abort
if has_key(a:loc, 'targetUri')
let l:uri = a:loc['targetUri']
let l:range = a:loc['targetRange']
else
let l:uri = a:loc['uri']
let l:range = a:loc['range']
endif
if !lsp#utils#is_file_uri(l:uri)
return v:null
endif
let l:path = lsp#utils#uri_to_path(l:uri)
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start'])
return {
\ 'filename': l:path,
\ 'cmd': printf('/\%%%dl\%%%dc/', l:line, l:col)
\ }
endfunction
function! s:handle_locations(ctx, server, type, data) abort
try
if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result')
call lsp#utils#error('Failed to retrieve ' . a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
let l:list = a:ctx['list']
let l:result = a:data['response']['result']
if type(l:result) != type([])
let l:result = [l:result]
endif
for l:loc in l:result
let l:tag = s:location_to_tag(l:loc)
if !empty(l:tag)
call add(l:list, extend(l:tag, { 'name': a:ctx['pattern'], 'kind': a:type }))
endif
endfor
finally
let a:ctx['counter'] -= 1
endtry
endfunction
function! s:handle_symbols(ctx, server, data) abort
try
if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result')
call lsp#utils#error('Failed to retrieve workspace symbols for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
let l:list = a:ctx['list']
for l:symbol in a:data['response']['result']
let l:tag = s:location_to_tag(l:symbol['location'])
if empty(l:tag)
continue
endif
let l:tag['name'] = l:symbol['name']
if has_key(l:symbol, 'kind')
let l:tag['kind'] = lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind'])
endif
call add(l:list, l:tag)
endfor
finally
let a:ctx['counter'] -= 1
endtry
endfunction
function! s:tag_view_sub(ctx, method, params) abort
let l:operation = substitute(a:method, '\u', ' \l\0', 'g')
let l:capabilities_func = printf('lsp#capabilities#has_%s_provider(v:val)', substitute(l:operation, ' ', '_', 'g'))
let l:servers = filter(lsp#get_allowed_servers(), l:capabilities_func)
if empty(l:servers)
call s:not_supported('retrieving ' . l:operation)
return v:false
endif
let a:ctx['counter'] += len(l:servers)
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/'.a:method,
\ 'params': a:params,
\ 'on_notification': function('s:handle_locations', [a:ctx, l:server, l:operation])
\})
endfor
return v:true
endfunction
function! s:tag_view(ctx) abort
let l:params = {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ }
return !empty(filter(copy(g:lsp_tagfunc_source_methods),
\ {_, m -> s:tag_view_sub(a:ctx, m, l:params)}))
endfunction
function! s:tag_search(ctx) abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_workspace_symbol_provider(v:val)')
if empty(l:servers)
call s:not_supported('retrieving workspace symbols')
return v:false
endif
let a:ctx['counter'] = len(l:servers)
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'workspace/symbol',
\ 'params': { 'query': a:ctx['pattern'] },
\ 'on_notification': function('s:handle_symbols', [a:ctx, l:server])
\ })
endfor
return v:true
endfunction
function! s:compare_tags(path, a, b) abort
" TODO: custom tag sorting, maybe?
if a:a['filename'] !=# a:b['filename']
if a:a['filename'] ==# a:path
return -1
elseif a:b['filename'] ==# a:path
return 1
endif
endif
let l:rank_a = index(s:tag_kind_priority, get(a:a, 'kind', ''))
let l:rank_b = index(s:tag_kind_priority, get(a:b, 'kind', ''))
if l:rank_a != l:rank_b
return l:rank_a < l:rank_b ? -1 : 1
endif
if a:a['filename'] !=# a:b['filename']
return a:a['filename'] <# a:b['filename'] ? -1 : 1
endif
return str2nr(a:a['cmd']) - str2nr(a:b['cmd'])
endfunction
function! lsp#tag#tagfunc(pattern, flags, info) abort
if stridx(a:flags, 'i') >= 0
return v:null
endif
let l:ctx = { 'pattern': a:pattern, 'counter': 0, 'list': [] }
if !(stridx(a:flags, 'c') >= 0 ? s:tag_view(l:ctx) : s:tag_search(l:ctx))
" No supported methods so use builtin tag source
return v:null
endif
call lsp#utils#_wait(-1, {-> l:ctx['counter'] == 0}, 50)
call sort(l:ctx['list'], function('s:compare_tags', [a:info['buf_ffname']]))
return l:ctx['list']
endfunction

View File

@@ -0,0 +1,497 @@
function! s:not_supported(what) abort
return lsp#utils#error(printf("%s not supported for filetype '%s'", a:what, &filetype))
endfunction
function! lsp#ui#vim#implementation(in_preview, ...) abort
let l:ctx = { 'in_preview': a:in_preview }
if a:0
let l:ctx['mods'] = a:1
endif
call s:list_location('implementation', l:ctx)
endfunction
function! lsp#ui#vim#type_definition(in_preview, ...) abort
let l:ctx = { 'in_preview': a:in_preview }
if a:0
let l:ctx['mods'] = a:1
endif
call s:list_location('typeDefinition', l:ctx)
endfunction
function! lsp#ui#vim#declaration(in_preview, ...) abort
let l:ctx = { 'in_preview': a:in_preview }
if a:0
let l:ctx['mods'] = a:1
endif
call s:list_location('declaration', l:ctx)
endfunction
function! lsp#ui#vim#definition(in_preview, ...) abort
let l:ctx = { 'in_preview': a:in_preview }
if a:0
let l:ctx['mods'] = a:1
endif
call s:list_location('definition', l:ctx)
endfunction
function! lsp#ui#vim#references() abort
let l:ctx = { 'jump_if_one': 0 }
let l:request_params = { 'context': { 'includeDeclaration': v:false } }
call s:list_location('references', l:ctx, l:request_params)
endfunction
function! s:list_location(method, ctx, ...) abort
" typeDefinition => type definition
let l:operation = substitute(a:method, '\u', ' \l\0', 'g')
let l:capabilities_func = printf('lsp#capabilities#has_%s_provider(v:val)', substitute(l:operation, ' ', '_', 'g'))
let l:servers = filter(lsp#get_allowed_servers(), l:capabilities_func)
let l:command_id = lsp#_new_command()
let l:ctx = extend({ 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id, 'jump_if_one': 1, 'mods': '', 'in_preview': 0 }, a:ctx)
if len(l:servers) == 0
call s:not_supported('Retrieving ' . l:operation)
return
endif
let l:params = {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ }
if a:0
call extend(l:params, a:1)
endif
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/' . a:method,
\ 'params': l:params,
\ 'on_notification': function('s:handle_location', [l:ctx, l:server, l:operation]),
\ })
endfor
echo printf('Retrieving %s ...', l:operation)
endfunction
function! s:rename(server, new_name, pos) abort
if empty(a:new_name)
echo '... Renaming aborted ...'
return
endif
" needs to flush existing open buffers
call lsp#send_request(a:server, {
\ 'method': 'textDocument/rename',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': a:pos,
\ 'newName': a:new_name,
\ },
\ 'on_notification': function('s:handle_workspace_edit', [a:server, lsp#_last_command(), 'rename']),
\ })
echo ' ... Renaming ...'
endfunction
function! lsp#ui#vim#rename() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_rename_prepare_provider(v:val)')
let l:prepare_support = 1
if len(l:servers) == 0
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_rename_provider(v:val)')
let l:prepare_support = 0
endif
let l:command_id = lsp#_new_command()
if len(l:servers) == 0
call s:not_supported('Renaming')
return
endif
" TODO: ask the user which server it should use to rename if there are multiple
let l:server = l:servers[0]
if l:prepare_support
call lsp#send_request(l:server, {
\ 'method': 'textDocument/prepareRename',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ },
\ 'on_notification': function('s:handle_rename_prepare', [l:server, l:command_id, 'rename_prepare', expand('<cword>'), lsp#get_position()]),
\ })
return
endif
call s:rename(l:server, input('new name: ', expand('<cword>')), lsp#get_position())
endfunction
function! lsp#ui#vim#stop_server(...) abort
let l:name = get(a:000, 0, '')
for l:server in lsp#get_allowed_servers()
if !empty(l:name) && l:server != l:name
continue
endif
echo 'Stopping' l:server 'server ...'
call lsp#stop_server(l:server)
endfor
endfunction
function! lsp#ui#vim#workspace_symbol(query) abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_workspace_symbol_provider(v:val)')
let l:command_id = lsp#_new_command()
if len(l:servers) == 0
call s:not_supported('Retrieving workspace symbols')
return
endif
if !empty(a:query)
let l:query = a:query
else
let l:query = inputdialog('query>', '', "\<ESC>")
if l:query ==# "\<ESC>"
return
endif
endif
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'workspace/symbol',
\ 'params': {
\ 'query': l:query,
\ },
\ 'on_notification': function('s:handle_symbol', [l:server, l:command_id, 'workspaceSymbol']),
\ })
endfor
redraw
echo 'Retrieving workspace symbols ...'
endfunction
function! lsp#ui#vim#document_symbol() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)')
let l:command_id = lsp#_new_command()
if len(l:servers) == 0
call s:not_supported('Retrieving symbols')
return
endif
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/documentSymbol',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ },
\ 'on_notification': function('s:handle_symbol', [l:server, l:command_id, 'documentSymbol']),
\ })
endfor
echo 'Retrieving document symbols ...'
endfunction
function! s:handle_symbol(server, last_command_id, type, data) abort
if a:last_command_id != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
let l:list = lsp#ui#vim#utils#symbols_to_loc_list(a:server, a:data)
if has('patch-8.2.2147')
call setqflist(l:list)
call setqflist([], 'a', {'title': a:type})
else
call setqflist([])
call setqflist(l:list)
endif
if empty(l:list)
call lsp#utils#error('No ' . a:type .' found')
else
echo 'Retrieved ' . a:type
botright copen
endif
endfunction
function! s:handle_location(ctx, server, type, data) abort "ctx = {counter, list, last_command_id, jump_if_one, mods, in_preview}
if a:ctx['last_command_id'] != lsp#_last_command()
return
endif
let a:ctx['counter'] = a:ctx['counter'] - 1
if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result')
call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
else
let a:ctx['list'] = a:ctx['list'] + lsp#utils#location#_lsp_to_vim_list(a:data['response']['result'])
endif
if a:ctx['counter'] == 0
if empty(a:ctx['list'])
call lsp#utils#error('No ' . a:type .' found')
else
call lsp#utils#tagstack#_update()
let l:loc = a:ctx['list'][0]
if len(a:ctx['list']) == 1 && a:ctx['jump_if_one'] && !a:ctx['in_preview']
call lsp#utils#location#_open_vim_list_item(l:loc, a:ctx['mods'])
echo 'Retrieved ' . a:type
redraw
elseif !a:ctx['in_preview']
call setqflist([])
call setqflist(a:ctx['list'])
echo 'Retrieved ' . a:type
botright copen
else
let l:lines = readfile(l:loc['filename'])
if has_key(l:loc,'viewstart') " showing a locationLink
let l:view = l:lines[l:loc['viewstart'] : l:loc['viewend']]
call lsp#ui#vim#output#preview(a:server, l:view, {
\ 'statusline': ' LSP Peek ' . a:type,
\ 'filetype': &filetype
\ })
else " showing a location
call lsp#ui#vim#output#preview(a:server, l:lines, {
\ 'statusline': ' LSP Peek ' . a:type,
\ 'cursor': { 'line': l:loc['lnum'], 'col': l:loc['col'], 'align': g:lsp_peek_alignment },
\ 'filetype': &filetype
\ })
endif
endif
endif
endif
endfunction
function! s:handle_rename_prepare(server, last_command_id, type, cword, position, data) abort
if a:last_command_id != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
let l:result = a:data['response']['result']
" Check response: null.
if empty(l:result)
echo 'The ' . a:server . ' returns for ' . a:type . ' (The rename request may be invalid at the given position).'
return
endif
" Check response: { defaultBehavior: boolean }.
if has_key(l:result, 'defaultBehavior')
call timer_start(1, {x->s:rename(a:server, input('new name: ', a:cword), a:position)})
return
endif
" Check response: { placeholder: string }
if has_key(l:result, 'placeholder') && !empty(l:result['placeholder'])
call timer_start(1, {x->s:rename(a:server, input('new name: ', a:cword), a:position)})
return
endif
" Check response: { range: Range } | Range
let l:range = get(l:result, 'range', l:result)
let l:lines = getline(1, '$')
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim('%', l:range['start'])
let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim('%', l:range['end'])
if l:start_line ==# l:end_line
let l:name = l:lines[l:start_line - 1][l:start_col - 1 : l:end_col - 2]
else
let l:name = l:lines[l:start_line - 1][l:start_col - 1 :]
for l:i in range(l:start_line, l:end_line - 2)
let l:name .= "\n" . l:lines[l:i]
endfor
if l:end_col - 2 < 0
let l:name .= "\n"
else
let l:name .= l:lines[l:end_line - 1][: l:end_col - 2]
endif
endif
call timer_start(1, {x->s:rename(a:server, input('new name: ', l:name), l:range['start'])})
endfunction
function! s:handle_workspace_edit(server, last_command_id, type, data) abort
if a:last_command_id != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
call lsp#utils#workspace_edit#apply_workspace_edit(a:data['response']['result'])
echo 'Renamed'
endfunction
function! s:handle_text_edit(server, last_command_id, type, data) abort
if a:last_command_id != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
call lsp#utils#text_edit#apply_text_edits(a:data['request']['params']['textDocument']['uri'], a:data['response']['result'])
redraw | echo 'Document formatted'
endfunction
function! lsp#ui#vim#code_action(opts) abort
call lsp#ui#vim#code_action#do(extend({
\ 'sync': v:false,
\ 'selection': v:false,
\ 'query': '',
\ }, a:opts))
endfunction
function! lsp#ui#vim#code_lens() abort
call lsp#ui#vim#code_lens#do({
\ 'sync': v:false,
\ })
endfunction
function! lsp#ui#vim#add_tree_call_hierarchy_incoming() abort
let l:ctx = { 'add_tree': v:true }
call lsp#ui#vim#call_hierarchy_incoming(l:ctx)
endfunction
function! lsp#ui#vim#call_hierarchy_incoming(ctx) abort
let l:ctx = extend({ 'method': 'incomingCalls', 'key': 'from' }, a:ctx)
call s:prepare_call_hierarchy(l:ctx)
endfunction
function! lsp#ui#vim#call_hierarchy_outgoing() abort
let l:ctx = { 'method': 'outgoingCalls', 'key': 'to' }
call s:prepare_call_hierarchy(l:ctx)
endfunction
function! s:prepare_call_hierarchy(ctx) abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_call_hierarchy_provider(v:val)')
let l:command_id = lsp#_new_command()
let l:ctx = extend({ 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id }, a:ctx)
if len(l:servers) == 0
call s:not_supported('Retrieving call hierarchy')
return
endif
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/prepareCallHierarchy',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ },
\ 'on_notification': function('s:handle_prepare_call_hierarchy', [l:ctx, l:server, 'prepare_call_hierarchy']),
\ })
endfor
echo 'Preparing call hierarchy ...'
endfunction
function! s:handle_prepare_call_hierarchy(ctx, server, type, data) abort
if a:ctx['last_command_id'] != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result')
call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
if empty(a:data['response']['result'])
call lsp#utils#warning('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
for l:item in a:data['response']['result']
call s:call_hierarchy(a:ctx, a:server, l:item)
endfor
endfunction
function! s:call_hierarchy(ctx, server, item) abort
call lsp#send_request(a:server, {
\ 'method': 'callHierarchy/' . a:ctx['method'],
\ 'params': {
\ 'item': a:item,
\ },
\ 'on_notification': function('s:handle_call_hierarchy', [a:ctx, a:server, 'call_hierarchy']),
\ })
endfunction
function! s:handle_call_hierarchy(ctx, server, type, data) abort
if a:ctx['last_command_id'] != lsp#_last_command()
return
endif
let a:ctx['counter'] = a:ctx['counter'] - 1
if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result')
call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
elseif a:data['response']['result'] isnot v:null
for l:item in a:data['response']['result']
let l:loc = s:hierarchy_item_to_vim(l:item[a:ctx['key']], a:server)
if l:loc isnot v:null
let a:ctx['list'] += [l:loc]
endif
endfor
endif
if a:ctx['counter'] == 0
if empty(a:ctx['list'])
call lsp#utils#error('No ' . a:type .' found')
else
call lsp#utils#tagstack#_update()
if get(a:ctx, 'add_tree', v:false)
let l:qf = getqflist({'idx' : 0, 'items': []})
let l:pos = l:qf.idx
let l:parent = l:qf.items
let l:level = count(l:parent[l:pos-1].text, g:lsp_tree_incoming_prefix)
let a:ctx['list'] = extend(l:parent, map(a:ctx['list'], 'extend(v:val, {"text": repeat("' . g:lsp_tree_incoming_prefix . '", l:level+1) . v:val.text})'), l:pos)
endif
call setqflist([])
call setqflist(a:ctx['list'])
echo 'Retrieved ' . a:type
botright copen
if get(a:ctx, 'add_tree', v:false)
" move the cursor to the newly added item
execute l:pos + 1
endif
endif
endif
endfunction
function! s:hierarchy_item_to_vim(item, server) abort
let l:uri = a:item['uri']
if !lsp#utils#is_file_uri(l:uri)
return v:null
endif
let l:path = lsp#utils#uri_to_path(l:uri)
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, a:item['range']['start'])
let l:text = '[' . lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, a:item['kind']) . '] ' . a:item['name']
if has_key(a:item, 'detail')
let l:text .= ": " . a:item['detail']
endif
return {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': l:text,
\ }
endfunction

View File

@@ -0,0 +1,207 @@
" vint: -ProhibitUnusedVariable
function! lsp#ui#vim#code_action#complete(input, command, len) abort
let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
let l:kinds = []
for l:server_name in l:server_names
let l:kinds += lsp#capabilities#get_code_action_kinds(l:server_name)
endfor
return filter(copy(l:kinds), { _, kind -> kind =~ '^' . a:input })
endfunction
"
" @param option = {
" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction`
" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre`
" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately.
" ui: 'float' | 'preview'
" }
"
function! lsp#ui#vim#code_action#do(option) abort
let l:selection = get(a:option, 'selection', v:false)
let l:sync = get(a:option, 'sync', v:false)
let l:query = get(a:option, 'query', '')
let l:ui = get(a:option, 'ui', g:lsp_code_action_ui)
if empty(l:ui)
let l:ui = lsp#utils#_has_popup_menu() ? 'float' : 'preview'
endif
let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
if len(l:server_names) == 0
return lsp#utils#error('Code action not supported for ' . &filetype)
endif
if l:selection
let l:range = lsp#utils#range#_get_recent_visual_range()
else
let l:range = lsp#utils#range#_get_current_line_range()
endif
let l:ctx = {
\ 'count': len(l:server_names),
\ 'results': [],
\}
let l:bufnr = bufnr('%')
let l:command_id = lsp#_new_command()
for l:server_name in l:server_names
let l:diagnostic = lsp#internal#diagnostics#under_cursor#get_diagnostic({'server': l:server_name})
call lsp#send_request(l:server_name, {
\ 'method': 'textDocument/codeAction',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': empty(l:diagnostic) || l:selection ? l:range : l:diagnostic['range'],
\ 'context': {
\ 'diagnostics' : empty(l:diagnostic) ? [] : [l:diagnostic],
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
\ },
\ },
\ 'sync': l:sync,
\ 'on_notification': function('s:handle_code_action', [l:ui, l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]),
\ })
endfor
echo 'Retrieving code actions ...'
endfunction
function! s:handle_code_action(ui, ctx, server_name, command_id, sync, query, bufnr, data) abort
" Ignore old request.
if a:command_id != lsp#_last_command()
return
endif
call add(a:ctx['results'], {
\ 'server_name': a:server_name,
\ 'data': a:data,
\})
let a:ctx['count'] -= 1
if a:ctx['count'] ># 0
return
endif
let l:total_code_actions = []
for l:result in a:ctx['results']
let l:server_name = l:result['server_name']
let l:data = l:result['data']
" Check response error.
if lsp#client#is_error(l:data['response'])
call lsp#utils#error('Failed to CodeAction for ' . l:server_name . ': ' . lsp#client#error_message(l:data['response']))
continue
endif
" Check code actions.
let l:code_actions = l:data['response']['result']
" Filter code actions.
if !empty(a:query)
let l:code_actions = filter(l:code_actions, { _, action -> get(action, 'kind', '') =~# '^' . a:query })
endif
if empty(l:code_actions)
continue
endif
for l:code_action in l:code_actions
let l:item = {
\ 'server_name': l:server_name,
\ 'code_action': l:code_action,
\ }
if get(l:code_action, 'isPreferred', v:false)
let l:total_code_actions = [l:item] + l:total_code_actions
else
call add(l:total_code_actions, l:item)
endif
endfor
endfor
if len(l:total_code_actions) == 0
echo 'No code actions found'
return
endif
call lsp#log('s:handle_code_action', l:total_code_actions)
if len(l:total_code_actions) == 1 && !empty(a:query)
let l:action = l:total_code_actions[0]
if s:handle_disabled_action(l:action) | return | endif
" Clear 'Retrieving code actions ...' message
echo ''
call s:handle_one_code_action(l:action['server_name'], a:sync, a:bufnr, l:action['code_action'])
return
endif
" Prompt to choose code actions when empty query provided.
let l:items = []
for l:action in l:total_code_actions
let l:title = printf('[%s] %s', l:action['server_name'], l:action['code_action']['title'])
if has_key(l:action['code_action'], 'kind') && l:action['code_action']['kind'] !=# ''
let l:title .= ' (' . l:action['code_action']['kind'] . ')'
endif
call add(l:items, { 'title': l:title, 'item': l:action })
endfor
if lsp#utils#_has_popup_menu() && a:ui ==? 'float'
call lsp#internal#ui#popupmenu#open({
\ 'title': 'Code actions',
\ 'items': mapnew(l:items, { idx, item -> item.title}),
\ 'pos': 'topleft',
\ 'line': 'cursor+1',
\ 'col': 'cursor',
\ 'callback': funcref('s:popup_accept_code_action', [a:sync, a:bufnr, l:items]),
\ })
else
call lsp#internal#ui#quickpick#open({
\ 'items': l:items,
\ 'key': 'title',
\ 'on_accept': funcref('s:quickpick_accept_code_action', [a:sync, a:bufnr]),
\ })
endif
endfunction
function! s:popup_accept_code_action(sync, bufnr, items, id, selected, ...) abort
if a:selected <= 0 | return | endif
let l:item = a:items[a:selected - 1]['item']
if s:handle_disabled_action(l:item) | return | endif
call s:handle_one_code_action(l:item['server_name'], a:sync, a:bufnr, l:item['code_action'])
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction
function! s:quickpick_accept_code_action(sync, bufnr, data, ...) abort
call lsp#internal#ui#quickpick#close()
if empty(a:data['items']) | return | endif
let l:selected = a:data['items'][0]['item']
if s:handle_disabled_action(l:selected) | return | endif
call s:handle_one_code_action(l:selected['server_name'], a:sync, a:bufnr, l:selected['code_action'])
endfunction
function! s:handle_disabled_action(code_action) abort
if has_key(a:code_action, 'disabled')
echo 'This action is disabled: ' . a:code_action['disabled']['reason']
return v:true
endif
return v:false
endfunction
function! s:handle_one_code_action(server_name, sync, bufnr, command_or_code_action) abort
" has WorkspaceEdit.
if has_key(a:command_or_code_action, 'edit')
call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
endif
" Command.
if has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
call lsp#ui#vim#execute_command#_execute({
\ 'server_name': a:server_name,
\ 'command_name': get(a:command_or_code_action, 'command', ''),
\ 'command_args': get(a:command_or_code_action, 'arguments', v:null),
\ 'sync': a:sync,
\ 'bufnr': a:bufnr,
\ })
" has Command.
elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type({})
call lsp#ui#vim#execute_command#_execute({
\ 'server_name': a:server_name,
\ 'command_name': get(a:command_or_code_action['command'], 'command', ''),
\ 'command_args': get(a:command_or_code_action['command'], 'arguments', v:null),
\ 'sync': a:sync,
\ 'bufnr': a:bufnr,
\ })
endif
endfunction

View File

@@ -0,0 +1,137 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens
" @param option = {
" }
"
function! lsp#ui#vim#code_lens#do(option) abort
let l:sync = get(a:option, 'sync', v:false)
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_lens_provider(v:val)')
if len(l:servers) == 0
return lsp#utils#error('Code lens not supported for ' . &filetype)
endif
redraw | echo 'Retrieving codelens ...'
let l:bufnr = bufnr('%')
call lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#callbag#pipe(
\ lsp#request(server, {
\ 'method': 'textDocument/codeLens',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(l:bufnr),
\ },
\ }),
\ lsp#callbag#map({x->x['response']['result']}),
\ lsp#callbag#filter({codelenses->!empty(codelenses)}),
\ lsp#callbag#flatMap({codelenses->
\ lsp#callbag#pipe(
\ lsp#callbag#fromList(codelenses),
\ lsp#callbag#flatMap({codelens->
\ has_key(codelens, 'command') ? lsp#callbag#of(codelens) : s:resolve_codelens(server, codelens)}),
\ )
\ }),
\ lsp#callbag#map({codelens->{ 'server': server, 'codelens': codelens }}),
\ )
\ }),
\ lsp#callbag#reduce({acc,curr->add(acc, curr)}, []),
\ lsp#callbag#flatMap({x->s:chooseCodeLens(x, l:bufnr)}),
\ lsp#callbag#tap({x-> lsp#ui#vim#execute_command#_execute({
\ 'server_name': x['server'],
\ 'command_name': get(x['codelens']['command'], 'command', ''),
\ 'command_args': get(x['codelens']['command'], 'arguments', v:null),
\ 'bufnr': l:bufnr,
\ })}),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe({
\ 'error': {e->lsp#utils#error('Error running codelens ' . json_encode(e))},
\ }),
\ )
endfunction
function! s:resolve_codelens(server, codelens) abort
" TODO: return callbag#lsp#empty() if codelens resolve not supported by server
return lsp#callbag#pipe(
\ lsp#request(a:server, {
\ 'method': 'codeLens/resolve',
\ 'params': a:codelens
\ }),
\ lsp#callbag#map({x->x['response']['result']}),
\ )
endfunction
function! s:chooseCodeLens(items, bufnr) abort
redraw | echo 'Select codelens:'
if empty(a:items)
return lsp#callbag#throwError('No codelens found')
endif
return lsp#callbag#create(function('s:quickpick_open', [a:items, a:bufnr]))
endfunction
function! lsp#ui#vim#code_lens#_get_subtitle(item) abort
" Since element of arguments property of Command interface is defined as any in LSP spec, it is
" up to the language server implementation.
" Currently this only impacts rust-analyzer. See #1118 for more details.
if !has_key(a:item['codelens']['command'], 'arguments')
return ''
endif
let l:arguments = a:item['codelens']['command']['arguments']
for l:argument in l:arguments
if type(l:argument) != type({}) || !has_key(l:argument, 'label')
return ''
endif
endfor
return ': ' . join(map(copy(l:arguments), 'v:val["label"]'), ' > ')
endfunction
function! s:quickpick_open(items, bufnr, next, error, complete) abort
if empty(a:items)
return lsp#callbag#empty()
endif
let l:items = []
for l:item in a:items
let l:title = printf("[%s] %s%s\t| L%s:%s",
\ l:item['server'],
\ l:item['codelens']['command']['title'],
\ lsp#ui#vim#code_lens#_get_subtitle(l:item),
\ lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['codelens']['range']['start']),
\ getbufline(a:bufnr, lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['codelens']['range']['start']))[0])
call add(l:items, { 'title': l:title, 'item': l:item })
endfor
call lsp#internal#ui#quickpick#open({
\ 'items': l:items,
\ 'key': 'title',
\ 'on_accept': function('s:quickpick_accept', [a:next, a:error, a:complete]),
\ 'on_cancel': function('s:quickpick_cancel', [a:next, a:error, a:complete]),
\ })
return function('s:quickpick_dispose')
endfunction
function! s:quickpick_dispose() abort
call lsp#internal#ui#quickpick#close()
endfunction
function! s:quickpick_accept(next, error, complete, data, ...) abort
call lsp#internal#ui#quickpick#close()
let l:items = a:data['items']
if len(l:items) > 0
call a:next(l:items[0]['item'])
endif
call a:complete()
endfunction
function! s:quickpick_cancel(next, error, complete, ...) abort
call a:complete()
endfunction

View File

@@ -0,0 +1,303 @@
" vint: -ProhibitUnusedVariable
"
let s:context = {}
function! lsp#ui#vim#completion#_setup() abort
augroup lsp_ui_vim_completion
autocmd!
autocmd CompleteDone * call s:on_complete_done()
augroup END
endfunction
function! lsp#ui#vim#completion#_disable() abort
augroup lsp_ui_vim_completion
autocmd!
augroup END
endfunction
"
" After CompleteDone, v:complete_item's word has been inserted into the line.
" Yet not inserted commit characters.
"
" below example uses | as cursor position.
"
" 1. `call getbuf|`<C-x><C-o>
" 2. select `getbufline` item.
" 3. Insert commit characters. e.g. `(`
" 4. fire CompleteDone, then the line is `call getbufline|`
" 5. call feedkeys to call `s:on_complete_done_after`
" 6. then the line is `call getbufline(|` in `s:on_complete_done_after`
"
function! s:on_complete_done() abort
" Sometimes, vim occurs `CompleteDone` unexpectedly.
" We try to detect it by checking empty completed_item.
if empty(v:completed_item) || get(v:completed_item, 'word', '') ==# '' && get(v:completed_item, 'abbr', '') ==# ''
doautocmd <nomodeline> User lsp_complete_done
return
endif
" Try to get managed user_data.
let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(v:completed_item)
" Clear managed user_data.
call lsp#omni#_clear_managed_user_data_map()
" If managed user_data does not exists, skip it.
if empty(l:managed_user_data)
doautocmd <nomodeline> User lsp_complete_done
return
endif
let s:context['done_line'] = getline('.')
let s:context['done_line_nr'] = line('.')
let s:context['completed_item'] = copy(v:completed_item)
let s:context['done_position'] = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
let s:context['complete_position'] = l:managed_user_data['complete_position']
let s:context['server_name'] = l:managed_user_data['server_name']
let s:context['completion_item'] = l:managed_user_data['completion_item']
let s:context['start_character'] = l:managed_user_data['start_character']
call feedkeys(printf("\<C-r>=<SNR>%d_on_complete_done_after()\<CR>", s:SID()), 'n')
endfunction
"
" Apply textEdit or insertText(snippet) and additionalTextEdits.
"
function! s:on_complete_done_after() abort
" Clear message line. feedkeys above leave garbage on message line.
echo ''
" Ignore process if the mode() is not insert-mode after feedkeys.
if mode(1)[0] !=# 'i'
return ''
endif
let l:done_line = s:context['done_line']
let l:done_line_nr = s:context['done_line_nr']
let l:completed_item = s:context['completed_item']
let l:done_position = s:context['done_position']
let l:complete_position = s:context['complete_position']
let l:server_name = s:context['server_name']
let l:completion_item = s:context['completion_item']
let l:start_character = s:context['start_character']
" check the commit characters are <BS> or <C-w>.
if line('.') ==# l:done_line_nr && strlen(getline('.')) < strlen(l:done_line)
doautocmd <nomodeline> User lsp_complete_done
return ''
endif
" Do nothing if text_edit is disabled.
if !g:lsp_text_edit_enabled
doautocmd <nomodeline> User lsp_complete_done
return ''
endif
let l:completion_item = s:resolve_completion_item(l:completion_item, l:server_name)
" clear completed string if need.
let l:is_expandable = s:is_expandable(l:done_line, l:done_position, l:complete_position, l:completion_item, l:completed_item)
if l:is_expandable
call s:clear_auto_inserted_text(l:done_line, l:done_position, l:complete_position)
endif
" apply additionalTextEdits.
if has_key(l:completion_item, 'additionalTextEdits') && !empty(l:completion_item['additionalTextEdits'])
call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), l:completion_item['additionalTextEdits'])
endif
" snippet or textEdit.
if l:is_expandable
" At this timing, the cursor may have been moved by additionalTextEdit, so we use overflow information instead of textEdit itself.
if type(get(l:completion_item, 'textEdit', v:null)) == type({})
let l:range = lsp#utils#text_edit#get_range(l:completion_item['textEdit'])
let l:overflow_before = max([0, l:start_character - l:range['start']['character']])
let l:overflow_after = max([0, l:range['end']['character'] - l:complete_position['character']])
let l:text = l:completion_item['textEdit']['newText']
else
let l:overflow_before = 0
let l:overflow_after = 0
let l:text = s:get_completion_text(l:completion_item)
endif
" apply snipept or text_edit
let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
let l:range = {
\ 'start': {
\ 'line': l:position['line'],
\ 'character': l:position['character'] - (l:complete_position['character'] - l:start_character) - l:overflow_before,
\ },
\ 'end': {
\ 'line': l:position['line'],
\ 'character': l:position['character'] + l:overflow_after,
\ }
\ }
if get(l:completion_item, 'insertTextFormat', 1) == 2
" insert Snippet.
call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': '' }])
call cursor(lsp#utils#position#lsp_to_vim('%', l:range['start']))
if exists('g:lsp_snippet_expand') && len(g:lsp_snippet_expand) > 0
call g:lsp_snippet_expand[0]({ 'snippet': l:text })
else
call s:simple_expand_text(l:text)
endif
else
" apply TextEdit.
call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': l:text }])
" The VSCode always apply completion word as snippet.
" It means we should place cursor to end of new inserted text as snippet does.
let l:lines = lsp#utils#_split_by_eol(l:text)
let l:start = l:range.start
let l:start.line += len(l:lines) - 1
let l:start.character += strchars(l:lines[-1])
call cursor(lsp#utils#position#lsp_to_vim('%', l:start))
endif
endif
doautocmd <nomodeline> User lsp_complete_done
return ''
endfunction
"
" is_expandable
"
function! s:is_expandable(done_line, done_position, complete_position, completion_item, completed_item) abort
if get(a:completion_item, 'textEdit', v:null) isnot# v:null
let l:range = lsp#utils#text_edit#get_range(a:completion_item['textEdit'])
if l:range['start']['line'] != l:range['end']['line']
return v:true
endif
" compute if textEdit will change text.
let l:completed_before = strcharpart(a:done_line, 0, a:complete_position['character'])
let l:completed_after = strcharpart(a:done_line, a:done_position['character'], strchars(a:done_line) - a:done_position['character'])
let l:completed_line = l:completed_before . l:completed_after
let l:text_edit_before = strcharpart(l:completed_line, 0, l:range['start']['character'])
let l:text_edit_after = strcharpart(l:completed_line, l:range['end']['character'], strchars(l:completed_line) - l:range['end']['character'])
return a:done_line !=# l:text_edit_before . s:trim_unmeaning_tabstop(a:completion_item['textEdit']['newText']) . l:text_edit_after
endif
return s:get_completion_text(a:completion_item) !=# s:trim_unmeaning_tabstop(a:completed_item['word'])
endfunction
"
" trim_unmeaning_tabstop
"
function! s:trim_unmeaning_tabstop(text) abort
return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g')
endfunction
"
" Try `completionItem/resolve` if it possible.
"
function! s:resolve_completion_item(completion_item, server_name) abort
" server_name is not provided.
if empty(a:server_name)
return a:completion_item
endif
" check server capabilities.
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !has_key(l:capabilities, 'completionProvider')
\ || type(l:capabilities['completionProvider']) != v:t_dict
\ || !has_key(l:capabilities['completionProvider'], 'resolveProvider')
\ || !l:capabilities['completionProvider']['resolveProvider']
return a:completion_item
endif
let l:ctx = {}
let l:ctx['response'] = {}
function! l:ctx['callback'](data) abort
let l:self['response'] = a:data['response']
endfunction
try
call lsp#send_request(a:server_name, {
\ 'method': 'completionItem/resolve',
\ 'params': a:completion_item,
\ 'sync': 1,
\ 'sync_timeout': g:lsp_completion_resolve_timeout,
\ 'on_notification': function(l:ctx['callback'], [], l:ctx)
\ })
catch /.*/
call lsp#log('s:resolve_completion_item', 'request timeout.')
endtry
if empty(l:ctx['response'])
return a:completion_item
endif
if lsp#client#is_error(l:ctx['response'])
return a:completion_item
endif
if empty(l:ctx['response']['result'])
return a:completion_item
endif
return l:ctx['response']['result']
endfunction
"
" Remove additional inserted text
"
" LSP server knows only `complete_position` so we should remove inserted text until complete_position.
"
function! s:clear_auto_inserted_text(done_line, done_position, complete_position) abort
let l:before = strcharpart(a:done_line, 0, a:complete_position['character'])
let l:after = strcharpart(a:done_line, a:done_position['character'], (strchars(a:done_line) - a:done_position['character']))
call setline('.', l:before . l:after)
call cursor([a:done_position['line'] + 1, strlen(l:before) + 1])
endfunction
"
" Expand text
"
function! s:simple_expand_text(text) abort
let l:pos = {
\ 'line': line('.') - 1,
\ 'character': lsp#utils#to_char('%', line('.'), col('.'))
\ }
" Remove placeholders and get first placeholder position that use to cursor position.
" e.g. `|getbufline(${1:expr}, ${2:lnum})${0}` to getbufline(|,)
let l:text = substitute(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)', '\=substitute(submatch(1), "\\", "", "g")', 'g')
let l:offset = match(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)')
if l:offset == -1
let l:offset = strchars(l:text)
endif
call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{
\ 'range': {
\ 'start': l:pos,
\ 'end': l:pos
\ },
\ 'newText': l:text
\ }])
let l:pos = lsp#utils#position#lsp_to_vim('%', {
\ 'line': l:pos['line'],
\ 'character': l:pos['character'] + l:offset
\ })
call cursor(l:pos)
endfunction
"
" Get completion text from CompletionItem. Fallback to label when insertText
" is falsy
"
function! s:get_completion_text(completion_item) abort
let l:text = get(a:completion_item, 'insertText', '')
if empty(l:text)
let l:text = a:completion_item['label']
endif
return l:text
endfunction
"
" Get script id that uses to call `s:` function in feedkeys.
"
function! s:SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endfunction

View File

@@ -0,0 +1,64 @@
let s:commands = {}
"
" @param {name} = string
" @param {callback} = funcref
"
function! lsp#ui#vim#execute_command#_register(command_name, callback) abort
if has_key(s:commands, a:command_name)
throw printf('lsp#ui#vim#execute_command#_register_command: %s already registered.', a:command_name)
endif
let s:commands[a:command_name] = a:callback
endfunction
"
" TODO: This method does not handle any return value.
"
function! lsp#ui#vim#execute_command#_execute(params) abort
let l:command_name = a:params['command_name']
let l:command_args = get(a:params, 'command_args', v:null)
let l:server_name = get(a:params, 'server_name', '')
let l:bufnr = get(a:params, 'bufnr', -1)
let l:sync = get(a:params, 'sync', v:false)
" create command.
let l:command = { 'command': l:command_name }
if l:command_args isnot v:null
let l:command['arguments'] = l:command_args
endif
" execute command on local.
if has_key(s:commands, l:command_name)
try
call s:commands[l:command_name]({
\ 'bufnr': l:bufnr,
\ 'server_name': l:server_name,
\ 'command': l:command,
\ })
catch /.*/
call lsp#utils#error(printf('Execute command failed: %s', string(a:params)))
endtry
return
endif
" execute command on server.
if !empty(l:server_name)
call lsp#send_request(l:server_name, {
\ 'method': 'workspace/executeCommand',
\ 'params': l:command,
\ 'sync': l:sync,
\ 'on_notification': function('s:handle_execute_command', [l:server_name, l:command]),
\ })
endif
endfunction
"
" handle workspace/executeCommand response
"
function! s:handle_execute_command(server_name, command, data) abort
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Execute command failed on ' . a:server_name . ': ' . string(a:command) . ' -> ' . string(a:data))
endif
endfunction

View File

@@ -0,0 +1,201 @@
let s:folding_ranges = {}
let s:textprop_name = 'vim-lsp-folding-linenr'
function! s:find_servers() abort
return filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_folding_range_provider(v:val)')
endfunction
function! lsp#ui#vim#folding#fold(sync) abort
let l:servers = s:find_servers()
if len(l:servers) == 0
call lsp#utils#error('Folding not supported for ' . &filetype)
return
endif
let l:server = l:servers[0]
call lsp#ui#vim#folding#send_request(l:server, bufnr('%'), a:sync)
endfunction
function! s:set_textprops(buf) abort
" Use zero-width text properties to act as a sort of "mark" in the buffer.
" This is used to remember the line numbers at the time the request was
" sent. We will let Vim handle updating the line numbers when the user
" inserts or deletes text.
" Skip if the buffer doesn't exist. This might happen when a buffer is
" opened and quickly deleted.
if !bufloaded(a:buf) | return | endif
" Create text property, if not already defined
silent! call prop_type_add(s:textprop_name, {'bufnr': a:buf, 'priority': lsp#internal#textprop#priority('folding')})
let l:line_count = s:get_line_count_buf(a:buf)
" First, clear all markers from the previous run
call prop_remove({'type': s:textprop_name, 'bufnr': a:buf}, 1, l:line_count)
" Add markers to each line
let l:i = 1
while l:i <= l:line_count
call prop_add(l:i, 1, {'bufnr': a:buf, 'type': s:textprop_name, 'id': l:i})
let l:i += 1
endwhile
endfunction
function! s:get_line_count_buf(buf) abort
if !has('patch-8.1.1967')
return line('$')
endif
let l:winids = win_findbuf(a:buf)
return empty(l:winids) ? line('$') : line('$', l:winids[0])
endfunction
function! lsp#ui#vim#folding#send_request(server_name, buf, sync) abort
if !lsp#capabilities#has_folding_range_provider(a:server_name)
return
endif
if !g:lsp_fold_enabled
call lsp#log('Skip sending fold request: folding was disabled explicitly')
return
endif
if has('textprop')
call s:set_textprops(a:buf)
endif
call lsp#send_request(a:server_name, {
\ 'method': 'textDocument/foldingRange',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(a:buf)
\ },
\ 'on_notification': function('s:handle_fold_request', [a:server_name]),
\ 'sync': a:sync,
\ 'bufnr': a:buf
\ })
endfunction
function! s:foldexpr(server, buf, linenr) abort
let l:foldlevel = 0
let l:prefix = ''
for l:folding_range in s:folding_ranges[a:server][a:buf]
if type(l:folding_range) == type({}) &&
\ has_key(l:folding_range, 'startLine') &&
\ has_key(l:folding_range, 'endLine')
let l:start = l:folding_range['startLine'] + 1
let l:end = l:folding_range['endLine'] + 1
if (l:start <= a:linenr) && (a:linenr <= l:end)
let l:foldlevel += 1
endif
if l:start == a:linenr
let l:prefix = '>'
elseif l:end == a:linenr
let l:prefix = '<'
endif
endif
endfor
" Only return marker if a fold starts/ends at this line.
" Otherwise, return '='.
return (l:prefix ==# '') ? '=' : (l:prefix . l:foldlevel)
endfunction
" Searches for text property of the correct type on the given line.
" Returns the original linenr on success, or -1 if no textprop of the correct
" type is associated with this line.
function! s:get_textprop_line(linenr) abort
let l:props = filter(prop_list(a:linenr), {idx, prop -> prop['type'] ==# s:textprop_name})
if empty(l:props)
return -1
else
return l:props[0]['id']
endif
endfunction
function! lsp#ui#vim#folding#foldexpr() abort
let l:servers = s:find_servers()
if len(l:servers) == 0
return
endif
let l:server = l:servers[0]
if has('textprop')
" Does the current line have a textprop with original line info?
let l:textprop_line = s:get_textprop_line(v:lnum)
if l:textprop_line == -1
" No information for current line available, so use indent for
" previous line.
return '='
else
" Info available, use foldexpr as it would be with original line
" number
return s:foldexpr(l:server, bufnr('%'), l:textprop_line)
endif
else
return s:foldexpr(l:server, bufnr('%'), v:lnum)
endif
endfunction
function! lsp#ui#vim#folding#foldtext() abort
let l:num_lines = v:foldend - v:foldstart + 1
let l:summary = getline(v:foldstart) . '...'
" Join all lines in the fold
let l:combined_lines = ''
let l:i = v:foldstart
while l:i <= v:foldend
let l:combined_lines .= getline(l:i) . ' '
let l:i += 1
endwhile
" Check if we're in a comment
let l:comment_regex = '\V' . substitute(&l:commentstring, '%s', '\\.\\*', '')
if l:combined_lines =~? l:comment_regex
let l:summary = l:combined_lines
endif
return l:summary . ' (' . l:num_lines . ' ' . (l:num_lines == 1 ? 'line' : 'lines') . ') '
endfunction
function! s:handle_fold_request(server, data) abort
if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result')
return
endif
let l:result = a:data['response']['result']
if type(l:result) != type([])
return
endif
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)
if l:bufnr < 0
return
endif
if !has_key(s:folding_ranges, a:server)
let s:folding_ranges[a:server] = {}
endif
let s:folding_ranges[a:server][l:bufnr] = l:result
" Set 'foldmethod' back to 'expr', which forces a re-evaluation of
" 'foldexpr'. Only do this if the user hasn't changed 'foldmethod',
" and this is the correct buffer.
for l:winid in win_findbuf(l:bufnr)
if getwinvar(l:winid, '&foldmethod') ==# 'expr'
call setwinvar(l:winid, '&foldmethod', 'expr')
endif
endfor
endfunction

View File

@@ -0,0 +1,450 @@
let s:use_vim_popup = has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim')
let s:use_nvim_float = exists('*nvim_open_win') && g:lsp_preview_float && has('nvim')
let s:use_preview = !s:use_vim_popup && !s:use_nvim_float
function! s:import_modules() abort
if exists('s:Markdown') | return | endif
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Text = vital#lsp#import('VS.LSP.Text')
endfunction
let s:winid = v:false
let s:prevwin = v:false
let s:preview_data = v:false
function! s:vim_popup_closed(...) abort
let s:preview_data = v:false
endfunction
function! lsp#ui#vim#output#closepreview() abort
if win_getid() ==# s:winid
" Don't close if window got focus
return
endif
if s:winid == v:false
return
endif
"closing floats in vim8.1 must use popup_close()
"nvim must use nvim_win_close. pclose is not reliable and does not always work
if s:use_vim_popup && s:winid
call popup_close(s:winid)
elseif s:use_nvim_float && s:winid
silent! call nvim_win_close(s:winid, 0)
else
pclose
endif
let s:winid = v:false
let s:preview_data = v:false
augroup lsp_float_preview_close
augroup end
autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
doautocmd <nomodeline> User lsp_float_closed
endfunction
function! lsp#ui#vim#output#focuspreview() abort
if s:is_cmdwin()
return
endif
" This does not work for vim8.1 popup but will work for nvim and old preview
if s:winid
if win_getid() !=# s:winid
let s:prevwin = win_getid()
call win_gotoid(s:winid)
elseif s:prevwin
" Temporarily disable hooks
" TODO: remove this when closing logic is able to distinguish different move directions
autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
call win_gotoid(s:prevwin)
call s:add_float_closing_hooks()
let s:prevwin = v:false
endif
endif
endfunction
function! s:bufwidth() abort
let l:width = winwidth(0)
let l:numberwidth = max([&numberwidth, strlen(line('$'))+1])
let l:numwidth = (&number || &relativenumber)? l:numberwidth : 0
let l:foldwidth = &foldcolumn
if &signcolumn ==? 'yes'
let l:signwidth = 2
elseif &signcolumn ==? 'auto'
let l:signs = execute(printf('sign place buffer=%d', bufnr('')))
let l:signs = split(l:signs, "\n")
let l:signwidth = len(l:signs)>2? 2: 0
else
let l:signwidth = 0
endif
return l:width - l:numwidth - l:foldwidth - l:signwidth
endfunction
function! s:get_float_positioning(height, width) abort
let l:height = a:height
let l:width = a:width
" TODO: add option to configure it 'docked' at the bottom/top/right
" NOTE: screencol() and screenrow() start from (1,1)
" but the popup window co-ordinates start from (0,0)
" Very convenient!
" For a simple single-line 'tooltip', the following
" two lines are enough to determine the position
let l:col = screencol()
let l:row = screenrow()
let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])])
let l:style = 'minimal'
" Positioning is not window but screen relative
let l:opts = {
\ 'relative': 'editor',
\ 'row': l:row,
\ 'col': l:col,
\ 'width': l:width,
\ 'height': l:height,
\ 'style': l:style,
\ }
return l:opts
endfunction
function! lsp#ui#vim#output#floatingpreview(data) abort
if s:use_nvim_float
let l:buf = nvim_create_buf(v:false, v:true)
" Try to get as much space around the cursor, but at least 10x10
let l:width = max([s:bufwidth(), 10])
let l:height = max([&lines - winline() + 1, winline() - 1, 10])
if g:lsp_preview_max_height > 0
let l:height = min([g:lsp_preview_max_height, l:height])
endif
let l:opts = s:get_float_positioning(l:height, l:width)
let s:winid = nvim_open_win(l:buf, v:false, l:opts)
call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu')
call nvim_win_set_option(s:winid, 'foldenable', v:false)
call nvim_win_set_option(s:winid, 'wrap', v:true)
call nvim_win_set_option(s:winid, 'statusline', '')
call nvim_win_set_option(s:winid, 'number', v:false)
call nvim_win_set_option(s:winid, 'relativenumber', v:false)
call nvim_win_set_option(s:winid, 'cursorline', v:false)
call nvim_win_set_option(s:winid, 'cursorcolumn', v:false)
call nvim_win_set_option(s:winid, 'colorcolumn', '')
call nvim_win_set_option(s:winid, 'signcolumn', 'no')
" Enable closing the preview with esc, but map only in the scratch buffer
call nvim_buf_set_keymap(l:buf, 'n', '<esc>', ':pclose<cr>', {'silent': v:true})
elseif s:use_vim_popup
let l:options = {
\ 'moved': 'any',
\ 'border': [1, 1, 1, 1],
\ 'callback': function('s:vim_popup_closed')
\ }
if g:lsp_preview_max_width > 0
let l:options['maxwidth'] = g:lsp_preview_max_width
endif
if g:lsp_preview_max_height > 0
let l:options['maxheight'] = g:lsp_preview_max_height
endif
let s:winid = popup_atcursor('...', l:options)
endif
return s:winid
endfunction
function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort
if s:use_vim_popup
" vim popup
call setbufline(winbufnr(a:winid), 1, a:lines)
call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
elseif s:use_nvim_float
" nvim floating
call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines)
call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true)
call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false)
call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover')
call nvim_win_set_cursor(a:winid, [1, 0])
elseif s:use_preview
" preview window
call setbufline(winbufnr(a:winid), 1, a:lines)
call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
endif
endfunction
function! lsp#ui#vim#output#adjust_float_placement(bufferlines, maxwidth) abort
if s:use_nvim_float
let l:win_config = {}
let l:height = min([winheight(s:winid), a:bufferlines])
let l:width = min([winwidth(s:winid), a:maxwidth])
let l:win_config = s:get_float_positioning(l:height, l:width)
call nvim_win_set_config(s:winid, l:win_config )
endif
endfunction
function! s:add_float_closing_hooks() abort
if g:lsp_preview_autoclose
augroup lsp_float_preview_close
autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
autocmd CursorMoved,CursorMovedI,VimResized * call lsp#ui#vim#output#closepreview()
augroup END
endif
endfunction
function! lsp#ui#vim#output#getpreviewwinid() abort
return s:winid
endfunction
function! s:open_preview(data) abort
if s:use_vim_popup || s:use_nvim_float
let l:winid = lsp#ui#vim#output#floatingpreview(a:data)
else
execute &previewheight.'new'
let l:winid = win_getid()
endif
return l:winid
endfunction
function! s:set_cursor(current_window_id, options) abort
if !has_key(a:options, 'cursor')
return
endif
if s:use_nvim_float
" Neovim floats
" Go back to the preview window to set the cursor
call win_gotoid(s:winid)
let l:old_scrolloff = &scrolloff
let &scrolloff = 0
call nvim_win_set_cursor(s:winid, [a:options['cursor']['line'], a:options['cursor']['col']])
call s:align_preview(a:options)
" Finally, go back to the original window
call win_gotoid(a:current_window_id)
let &scrolloff = l:old_scrolloff
elseif s:use_vim_popup
" Vim popups
function! AlignVimPopup(timer) closure abort
call s:align_preview(a:options)
endfunction
call timer_start(0, function('AlignVimPopup'))
else
" Preview
" Don't use 'scrolloff', it might mess up the cursor's position
let &l:scrolloff = 0
call cursor(a:options['cursor']['line'], a:options['cursor']['col'])
call s:align_preview(a:options)
endif
endfunction
function! s:align_preview(options) abort
if !has_key(a:options, 'cursor') ||
\ !has_key(a:options['cursor'], 'align')
return
endif
let l:align = a:options['cursor']['align']
if s:use_vim_popup
" Vim popups
let l:pos = popup_getpos(s:winid)
let l:below = winline() < winheight(0) / 2
if l:below
let l:height = min([l:pos['core_height'], winheight(0) - winline() - 2])
else
let l:height = min([l:pos['core_height'], winline() - 3])
endif
let l:width = l:pos['core_width']
let l:options = {
\ 'minwidth': l:width,
\ 'maxwidth': l:width,
\ 'minheight': l:height,
\ 'maxheight': l:height,
\ 'pos': l:below ? 'topleft' : 'botleft',
\ 'line': l:below ? 'cursor+1' : 'cursor-1'
\ }
if l:align ==? 'top'
let l:options['firstline'] = a:options['cursor']['line']
elseif l:align ==? 'center'
let l:options['firstline'] = a:options['cursor']['line'] - (l:height - 1) / 2
elseif l:align ==? 'bottom'
let l:options['firstline'] = a:options['cursor']['line'] - l:height + 1
endif
call popup_setoptions(s:winid, l:options)
redraw!
else
" Preview and Neovim floats
if l:align ==? 'top'
normal! zt
elseif l:align ==? 'center'
normal! zz
elseif l:align ==? 'bottom'
normal! zb
endif
endif
endfunction
function! lsp#ui#vim#output#get_size_info(winid) abort
" Get size information while still having the buffer active
let l:buffer = winbufnr(a:winid)
let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)'))
let l:bufferlines = 0
if g:lsp_preview_max_width > 0
let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth])
" Determine, for each line, how many "virtual" lines it spans, and add
" these together for all lines in the buffer
for l:line in getbufline(l:buffer, 1, '$')
let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width)))
let l:bufferlines += max([l:num_lines, 1])
endfor
else
if s:use_vim_popup
let l:bufferlines = line('$', a:winid)
elseif s:use_nvim_float
let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid))
endif
endif
return [l:bufferlines, l:maxwidth]
endfunction
function! lsp#ui#vim#output#float_supported() abort
return s:use_vim_popup || s:use_nvim_float
endfunction
function! lsp#ui#vim#output#preview(server, data, options) abort
if s:is_cmdwin()
return
endif
if s:winid && type(s:preview_data) ==# type(a:data)
\ && s:preview_data ==# a:data
\ && type(g:lsp_preview_doubletap) ==# 3
\ && len(g:lsp_preview_doubletap) >= 1
\ && type(g:lsp_preview_doubletap[0]) ==# 2
\ && index(['i', 's'], mode()[0]) ==# -1
echo ''
return call(g:lsp_preview_doubletap[0], [])
endif
" Close any previously opened preview window
call lsp#ui#vim#output#closepreview()
let l:current_window_id = win_getid()
let s:winid = s:open_preview(a:data)
let s:preview_data = a:data
let l:lines = []
let l:syntax_lines = []
let l:ft = lsp#ui#vim#output#append(a:data, l:lines, l:syntax_lines)
if has_key(a:options, 'filetype')
let l:ft = a:options['filetype']
endif
let l:do_conceal = g:lsp_hover_conceal
let l:server_info = a:server !=# '' ? lsp#get_server_info(a:server) : {}
let l:config = get(l:server_info, 'config', {})
let l:do_conceal = get(l:config, 'hover_conceal', l:do_conceal)
call setbufvar(winbufnr(s:winid), 'lsp_syntax_highlights', l:syntax_lines)
call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal)
call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft)
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid)
if s:use_preview
" Set statusline
if has_key(a:options, 'statusline')
let &l:statusline = a:options['statusline']
endif
call s:set_cursor(l:current_window_id, a:options)
endif
" Go to the previous window to adjust positioning
call win_gotoid(l:current_window_id)
echo ''
if s:winid && (s:use_vim_popup || s:use_nvim_float)
if s:use_nvim_float
" Neovim floats
call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth)
call s:set_cursor(l:current_window_id, a:options)
call s:add_float_closing_hooks()
elseif s:use_vim_popup
" Vim popups
call s:set_cursor(l:current_window_id, a:options)
endif
doautocmd <nomodeline> User lsp_float_opened
endif
if l:ft ==? 'markdown'
call s:import_modules()
call s:Window.do(s:winid, {->s:Markdown.apply()})
endif
if !g:lsp_preview_keep_focus
" set the focus to the preview window
call win_gotoid(s:winid)
endif
return ''
endfunction
function! s:escape_string_for_display(str) abort
return substitute(substitute(a:str, '\r\n', '\n', 'g'), '\r', '\n', 'g')
endfunction
function! lsp#ui#vim#output#append(data, lines, syntax_lines) abort
if type(a:data) == type([])
for l:entry in a:data
call lsp#ui#vim#output#append(l:entry, a:lines, a:syntax_lines)
endfor
return 'markdown'
elseif type(a:data) ==# type('')
call extend(a:lines, split(s:escape_string_for_display(a:data), "\n", v:true))
return 'markdown'
elseif type(a:data) ==# type({}) && has_key(a:data, 'language')
let l:new_lines = split(s:escape_string_for_display(a:data.value), '\n')
let l:i = 1
while l:i <= len(l:new_lines)
call add(a:syntax_lines, { 'line': len(a:lines) + l:i, 'language': a:data.language })
let l:i += 1
endwhile
call extend(a:lines, l:new_lines)
return 'markdown'
elseif type(a:data) ==# type({}) && has_key(a:data, 'kind')
if a:data.kind ==? 'markdown'
call s:import_modules()
let l:detail = s:MarkupContent.normalize(a:data.value, {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
call extend(a:lines, s:Text.split_by_eol(l:detail))
else
call extend(a:lines, split(s:escape_string_for_display(a:data.value), '\n', v:true))
endif
return a:data.kind ==? 'plaintext' ? 'text' : a:data.kind
endif
endfunction
function! s:is_cmdwin() abort
return getcmdwintype() !=# ''
endfunction

View File

@@ -0,0 +1,160 @@
" vint: -ProhibitUnusedVariable
let s:debounce_timer_id = 0
function! s:not_supported(what) abort
return lsp#utils#error(a:what.' not supported for '.&filetype)
endfunction
function! lsp#ui#vim#signature_help#get_signature_help_under_cursor() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_signature_help_provider(v:val)')
if len(l:servers) == 0
call s:not_supported('Retrieving signature help')
return
endif
let l:position = lsp#get_position()
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/signatureHelp',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': l:position,
\ },
\ 'on_notification': function('s:handle_signature_help', [l:server]),
\ })
endfor
call lsp#log('Retrieving signature help')
return
endfunction
function! s:handle_signature_help(server, data) abort
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to retrieve signature help information for ' . a:server)
return
endif
if !has_key(a:data['response'], 'result')
return
endif
if !empty(a:data['response']['result']) && !empty(a:data['response']['result']['signatures'])
" Get current signature.
let l:signatures = get(a:data['response']['result'], 'signatures', [])
let l:signature_index = get(a:data['response']['result'], 'activeSignature', 0)
let l:signature = get(l:signatures, l:signature_index, {})
if empty(l:signature)
return
endif
" Signature label.
let l:label = l:signature['label']
" Mark current parameter.
if has_key(a:data['response']['result'], 'activeParameter')
let l:parameters = get(l:signature, 'parameters', [])
let l:parameter_index = a:data['response']['result']['activeParameter']
let l:parameter = get(l:parameters, l:parameter_index, {})
let l:parameter_label = s:get_parameter_label(l:signature, l:parameter)
if !empty(l:parameter_label)
let l:label = substitute(l:label, '\V\(' . escape(l:parameter_label, '\/?') . '\)', '`\1`', 'g')
endif
endif
let l:contents = [l:label]
if exists('l:parameter')
let l:parameter_doc = s:get_parameter_doc(l:parameter)
if !empty(l:parameter_doc)
call add(l:contents, '')
call add(l:contents, l:parameter_doc)
call add(l:contents, '')
endif
endif
if has_key(l:signature, 'documentation')
call add(l:contents, l:signature['documentation'])
endif
call lsp#ui#vim#output#preview(a:server, l:contents, {'statusline': ' LSP SignatureHelp'})
return
else
" signature help is used while inserting. So this must be graceful.
"call lsp#utils#error('No signature help information found')
endif
endfunction
function! s:get_parameter_label(signature, parameter) abort
if has_key(a:parameter, 'label')
if type(a:parameter['label']) == type([])
let l:string_range = a:parameter['label']
return strcharpart(
\ a:signature['label'],
\ l:string_range[0],
\ l:string_range[1] - l:string_range[0])
endif
return a:parameter['label']
endif
return ''
endfunction
function! s:get_parameter_doc(parameter) abort
if !has_key(a:parameter, 'documentation')
return ''
endif
let l:doc = copy(a:parameter['documentation'])
if type(l:doc) == type({})
let l:doc['value'] = printf('***%s*** - %s', a:parameter['label'], l:doc['value'])
return l:doc
endif
return printf('***%s*** - %s', a:parameter['label'], l:doc)
endfunction
function! s:on_cursor_moved() abort
let l:bufnr = bufnr('%')
call timer_stop(s:debounce_timer_id)
if g:lsp_signature_help_enabled
let s:debounce_timer_id = timer_start(g:lsp_signature_help_delay, function('s:on_text_changed_after', [l:bufnr]), { 'repeat': 1 })
endif
endfunction
function! s:on_text_changed_after(bufnr, timer) abort
if bufnr('%') != a:bufnr
return
endif
if index(['i', 's'], mode()[0]) == -1
return
endif
if win_id2win(lsp#ui#vim#output#getpreviewwinid()) >= 1
return
endif
" Cache trigger chars since this loop is heavy
let l:chars = get(b:, 'lsp_signature_help_trigger_character', [])
if empty(l:chars)
for l:server_name in lsp#get_allowed_servers(a:bufnr)
let l:chars += lsp#capabilities#get_signature_help_trigger_characters(l:server_name)
endfor
let b:lsp_signature_help_trigger_character = l:chars
endif
if index(l:chars, lsp#utils#_get_before_char_skip_white()) >= 0
call lsp#ui#vim#signature_help#get_signature_help_under_cursor()
endif
endfunction
function! lsp#ui#vim#signature_help#setup() abort
augroup _lsp_signature_help_
autocmd!
autocmd CursorMoved,CursorMovedI * call s:on_cursor_moved()
augroup END
endfunction
function! lsp#ui#vim#signature_help#_disable() abort
augroup _lsp_signature_help_
autocmd!
augroup END
endfunction

View File

@@ -0,0 +1,164 @@
let s:default_symbol_kinds = {
\ '1': 'file',
\ '2': 'module',
\ '3': 'namespace',
\ '4': 'package',
\ '5': 'class',
\ '6': 'method',
\ '7': 'property',
\ '8': 'field',
\ '9': 'constructor',
\ '10': 'enum',
\ '11': 'interface',
\ '12': 'function',
\ '13': 'variable',
\ '14': 'constant',
\ '15': 'string',
\ '16': 'number',
\ '17': 'boolean',
\ '18': 'array',
\ '19': 'object',
\ '20': 'key',
\ '21': 'null',
\ '22': 'enum member',
\ '23': 'struct',
\ '24': 'event',
\ '25': 'operator',
\ '26': 'type parameter',
\ }
let s:symbol_kinds = {}
let s:diagnostic_severity = {
\ 1: 'Error',
\ 2: 'Warning',
\ 3: 'Information',
\ 4: 'Hint',
\ }
function! s:symbols_to_loc_list_children(server, path, list, symbols, depth) abort
for l:symbol in a:symbols
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(a:path, l:symbol['range']['start'])
call add(a:list, {
\ 'filename': a:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . printf('%' . a:depth. 's', ' ') . l:symbol['name'],
\ })
if has_key(l:symbol, 'children') && !empty(l:symbol['children'])
call s:symbols_to_loc_list_children(a:server, a:path, a:list, l:symbol['children'], a:depth + 1)
endif
endfor
endfunction
function! lsp#ui#vim#utils#symbols_to_loc_list(server, result) abort
if !has_key(a:result['response'], 'result')
return []
endif
let l:list = []
let l:locations = type(a:result['response']['result']) == type({}) ? [a:result['response']['result']] : a:result['response']['result']
if !empty(l:locations) " some servers also return null so check to make sure it isn't empty
for l:symbol in a:result['response']['result']
if has_key(l:symbol, 'location')
let l:location = l:symbol['location']
if lsp#utils#is_file_uri(l:location['uri'])
let l:path = lsp#utils#uri_to_path(l:location['uri'])
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:location['range']['start'])
call add(l:list, {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . l:symbol['name'],
\ })
endif
else
let l:location = a:result['request']['params']['textDocument']['uri']
if lsp#utils#is_file_uri(l:location)
let l:path = lsp#utils#uri_to_path(l:location)
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:symbol['range']['start'])
call add(l:list, {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . l:symbol['name'],
\ })
if has_key(l:symbol, 'children') && !empty(l:symbol['children'])
call s:symbols_to_loc_list_children(a:server, l:path, l:list, l:symbol['children'], 1)
endif
endif
endif
endfor
endif
return l:list
endfunction
function! lsp#ui#vim#utils#diagnostics_to_loc_list(result) abort
if !has_key(a:result['response'], 'params')
return
endif
let l:uri = a:result['response']['params']['uri']
let l:diagnostics = lsp#utils#iteratable(a:result['response']['params']['diagnostics'])
let l:list = []
if !empty(l:diagnostics) && lsp#utils#is_file_uri(l:uri)
let l:path = lsp#utils#uri_to_path(l:uri)
for l:item in l:diagnostics
let l:severity_text = ''
if has_key(l:item, 'severity') && !empty(l:item['severity'])
let l:severity_text = s:get_diagnostic_severity_text(l:item['severity'])
endif
let l:text = ''
if has_key(l:item, 'source') && !empty(l:item['source'])
let l:text .= l:item['source'] . ':'
endif
if l:severity_text !=# ''
let l:text .= l:severity_text . ':'
endif
if has_key(l:item, 'code') && !empty(l:item['code'])
let l:text .= l:item['code'] . ':'
endif
let l:text .= l:item['message']
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:item['range']['start'])
let l:location_item = {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': l:text,
\ }
if l:severity_text !=# ''
" 'E' for error, 'W' for warning, 'I' for information, 'H' for hint
let l:location_item['type'] = l:severity_text[0]
endif
call add(l:list, l:location_item)
endfor
endif
return l:list
endfunction
function! lsp#ui#vim#utils#_get_symbol_text_from_kind(server, kind) abort
if !has_key(s:symbol_kinds, a:server)
let l:server_info = lsp#get_server_info(a:server)
if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'symbol_kinds')
let s:symbol_kinds[a:server] = extend(copy(s:default_symbol_kinds), l:server_info['config']['symbol_kinds'])
else
let s:symbol_kinds[a:server] = s:default_symbol_kinds
endif
endif
return get(s:symbol_kinds[a:server], a:kind, 'unknown symbol ' . a:kind)
endfunction
function! lsp#ui#vim#utils#get_symbol_kinds() abort
return map(keys(s:default_symbol_kinds), {idx, key -> str2nr(key)})
endfunction
function! s:get_diagnostic_severity_text(severity) abort
return s:diagnostic_severity[a:severity]
endfunction

View File

@@ -0,0 +1,523 @@
let s:has_lua = has('nvim-0.4.0') || (has('lua') && has('patch-8.2.0775'))
function! lsp#utils#has_lua() abort
return s:has_lua
endfunction
let s:has_native_lsp_client = !has('nvim') && has('patch-8.2.4780')
function! lsp#utils#has_native_lsp_client() abort
return s:has_native_lsp_client
endfunction
let s:has_virtual_text = exists('*nvim_buf_set_virtual_text') && exists('*nvim_create_namespace')
function! lsp#utils#_has_nvim_virtual_text() abort
return s:has_virtual_text
endfunction
let s:has_signs = exists('*sign_define') && (has('nvim') || has('patch-8.1.0772'))
function! lsp#utils#_has_signs() abort
return s:has_signs
endfunction
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
" https://github.com/prabirshrestha/vim-lsp/issues/399#issuecomment-500585549
let s:has_textprops = exists('*prop_add') && has('patch-8.1.1035')
function! lsp#utils#_has_textprops() abort
return s:has_textprops
endfunction
let s:has_vim9textprops = exists('*prop_add') && has('patch-9.0.0178')
function! lsp#utils#_has_vim_virtual_text() abort
return s:has_vim9textprops
endfunction
let s:has_prop_remove_types = exists('*prop_remove') && has('patch-9.0.0233')
function! lsp#utils#_has_prop_remove_types() abort
return s:has_prop_remove_types
endfunction
let s:has_higlights = has('nvim') ? lsp#utils#_has_nvim_buf_highlight() : lsp#utils#_has_textprops()
function! lsp#utils#_has_highlights() abort
return s:has_higlights
endfunction
let s:has_popup_menu = exists('*popup_menu')
function! lsp#utils#_has_popup_menu() abort
return s:has_popup_menu
endfunction
function! lsp#utils#is_file_uri(uri) abort
return stridx(a:uri, 'file:///') == 0
endfunction
function! lsp#utils#is_remote_uri(uri) abort
return a:uri =~# '^\w\+::' || a:uri =~# '^[a-z][a-z0-9+.-]*://'
endfunction
function! s:decode_uri(uri) abort
let l:ret = substitute(a:uri, '[?#].*', '', '')
return substitute(l:ret, '%\(\x\x\)', '\=printf("%c", str2nr(submatch(1), 16))', 'g')
endfunction
function! s:urlencode_char(c) abort
return printf('%%%02X', char2nr(a:c))
endfunction
function! s:get_prefix(path) abort
return matchstr(a:path, '\(^\w\+::\|^\w\+://\)')
endfunction
function! s:encode_uri(path, start_pos_encode, default_prefix) abort
let l:prefix = s:get_prefix(a:path)
let l:path = a:path[len(l:prefix):]
if len(l:prefix) == 0
let l:prefix = a:default_prefix
endif
let l:result = strpart(a:path, 0, a:start_pos_encode)
for l:i in range(a:start_pos_encode, len(l:path) - 1)
" Don't encode '/' here, `path` is expected to be a valid path.
if l:path[l:i] =~# '^[a-zA-Z0-9_.~/@-]$'
let l:result .= l:path[l:i]
else
let l:result .= s:urlencode_char(l:path[l:i])
endif
endfor
return l:prefix . l:result
endfunction
let s:path_to_uri_cache = {}
if has('win32') || has('win64') || has('win32unix')
function! lsp#utils#path_to_uri(path) abort
if has_key(s:path_to_uri_cache, a:path)
return s:path_to_uri_cache[a:path]
endif
if empty(a:path) || lsp#utils#is_remote_uri(a:path)
let s:path_to_uri_cache[a:path] = a:path
return s:path_to_uri_cache[a:path]
else
" Transform cygwin paths to windows paths
let l:path = a:path
if has('win32unix')
let l:path = substitute(a:path, '\c^/\([a-z]\)/', '\U\1:/', '')
endif
" You must not encode the volume information on the path if
" present
let l:end_pos_volume = matchstrpos(l:path, '\c[A-Z]:')[2]
if l:end_pos_volume == -1
let l:end_pos_volume = 0
endif
let s:path_to_uri_cache[l:path] = s:encode_uri(substitute(l:path, '\', '/', 'g'), l:end_pos_volume, 'file:///')
return s:path_to_uri_cache[l:path]
endif
endfunction
else
function! lsp#utils#path_to_uri(path) abort
if has_key(s:path_to_uri_cache, a:path)
return s:path_to_uri_cache[a:path]
endif
if empty(a:path) || lsp#utils#is_remote_uri(a:path)
let s:path_to_uri_cache[a:path] = a:path
return s:path_to_uri_cache[a:path]
else
let s:path_to_uri_cache[a:path] = s:encode_uri(a:path, 0, 'file://')
return s:path_to_uri_cache[a:path]
endif
endfunction
endif
let s:uri_to_path_cache = {}
if has('win32') || has('win64') || has('win32unix')
function! lsp#utils#uri_to_path(uri) abort
if has_key(s:uri_to_path_cache, a:uri)
return s:uri_to_path_cache[a:uri]
endif
let l:path = substitute(s:decode_uri(a:uri[len('file:///'):]), '/', '\\', 'g')
" Transform windows paths to cygwin paths
if has('win32unix')
let l:path = substitute(l:path, '\c^\([A-Z]\):\\', '/\l\1/', '')
let l:path = substitute(l:path, '\\', '/', 'g')
endif
let s:uri_to_path_cache[a:uri] = l:path
return s:uri_to_path_cache[a:uri]
endfunction
else
function! lsp#utils#uri_to_path(uri) abort
if has_key(s:uri_to_path_cache, a:uri)
return s:uri_to_path_cache[a:uri]
endif
let s:uri_to_path_cache[a:uri] = s:decode_uri(a:uri[len('file://'):])
return s:uri_to_path_cache[a:uri]
endfunction
endif
if has('win32') || has('win64')
function! lsp#utils#normalize_uri(uri) abort
" Refer to https://github.com/microsoft/language-server-protocol/pull/1019 on normalization of urls.
" TODO: after the discussion is settled, modify this function.
let l:ret = substitute(a:uri, '^file:///[a-zA-Z]\zs%3[aA]', ':', '')
return substitute(l:ret, '^file:///\zs\([A-Z]\)', "\\=tolower(submatch(1))", '')
endfunction
else
function! lsp#utils#normalize_uri(uri) abort
return a:uri
endfunction
endif
function! lsp#utils#get_default_root_uri() abort
return lsp#utils#path_to_uri(getcwd())
endfunction
function! lsp#utils#get_buffer_path(...) abort
return expand((a:0 > 0 ? '#' . a:1 : '%') . ':p')
endfunction
function! lsp#utils#get_buffer_uri(...) abort
let l:name = a:0 > 0 ? bufname(a:1) : expand('%')
if empty(l:name)
let l:nr = a:0 > 0 ? a:1 : bufnr('%')
let l:name = printf('%s/__NO_NAME_%d__', getcwd(), l:nr)
endif
return lsp#utils#path_to_uri(fnamemodify(l:name, ':p'))
endfunction
" Find a nearest to a `path` parent directory `directoryname` by traversing the filesystem upwards
function! lsp#utils#find_nearest_parent_directory(path, directoryname) abort
let l:relative_path = finddir(a:directoryname, a:path . ';')
if !empty(l:relative_path)
return fnamemodify(l:relative_path, ':p')
else
return ''
endif
endfunction
" Find a nearest to a `path` parent filename `filename` by traversing the filesystem upwards
function! lsp#utils#find_nearest_parent_file(path, filename) abort
let l:relative_path = findfile(a:filename, a:path . ';')
if !empty(l:relative_path)
return fnamemodify(l:relative_path, ':p')
else
return ''
endif
endfunction
function! lsp#utils#_compare_nearest_path(matches, lhs, rhs) abort
let l:llhs = len(a:lhs)
let l:lrhs = len(a:rhs)
if l:llhs ># l:lrhs
return -1
elseif l:llhs <# l:lrhs
return 1
endif
if a:matches[a:lhs] ># a:matches[a:rhs]
return -1
elseif a:matches[a:lhs] <# a:matches[a:rhs]
return 1
endif
return 0
endfunction
function! lsp#utils#_nearest_path(matches) abort
return empty(a:matches) ?
\ '' :
\ sort(keys(a:matches), function('lsp#utils#_compare_nearest_path', [a:matches]))[0]
endfunction
" Find a nearest to a `path` parent filename `filename` by traversing the filesystem upwards
" The filename ending with '/' or '\' will be regarded as directory name,
" otherwith as file name
function! lsp#utils#find_nearest_parent_file_directory(path, filename) abort
if type(a:filename) == 3
let l:matched_paths = {}
for l:current_name in a:filename
let l:path = lsp#utils#find_nearest_parent_file_directory(a:path, l:current_name)
if !empty(l:path)
if has_key(l:matched_paths, l:path)
let l:matched_paths[l:path] += 1
else
let l:matched_paths[l:path] = 1
endif
endif
endfor
return lsp#utils#_nearest_path(l:matched_paths)
elseif type(a:filename) == 1
if a:filename[-1:] ==# '/' || a:filename[-1:] ==# '\'
let l:modify_str = ':p:h:h'
let l:path = lsp#utils#find_nearest_parent_directory(a:path, a:filename[:-2])
else
let l:modify_str = ':p:h'
let l:path = lsp#utils#find_nearest_parent_file(a:path, a:filename)
endif
return empty(l:path) ? '' : fnamemodify(l:path, l:modify_str)
else
echoerr "The type of argument \"filename\" must be String or List"
endif
endfunction
if exists('*matchstrpos')
function! lsp#utils#matchstrpos(expr, pattern) abort
return matchstrpos(a:expr, a:pattern)
endfunction
else
function! lsp#utils#matchstrpos(expr, pattern) abort
return [matchstr(a:expr, a:pattern), match(a:expr, a:pattern), matchend(a:expr, a:pattern)]
endfunction
endif
function! lsp#utils#empty_complete(...) abort
return []
endfunction
function! lsp#utils#error(msg) abort
echohl ErrorMsg
echom a:msg
echohl NONE
endfunction
function! lsp#utils#warning(msg) abort
echohl WarningMsg
echom a:msg
echohl NONE
endfunction
function! lsp#utils#echo_with_truncation(msg) abort
let l:msg = a:msg
if &laststatus == 0 || (&laststatus == 1 && tabpagewinnr(tabpagenr(), '$') == 1)
let l:winwidth = winwidth(0)
if &ruler
let l:winwidth -= 18
endif
else
let l:winwidth = &columns
endif
if &showcmd
let l:winwidth -= 12
endif
if l:winwidth > 5 && l:winwidth < strdisplaywidth(l:msg)
let l:msg = l:msg[:l:winwidth - 5] . '...'
endif
exec 'echo l:msg'
endfunction
" Convert a byte-index (1-based) to a character-index (0-based)
" This function requires a buffer specifier (expr, see :help bufname()),
" a line number (lnum, 1-based), and a byte-index (char, 1-based).
function! lsp#utils#to_char(expr, lnum, col) abort
let l:lines = getbufline(a:expr, a:lnum)
if l:lines == []
if type(a:expr) != v:t_string || !filereadable(a:expr)
" invalid a:expr
return a:col - 1
endif
" a:expr is a file that is not yet loaded as a buffer
let l:lines = readfile(a:expr, '', a:lnum)
endif
let l:linestr = l:lines[-1]
return strchars(strpart(l:linestr, 0, a:col - 1))
endfunction
function! s:get_base64_alphabet() abort
let l:alphabet = []
" Uppercase letters
for l:c in range(char2nr('A'), char2nr('Z'))
call add(l:alphabet, nr2char(l:c))
endfor
" Lowercase letters
for l:c in range(char2nr('a'), char2nr('z'))
call add(l:alphabet, nr2char(l:c))
endfor
" Numbers
for l:c in range(char2nr('0'), char2nr('9'))
call add(l:alphabet, nr2char(l:c))
endfor
" Symbols
call add(l:alphabet, '+')
call add(l:alphabet, '/')
return l:alphabet
endfunction
if exists('*trim')
function! lsp#utils#_trim(string) abort
return trim(a:string)
endfunction
else
function! lsp#utils#_trim(string) abort
return substitute(a:string, '^\s*\|\s*$', '', 'g')
endfunction
endif
function! lsp#utils#_get_before_line() abort
let l:text = getline('.')
let l:idx = min([strlen(l:text), col('.') - 2])
let l:idx = max([l:idx, -1])
if l:idx == -1
return ''
endif
return l:text[0 : l:idx]
endfunction
function! lsp#utils#_get_before_char_skip_white() abort
let l:current_lnum = line('.')
let l:lnum = l:current_lnum
while l:lnum > 0
if l:lnum == l:current_lnum
let l:text = lsp#utils#_get_before_line()
else
let l:text = getline(l:lnum)
endif
let l:match = matchlist(l:text, '\([^[:blank:]]\)\s*$')
if get(l:match, 1, v:null) isnot v:null
return l:match[1]
endif
let l:lnum -= 1
endwhile
return ''
endfunction
let s:alphabet = s:get_base64_alphabet()
function! lsp#utils#base64_decode(data) abort
let l:ret = []
" Process base64 string in chunks of 4 chars
for l:group in split(a:data, '.\{4}\zs')
let l:group_dec = 0
" Convert 4 chars to 3 octets
for l:char in split(l:group, '\zs')
let l:group_dec = l:group_dec * 64
let l:group_dec += max([index(s:alphabet, l:char), 0])
endfor
" Split the number representing the 3 octets into the individual
" octets
let l:octets = []
let l:i = 0
while l:i < 3
call add(l:octets, l:group_dec % 256)
let l:group_dec = l:group_dec / 256
let l:i += 1
endwhile
call extend(l:ret, reverse(l:octets))
endfor
" Handle padding
if len(a:data) >= 2
if strpart(a:data, len(a:data) - 2) ==# '=='
call remove(l:ret, -2, -1)
elseif strpart(a:data, len(a:data) - 1) ==# '='
call remove(l:ret, -1, -1)
endif
endif
return l:ret
endfunction
function! lsp#utils#make_valid_word(str) abort
let l:str = substitute(a:str, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')
let l:str = substitute(l:str, '\\\(.\)', '\1', 'g')
let l:valid = matchstr(l:str, '^[^"'' (<{\[\t\r\n]\+')
if empty(l:valid)
return l:str
endif
if l:valid =~# ':$'
return l:valid[:-2]
endif
return l:valid
endfunction
function! lsp#utils#_split_by_eol(text) abort
return split(a:text, '\r\n\|\r\|\n', v:true)
endfunction
" parse command options like "-key" or "-key=value"
function! lsp#utils#parse_command_options(params) abort
let l:result = {}
for l:param in a:params
let l:match = matchlist(l:param, '-\{1,2}\zs\([^=]*\)\(=\(.*\)\)\?\m')
let l:result[l:match[1]] = l:match[3]
endfor
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
if type(a:timeout) != type(0)
return -3
endif
if type(get(a:000, 0, 0)) != type(0)
return -3
endif
while 1
let l:result=call('wait', extend([a:timeout, a:condition], a:000))
if l:result != -3 " ignore spurious errors
return l:result
endif
endwhile
endfunction
else
function! lsp#utils#_wait(timeout, condition, ...) abort
try
let l:timeout = a:timeout / 1000.0
let l:interval = get(a:000, 0, 200)
let l:Condition = a:condition
if type(l:Condition) != type(function('eval'))
let l:Condition = function('eval', l:Condition)
endif
let l:start = reltime()
while l:timeout < 0 || reltimefloat(reltime(l:start)) < l:timeout
if l:Condition()
return 0
endif
execute 'sleep ' . l:interval . 'm'
endwhile
return -1
catch /^Vim:Interrupt$/
return -2
endtry
endfunction
endif
function! lsp#utils#iteratable(list) abort
return type(a:list) !=# v:t_list ? [] : a:list
endfunction

View File

@@ -0,0 +1,42 @@
function! lsp#utils#args#_parse(args, opt, remainder_key) abort
let l:result = {}
let l:is_opts = v:true
let l:remainder = []
for l:item in split(a:args, ' ')
if l:item[:1] !=# '--'
let l:is_opts = v:false
endif
if l:is_opts == v:false
call add(l:remainder, l:item)
continue
endif
let l:parts = split(l:item, '=')
let l:key = l:parts[0]
let l:value = get(l:parts, 1, '')
let l:key = l:key[2:]
if has_key(a:opt, l:key)
if has_key(a:opt[l:key], 'type')
let l:type = a:opt[l:key]['type']
if l:type == type(v:true)
if l:value ==# 'false' || l:value ==# '0' || l:value ==# ''
let l:value = 0
else
let l:value = 1
endif
elseif l:type ==# type(0)
let l:value = str2nr(l:value)
endif
endif
endif
let l:result[l:key] = l:value
endfor
if a:remainder_key != v:null
let l:result[a:remainder_key] = join(l:remainder)
endif
return l:result
endfunction

View File

@@ -0,0 +1,74 @@
let s:fixendofline_exists = exists('+fixendofline')
function! s:get_fixendofline(buf) abort
let l:eol = getbufvar(a:buf, '&endofline')
let l:binary = getbufvar(a:buf, '&binary')
if s:fixendofline_exists
let l:fixeol = getbufvar(a:buf, '&fixendofline')
if !l:binary
" When 'binary' is off and 'fixeol' is on, 'endofline' is not used
"
" When 'binary' is off and 'fixeol' is off, 'endofline' is used to
" remember the presence of a <EOL>
return l:fixeol || l:eol
else
" When 'binary' is on, the value of 'fixeol' doesn't matter
return l:eol
endif
else
" When 'binary' is off the value of 'endofline' is not used
"
" When 'binary' is on 'endofline' is used to remember the presence of
" a <EOL>
return !l:binary || l:eol
endif
endfunction
function! lsp#utils#buffer#_get_fixendofline(bufnr) abort
return s:get_fixendofline(a:bufnr)
endfunction
function! lsp#utils#buffer#_get_lines(buf) abort
let l:lines = getbufline(a:buf, 1, '$')
if s:get_fixendofline(a:buf)
let l:lines += ['']
endif
return l:lines
endfunction
" @params {location} = {
" 'uri': 'file://....',
" 'range': {
" 'start': { 'line': 1, 'character': 1 },
" 'end': { 'line': 1, 'character': 1 },
" }
" }
function! lsp#utils#buffer#_open_lsp_location(location) abort
let l:path = lsp#utils#uri_to_path(a:location['uri'])
let l:bufnr = bufnr(l:path)
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['start'])
let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['end'])
normal! m'
if &modified && !&hidden
let l:cmd = l:bufnr !=# -1 ? 'sb ' . l:bufnr : 'split ' . fnameescape(l:path)
else
let l:cmd = l:bufnr !=# -1 ? 'b ' . l:bufnr : 'edit ' . fnameescape(l:path)
endif
execute l:cmd . ' | call cursor('.l:start_line.','.l:start_col.')'
normal! V
call setpos("'<", [l:bufnr, l:start_line, l:start_col])
call setpos("'>", [l:bufnr, l:end_line, l:end_col])
endfunction
function! lsp#utils#buffer#get_indent_size(bufnr) abort
let l:shiftwidth = getbufvar(a:bufnr, '&shiftwidth')
if getbufvar(a:bufnr, '&shiftwidth')
return l:shiftwidth
endif
return getbufvar(a:bufnr, '&tabstop')
endfunction

View File

@@ -0,0 +1,165 @@
" This is copied from https://github.com/natebosch/vim-lsc/blob/master/autoload/lsc/diff.vim
"
" Computes a simplistic diff between [old] and [new].
"
" Returns a dict with keys `range`, `rangeLength`, and `text` matching the LSP
" definition of `TextDocumentContentChangeEvent`.
"
" Finds a single change between the common prefix, and common postfix.
let s:has_lua = has('nvim-0.4.0') || (has('lua') && has('patch-8.2.0775'))
" lua array and neovim vim list index starts with 1 while vim lists starts with 0.
" starting patch-8.2.1066 vim lists array index was changed to start with 1.
let s:lua_array_start_index = has('nvim-0.4.0') || has('patch-8.2.1066')
function! s:init_lua() abort
lua <<EOF
-- Returns a zero-based index of the last line that is different between
-- old and new. If old and new are not zero indexed, pass offset to indicate
-- the index base.
function vimlsp_last_difference(old, new, offset, line_count)
for i = 0, line_count - 1 do
if old[#old - i + offset] ~= new[#new - i + offset] then
return -1 * i
end
end
return -1 * line_count
end
-- Returns a zero-based index of the first line that is different between
-- old and new. If old and new are not zero indexed, pass offset to indicate
-- the index base.
function vimlsp_first_difference(old, new, offset, line_count)
for i = 0, line_count - 1 do
if old[i + offset] ~= new[i + offset] then
return i
end
end
return line_count - 1
end
EOF
let s:lua = 1
endfunction
if s:has_lua && !exists('s:lua')
call s:init_lua()
endif
function! lsp#utils#diff#compute(old, new) abort
let [l:start_line, l:start_char] = s:FirstDifference(a:old, a:new)
let [l:end_line, l:end_char] =
\ s:LastDifference(a:old[l:start_line :], a:new[l:start_line :], l:start_char)
let l:text = s:ExtractText(a:new, l:start_line, l:start_char, l:end_line, l:end_char)
let l:length = s:Length(a:old, l:start_line, l:start_char, l:end_line, l:end_char)
let l:adj_end_line = len(a:old) + l:end_line
let l:adj_end_char = l:end_line == 0 ? 0 : strchars(a:old[l:end_line]) + l:end_char + 1
let l:result = { 'range': {'start': {'line': l:start_line, 'character': l:start_char},
\ 'end': {'line': l:adj_end_line, 'character': l:adj_end_char}},
\ 'text': l:text,
\ 'rangeLength': l:length,
\}
return l:result
endfunction
" Finds the line and character of the first different character between two
" list of Strings.
function! s:FirstDifference(old, new) abort
let l:line_count = min([len(a:old), len(a:new)])
if l:line_count == 0 | return [0, 0] | endif
if g:lsp_use_lua && s:has_lua
let l:eval = has('nvim') ? 'vim.api.nvim_eval' : 'vim.eval'
let l:i = luaeval('vimlsp_first_difference('
\.l:eval.'("a:old"),'.l:eval.'("a:new"),'.s:lua_array_start_index.','.l:line_count.')')
else
for l:i in range(l:line_count)
if a:old[l:i] !=# a:new[l:i] | break | endif
endfor
endif
if l:i >= l:line_count
return [l:line_count - 1, strchars(a:old[l:line_count - 1])]
endif
let l:old_line = a:old[l:i]
let l:new_line = a:new[l:i]
let l:length = min([strchars(l:old_line), strchars(l:new_line)])
let l:j = 0
while l:j < l:length
if strgetchar(l:old_line, l:j) != strgetchar(l:new_line, l:j) | break | endif
let l:j += 1
endwhile
return [l:i, l:j]
endfunction
function! s:LastDifference(old, new, start_char) abort
let l:line_count = min([len(a:old), len(a:new)])
if l:line_count == 0 | return [0, 0] | endif
if g:lsp_use_lua && s:has_lua
let l:eval = has('nvim') ? 'vim.api.nvim_eval' : 'vim.eval'
let l:i = luaeval('vimlsp_last_difference('
\.l:eval.'("a:old"),'.l:eval.'("a:new"),'.s:lua_array_start_index.','.l:line_count.')')
else
for l:i in range(-1, -1 * l:line_count, -1)
if a:old[l:i] !=# a:new[l:i] | break | endif
endfor
endif
if l:i <= -1 * l:line_count
let l:i = -1 * l:line_count
let l:old_line = strcharpart(a:old[l:i], a:start_char)
let l:new_line = strcharpart(a:new[l:i], a:start_char)
else
let l:old_line = a:old[l:i]
let l:new_line = a:new[l:i]
endif
let l:old_line_length = strchars(l:old_line)
let l:new_line_length = strchars(l:new_line)
let l:length = min([l:old_line_length, l:new_line_length])
let l:j = -1
while l:j >= -1 * l:length
if strgetchar(l:old_line, l:old_line_length + l:j) !=
\ strgetchar(l:new_line, l:new_line_length + l:j)
break
endif
let l:j -= 1
endwhile
return [l:i, l:j]
endfunction
function! s:ExtractText(lines, start_line, start_char, end_line, end_char) abort
if a:start_line == len(a:lines) + a:end_line
if a:end_line == 0 | return '' | endif
let l:line = a:lines[a:start_line]
let l:length = strchars(l:line) + a:end_char - a:start_char + 1
return strcharpart(l:line, a:start_char, l:length)
endif
let l:result = strcharpart(a:lines[a:start_line], a:start_char) . "\n"
for l:line in a:lines[a:start_line + 1:a:end_line - 1]
let l:result .= l:line . "\n"
endfor
if a:end_line != 0
let l:line = a:lines[a:end_line]
let l:length = strchars(l:line) + a:end_char + 1
let l:result .= strcharpart(l:line, 0, l:length)
endif
return l:result
endfunction
function! s:Length(lines, start_line, start_char, end_line, end_char) abort
let l:adj_end_line = len(a:lines) + a:end_line
if l:adj_end_line >= len(a:lines)
let l:adj_end_char = a:end_char - 1
else
let l:adj_end_char = strchars(a:lines[l:adj_end_line]) + a:end_char
endif
if a:start_line == l:adj_end_line
return l:adj_end_char - a:start_char + 1
endif
let l:result = strchars(a:lines[a:start_line]) - a:start_char + 1
let l:line = a:start_line + 1
while l:line < l:adj_end_line
let l:result += strchars(a:lines[l:line]) + 1
let l:line += 1
endwhile
let l:result += l:adj_end_char + 1
return l:result
endfunction

View File

@@ -0,0 +1,409 @@
" https://github.com/prabirshrestha/async.vim#2082d13bb195f3203d41a308b89417426a7deca1 (dirty)
" :AsyncEmbed path=./autoload/lsp/utils/job.vim namespace=lsp#utils#job
" Author: Prabir Shrestha <mail at prabir dot me>
" Website: https://github.com/prabirshrestha/async.vim
" License: The MIT License {{{
" The MIT License (MIT)
"
" Copyright (c) 2016 Prabir Shrestha
"
" Permission is hereby granted, free of charge, to any person obtaining a copy
" of this software and associated documentation files (the "Software"), to deal
" in the Software without restriction, including without limitation the rights
" to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
" copies of the Software, and to permit persons to whom the Software is
" furnished to do so, subject to the following conditions:
"
" The above copyright notice and this permission notice shall be included in all
" copies or substantial portions of the Software.
"
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
" IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
" FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
" AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
" LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
" OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
" SOFTWARE.
" }}}
let s:save_cpo = &cpo
set cpo&vim
let s:jobidseq = 0
let s:jobs = {} " { job, opts, type: 'vimjob|nvimjob'}
let s:job_type_nvimjob = 'nvimjob'
let s:job_type_vimjob = 'vimjob'
let s:job_error_unsupported_job_type = -2 " unsupported job type
function! s:noop(...) abort
endfunction
function! s:job_supported_types() abort
let l:supported_types = []
if has('nvim')
let l:supported_types += [s:job_type_nvimjob]
endif
if !has('nvim') && has('job') && has('channel') && has('lambda')
let l:supported_types += [s:job_type_vimjob]
endif
return l:supported_types
endfunction
function! s:job_supports_type(type) abort
return index(s:job_supported_types(), a:type) >= 0
endfunction
function! s:out_cb(jobid, opts, job, data) abort
call a:opts.on_stdout(a:jobid, a:data, 'stdout')
endfunction
function! s:out_cb_array(jobid, opts, job, data) abort
call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout')
endfunction
function! s:err_cb(jobid, opts, job, data) abort
call a:opts.on_stderr(a:jobid, a:data, 'stderr')
endfunction
function! s:err_cb_array(jobid, opts, job, data) abort
call a:opts.on_stderr(a:jobid, split(a:data, "\n", 1), 'stderr')
endfunction
function! s:exit_cb(jobid, opts, job, status) abort
if has_key(a:opts, 'on_exit')
call a:opts.on_exit(a:jobid, a:status, 'exit')
endif
if has_key(s:jobs, a:jobid)
call remove(s:jobs, a:jobid)
endif
endfunction
function! s:on_stdout(jobid, data, event) abort
let l:jobinfo = s:jobs[a:jobid]
call l:jobinfo.opts.on_stdout(a:jobid, a:data, a:event)
endfunction
function! s:on_stdout_string(jobid, data, event) abort
let l:jobinfo = s:jobs[a:jobid]
call l:jobinfo.opts.on_stdout(a:jobid, join(a:data, "\n"), a:event)
endfunction
function! s:on_stderr(jobid, data, event) abort
let l:jobinfo = s:jobs[a:jobid]
call l:jobinfo.opts.on_stderr(a:jobid, a:data, a:event)
endfunction
function! s:on_stderr_string(jobid, data, event) abort
let l:jobinfo = s:jobs[a:jobid]
call l:jobinfo.opts.on_stderr(a:jobid, join(a:data, "\n"), a:event)
endfunction
function! s:on_exit(jobid, status, event) abort
if has_key(s:jobs, a:jobid)
let l:jobinfo = s:jobs[a:jobid]
if has_key(l:jobinfo.opts, 'on_exit')
call l:jobinfo.opts.on_exit(a:jobid, a:status, a:event)
endif
if has_key(s:jobs, a:jobid)
call remove(s:jobs, a:jobid)
endif
endif
endfunction
function! s:job_start(cmd, opts) abort
let l:jobtypes = s:job_supported_types()
let l:jobtype = ''
if has_key(a:opts, 'type')
if type(a:opts.type) == type('')
if !s:job_supports_type(a:opts.type)
return s:job_error_unsupported_job_type
endif
let l:jobtype = a:opts.type
else
let l:jobtypes = a:opts.type
endif
endif
if empty(l:jobtype)
" find the best jobtype
for l:jobtype2 in l:jobtypes
if s:job_supports_type(l:jobtype2)
let l:jobtype = l:jobtype2
endif
endfor
endif
if l:jobtype ==? ''
return s:job_error_unsupported_job_type
endif
" options shared by both vim and neovim
let l:jobopt = {}
if has_key(a:opts, 'cwd')
let l:jobopt.cwd = a:opts.cwd
endif
if has_key(a:opts, 'env')
let l:jobopt.env = a:opts.env
endif
let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw
if l:jobtype == s:job_type_nvimjob
if l:normalize ==# 'string'
let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout_string') : function('s:noop')
let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr_string') : function('s:noop')
else " array or raw
let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout') : function('s:noop')
let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr') : function('s:noop')
endif
call extend(l:jobopt, { 'on_exit': function('s:on_exit') })
let l:job = jobstart(a:cmd, l:jobopt)
if l:job <= 0
return l:job
endif
let l:jobid = l:job " nvimjobid and internal jobid is same
let s:jobs[l:jobid] = {
\ 'type': s:job_type_nvimjob,
\ 'opts': a:opts,
\ }
let s:jobs[l:jobid].job = l:job
elseif l:jobtype == s:job_type_vimjob
let s:jobidseq = s:jobidseq + 1
let l:jobid = s:jobidseq
if l:normalize ==# 'array'
let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb_array', [l:jobid, a:opts]) : function('s:noop')
let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb_array', [l:jobid, a:opts]) : function('s:noop')
else " raw or string
let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb', [l:jobid, a:opts]) : function('s:noop')
let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb', [l:jobid, a:opts]) : function('s:noop')
endif
call extend(l:jobopt, {
\ 'exit_cb': function('s:exit_cb', [l:jobid, a:opts]),
\ 'mode': 'raw',
\ })
if has('patch-8.1.889')
let l:jobopt['noblock'] = 1
endif
let l:job = job_start(a:cmd, l:jobopt)
if job_status(l:job) !=? 'run'
return -1
endif
let s:jobs[l:jobid] = {
\ 'type': s:job_type_vimjob,
\ 'opts': a:opts,
\ 'job': l:job,
\ 'channel': job_getchannel(l:job),
\ 'buffer': ''
\ }
else
return s:job_error_unsupported_job_type
endif
return l:jobid
endfunction
function! s:job_stop(jobid) abort
if has_key(s:jobs, a:jobid)
let l:jobinfo = s:jobs[a:jobid]
if l:jobinfo.type == s:job_type_nvimjob
" See: vital-Whisky/System.Job
try
call jobstop(a:jobid)
catch /^Vim\%((\a\+)\)\=:E900/
" NOTE:
" Vim does not raise exception even the job has already closed so fail
" silently for 'E900: Invalid job id' exception
endtry
elseif l:jobinfo.type == s:job_type_vimjob
if type(s:jobs[a:jobid].job) == v:t_job
call job_stop(s:jobs[a:jobid].job)
elseif type(s:jobs[a:jobid].job) == v:t_channel
call ch_close(s:jobs[a:jobid].job)
endif
endif
endif
endfunction
function! s:job_send(jobid, data, opts) abort
let l:jobinfo = s:jobs[a:jobid]
let l:close_stdin = get(a:opts, 'close_stdin', 0)
if l:jobinfo.type == s:job_type_nvimjob
call jobsend(a:jobid, a:data)
if l:close_stdin
call chanclose(a:jobid, 'stdin')
endif
elseif l:jobinfo.type == s:job_type_vimjob
" There is no easy way to know when ch_sendraw() finishes writing data
" on a non-blocking channels -- has('patch-8.1.889') -- and because of
" this, we cannot safely call ch_close_in(). So when we find ourselves
" in this situation (i.e. noblock=1 and close stdin after send) we fall
" back to using s:flush_vim_sendraw() and wait for transmit buffer to be
" empty
"
" Ref: https://groups.google.com/d/topic/vim_dev/UNNulkqb60k/discussion
if has('patch-8.1.818') && (!has('patch-8.1.889') || !l:close_stdin)
call ch_sendraw(l:jobinfo.channel, a:data)
else
let l:jobinfo.buffer .= a:data
call s:flush_vim_sendraw(a:jobid, v:null)
endif
if l:close_stdin
while len(l:jobinfo.buffer) != 0
sleep 1m
endwhile
call ch_close_in(l:jobinfo.channel)
endif
endif
endfunction
function! s:flush_vim_sendraw(jobid, timer) abort
" https://github.com/vim/vim/issues/2548
" https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091
let l:jobinfo = s:jobs[a:jobid]
sleep 1m
if len(l:jobinfo.buffer) <= 4096
call ch_sendraw(l:jobinfo.channel, l:jobinfo.buffer)
let l:jobinfo.buffer = ''
else
let l:to_send = l:jobinfo.buffer[:4095]
let l:jobinfo.buffer = l:jobinfo.buffer[4096:]
call ch_sendraw(l:jobinfo.channel, l:to_send)
call timer_start(1, function('s:flush_vim_sendraw', [a:jobid]))
endif
endfunction
function! s:job_wait_single(jobid, timeout, start) abort
if !has_key(s:jobs, a:jobid)
return -3
endif
let l:jobinfo = s:jobs[a:jobid]
if l:jobinfo.type == s:job_type_nvimjob
let l:timeout = a:timeout - reltimefloat(reltime(a:start)) * 1000
return jobwait([a:jobid], float2nr(l:timeout))[0]
elseif l:jobinfo.type == s:job_type_vimjob
let l:timeout = a:timeout / 1000.0
try
while l:timeout < 0 || reltimefloat(reltime(a:start)) < l:timeout
let l:info = job_info(l:jobinfo.job)
if l:info.status ==# 'dead'
return l:info.exitval
elseif l:info.status ==# 'fail'
return -3
endif
sleep 1m
endwhile
catch /^Vim:Interrupt$/
return -2
endtry
endif
return -1
endfunction
function! s:job_wait(jobids, timeout) abort
let l:start = reltime()
let l:exitcode = 0
let l:ret = []
for l:jobid in a:jobids
if l:exitcode != -2 " Not interrupted.
let l:exitcode = s:job_wait_single(l:jobid, a:timeout, l:start)
endif
let l:ret += [l:exitcode]
endfor
return l:ret
endfunction
function! s:job_pid(jobid) abort
if !has_key(s:jobs, a:jobid)
return 0
endif
let l:jobinfo = s:jobs[a:jobid]
if l:jobinfo.type == s:job_type_nvimjob
return jobpid(a:jobid)
elseif l:jobinfo.type == s:job_type_vimjob
let l:vimjobinfo = job_info(a:jobid)
if type(l:vimjobinfo) == type({}) && has_key(l:vimjobinfo, 'process')
return l:vimjobinfo['process']
endif
endif
return 0
endfunction
function! s:callback_cb(jobid, opts, ch, data) abort
if has_key(a:opts, 'on_stdout')
call a:opts.on_stdout(a:jobid, a:data, 'stdout')
endif
endfunction
function! s:callback_cb_array(jobid, opts, ch, data) abort
if has_key(a:opts, 'on_stdout')
call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout')
endif
endfunction
function! s:close_cb(jobid, opts, ch) abort
if has_key(a:opts, 'on_exit')
call a:opts.on_exit(a:jobid, 'closed', 'exit')
endif
if has_key(s:jobs, a:jobid)
call remove(s:jobs, a:jobid)
endif
endfunction
" public apis {{{
function! lsp#utils#job#start(cmd, opts) abort
return s:job_start(a:cmd, a:opts)
endfunction
function! lsp#utils#job#stop(jobid) abort
call s:job_stop(a:jobid)
endfunction
function! lsp#utils#job#send(jobid, data, ...) abort
let l:opts = get(a:000, 0, {})
call s:job_send(a:jobid, a:data, l:opts)
endfunction
function! lsp#utils#job#wait(jobids, ...) abort
let l:timeout = get(a:000, 0, -1)
return s:job_wait(a:jobids, l:timeout)
endfunction
function! lsp#utils#job#pid(jobid) abort
return s:job_pid(a:jobid)
endfunction
function! lsp#utils#job#connect(addr, opts) abort
let s:jobidseq = s:jobidseq + 1
let l:jobid = s:jobidseq
let l:retry = 0
let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw
while l:retry < 5
let l:ch = ch_open(a:addr, {'waittime': 1000})
call ch_setoptions(l:ch, {
\ 'callback': function(l:normalize ==# 'array' ? 's:callback_cb_array' : 's:callback_cb', [l:jobid, a:opts]),
\ 'close_cb': function('s:close_cb', [l:jobid, a:opts]),
\ 'mode': 'raw',
\})
if ch_status(l:ch) ==# 'open'
break
endif
sleep 100m
let l:retry += 1
endwhile
let s:jobs[l:jobid] = {
\ 'type': s:job_type_vimjob,
\ 'opts': a:opts,
\ 'job': l:ch,
\ 'channel': l:ch,
\ 'buffer': ''
\}
return l:jobid
endfunction
" }}}
let &cpo = s:save_cpo
unlet s:save_cpo

View File

@@ -0,0 +1,135 @@
function! s:open_location(path, line, col, ...) abort
normal! m'
let l:mods = a:0 ? a:1 : ''
let l:buffer = bufnr(a:path)
if l:mods ==# '' && &modified && !&hidden && l:buffer != bufnr('%')
let l:mods = &splitbelow ? 'rightbelow' : 'leftabove'
endif
if l:mods ==# ''
if l:buffer == bufnr('%')
let l:cmd = ''
else
let l:cmd = (l:buffer !=# -1 ? 'b ' . l:buffer : 'edit ' . fnameescape(a:path)) . ' | '
endif
else
let l:cmd = l:mods . ' ' . (l:buffer !=# -1 ? 'sb ' . l:buffer : 'split ' . fnameescape(a:path)) . ' | '
endif
execute l:cmd . 'call cursor('.a:line.','.a:col.')'
endfunction
" @param location = {
" 'filename',
" 'lnum',
" 'col',
" }
function! lsp#utils#location#_open_vim_list_item(location, mods) abort
call s:open_location(a:location['filename'], a:location['lnum'], a:location['col'], a:mods)
endfunction
" @params {location} = {
" 'uri': 'file://....',
" 'range': {
" 'start': { 'line': 1, 'character': 1 },
" 'end': { 'line': 1, 'character': 1 },
" }
" }
function! lsp#utils#location#_open_lsp_location(location) abort
let l:path = lsp#utils#uri_to_path(a:location['uri'])
let l:bufnr = bufnr(l:path)
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['start'])
let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['end'])
call s:open_location(l:path, l:start_line, l:start_col)
normal! V
call setpos("'<", [l:bufnr, l:start_line, l:start_col])
call setpos("'>", [l:bufnr, l:end_line, l:end_col])
endfunction
" @param loc = Location | LocationLink
" @param cache = {} empty dict
" @returns {
" 'filename',
" 'lnum',
" 'col',
" 'text',
" 'viewstart?',
" 'viewend?',
" }
function! s:lsp_location_item_to_vim(loc, cache) abort
if has_key(a:loc, 'targetUri') " LocationLink
let l:uri = a:loc['targetUri']
let l:range = a:loc['targetSelectionRange']
let l:use_link = 1
else " Location
let l:uri = a:loc['uri']
let l:range = a:loc['range']
let l:use_link = 0
endif
if !lsp#utils#is_file_uri(l:uri)
return v:null
endif
let l:path = lsp#utils#uri_to_path(l:uri)
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start'])
let l:index = l:line - 1
if has_key(a:cache, l:path)
let l:text = a:cache[l:path][l:index]
else
let l:contents = getbufline(l:path, 1, '$')
if !empty(l:contents)
let l:text = get(l:contents, l:index, '')
else
let l:contents = readfile(l:path)
let a:cache[l:path] = l:contents
let l:text = get(l:contents, l:index, '')
endif
endif
if l:use_link
" viewstart/end decremented to account for incrementing in _lsp_to_vim
return {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': l:text,
\ 'viewstart': lsp#utils#position#lsp_to_vim(l:path, a:loc['targetRange']['start'])[0] - 1,
\ 'viewend': lsp#utils#position#lsp_to_vim(l:path, a:loc['targetRange']['end'])[0] - 1,
\ }
else
return {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': l:text,
\ }
endif
endfunction
" @summary Use this to convert loc to vim list that is compatible with
" quickfix and locllist items
" @param loc = v:null | Location | Location[] | LocationLink
" @returns []
function! lsp#utils#location#_lsp_to_vim_list(loc) abort
let l:result = []
let l:cache = {}
if empty(a:loc) " v:null
return l:result
elseif type(a:loc) == type([]) " Location[]
for l:location in a:loc
let l:vim_loc = s:lsp_location_item_to_vim(l:location, l:cache)
if !empty(l:vim_loc) " https:// uri will return empty
call add(l:result, l:vim_loc)
endif
endfor
else " Location or LocationLink
let l:vim_loc = s:lsp_location_item_to_vim(a:loc, l:cache)
if !empty(l:vim_loc) " https:// uri will return empty
call add(l:result, l:vim_loc)
endif
endif
return l:result
endfunction

View File

@@ -0,0 +1,91 @@
" This function can be error prone if the caller forgets to use +1 to vim line
" so use lsp#utils#position#lsp_to_vim instead
" Convert a character-index (0-based) to byte-index (1-based)
" This function requires a buffer specifier (expr, see :help bufname()),
" a line number (lnum, 1-based), and a character-index (char, 0-based).
function! s:to_col(expr, lnum, char) abort
let l:lines = getbufline(a:expr, a:lnum)
if l:lines == []
if type(a:expr) != v:t_string || !filereadable(a:expr)
" invalid a:expr
return a:char + 1
endif
" a:expr is a file that is not yet loaded as a buffer
let l:lines = readfile(a:expr, '', a:lnum)
if l:lines == []
" when the file is empty. a:char should be 0 in the case
return a:char + 1
endif
endif
let l:linestr = l:lines[-1]
return strlen(strcharpart(l:linestr, 0, a:char)) + 1
endfunction
" The inverse version of `s:to_col`.
" Convert [lnum, col] to LSP's `Position`.
function! s:to_char(expr, lnum, col) abort
let l:lines = getbufline(a:expr, a:lnum)
if l:lines == []
if type(a:expr) != v:t_string || !filereadable(a:expr)
" invalid a:expr
return a:col - 1
endif
" a:expr is a file that is not yet loaded as a buffer
let l:lines = readfile(a:expr, '', a:lnum)
endif
let l:linestr = l:lines[-1]
return strchars(strpart(l:linestr, 0, a:col - 1))
endfunction
" @param expr = see :help bufname()
" @param position = {
" 'line': 1,
" 'character': 1
" }
" @returns [
" line,
" col
" ]
function! lsp#utils#position#lsp_to_vim(expr, position) abort
let l:line = lsp#utils#position#lsp_line_to_vim(a:expr, a:position)
let l:col = lsp#utils#position#lsp_character_to_vim(a:expr, a:position)
return [l:line, l:col]
endfunction
" @param expr = see :help bufname()
" @param position = {
" 'line': 1,
" 'character': 1
" }
" @returns
" line
function! lsp#utils#position#lsp_line_to_vim(expr, position) abort
return a:position['line'] + 1
endfunction
" @param expr = see :help bufname()
" @param position = {
" 'line': 1,
" 'character': 1
" }
" @returns
" line
function! lsp#utils#position#lsp_character_to_vim(expr, position) abort
let l:line = a:position['line'] + 1 " optimize function overhead by not calling lsp_line_to_vim
let l:char = a:position['character']
return s:to_col(a:expr, l:line, l:char)
endfunction
" @param expr = :help bufname()
" @param pos = [lnum, col]
" @returns {
" 'line': line,
" 'character': character
" }
function! lsp#utils#position#vim_to_lsp(expr, pos) abort
return {
\ 'line': a:pos[0] - 1,
\ 'character': s:to_char(a:expr, a:pos[0], a:pos[1])
\ }
endfunction

View File

@@ -0,0 +1,82 @@
"
" Returns recent visual-mode range.
"
function! lsp#utils#range#_get_recent_visual_range() abort
let l:start_pos = getpos("'<")[1 : 2]
let l:end_pos = getpos("'>")[1 : 2]
let l:end_pos[1] += 1 " To exclusive
" Fix line selection.
let l:end_line = getline(l:end_pos[0])
if l:end_pos[1] > strlen(l:end_line)
let l:end_pos[1] = strlen(l:end_line) + 1
endif
let l:range = {}
let l:range['start'] = lsp#utils#position#vim_to_lsp('%', l:start_pos)
let l:range['end'] = lsp#utils#position#vim_to_lsp('%', l:end_pos)
return l:range
endfunction
"
" Returns current line range.
"
function! lsp#utils#range#_get_current_line_range() abort
let l:pos = getpos('.')[1 : 2]
let l:range = {}
let l:range['start'] = lsp#utils#position#vim_to_lsp('%', l:pos)
let l:range['end'] = lsp#utils#position#vim_to_lsp('%', [l:pos[0], l:pos[1] + strlen(getline(l:pos[0])) + 1])
return l:range
endfunction
" Convert a LSP range to one or more vim match positions.
" If the range spans over multiple lines, break it down to multiple
" positions, one for each line.
" Return a list of positions.
function! lsp#utils#range#lsp_to_vim(bufnr, range) abort
let l:position = []
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(a:bufnr, a:range['start'])
let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(a:bufnr, a:range['end'])
if l:end_line == l:start_line
let l:position = [[
\ l:start_line,
\ l:start_col,
\ l:end_col - l:start_col
\ ]]
else
" First line
let l:position = [[
\ l:start_line,
\ l:start_col,
\ 999
\ ]]
" Last line
call add(l:position, [
\ l:end_line,
\ 1,
\ l:end_col
\ ])
" Lines in the middle
let l:middle_lines = map(
\ range(l:start_line + 1, l:end_line - 1),
\ {_, l -> [l, 0, 999]}
\ )
call extend(l:position, l:middle_lines)
endif
return l:position
endfunction
function! lsp#utils#range#get_range() abort
let l:char = lsp#utils#to_char('%', line('$'), col('$'))
return {'start': {'line': 0, 'character': 0}, 'end': {'line': line('$')-1, 'character': l:char}}
endfunction
function! lsp#utils#range#get_range_curline() abort
let l:char = lsp#utils#to_char('%', line('.'), col('$'))
return {'start': {'line': line('.')-1, 'character': 0}, 'end': {'line': line('.')-1, 'character': l:char}}
endfunction

View File

@@ -0,0 +1,19 @@
function! s:next(steps, current_index, result) abort
if len(a:steps) == a:current_index
return
endif
let l:Step = a:steps[a:current_index]
let l:ctx = {
\ 'callback': function('s:callback', [a:steps, a:current_index]),
\ 'result': a:result
\ }
call call(l:Step, [l:ctx])
endfunction
function! s:callback(steps, current_index, ...) abort
call s:next(a:steps, a:current_index + 1, a:000)
endfunction
function! lsp#utils#step#start(steps) abort
call s:next(a:steps, 0, [])
endfunction

View File

@@ -0,0 +1,33 @@
if exists('*gettagstack') && exists('*settagstack')
function! lsp#utils#tagstack#_update() abort
let l:bufnr = bufnr('%')
let l:item = {'bufnr': l:bufnr, 'from': [l:bufnr, line('.'), col('.'), 0], 'tagname': expand('<cword>')}
let l:winid = win_getid()
let l:stack = gettagstack(l:winid)
if l:stack['length'] == l:stack['curidx']
" Replace the last items with item.
let l:action = 'r'
let l:stack['items'][l:stack['curidx']-1] = l:item
elseif l:stack['length'] > l:stack['curidx']
" Replace items after used items with item.
let l:action = 'r'
if l:stack['curidx'] > 1
let l:stack['items'] = add(l:stack['items'][:l:stack['curidx']-2], l:item)
else
let l:stack['items'] = [l:item]
endif
else
" Append item.
let l:action = 'a'
let l:stack['items'] = [l:item]
endif
let l:stack['curidx'] += 1
call settagstack(l:winid, l:stack, l:action)
endfunction
else
function! lsp#utils#tagstack#_update() abort
" do nothing
endfunction
endif

View File

@@ -0,0 +1,229 @@
function! lsp#utils#text_edit#get_range(text_edit) abort
if type(a:text_edit) != v:t_dict
return v:null
endif
let l:insert = get(a:text_edit, 'insert', v:null)
if type(l:insert) == v:t_dict
return l:insert
endif
return get(a:text_edit, 'range', v:null)
endfunction
function! lsp#utils#text_edit#apply_text_edits(uri, text_edits) abort
let l:current_bufname = bufname('%')
let l:target_bufname = lsp#utils#uri_to_path(a:uri)
let l:cursor_position = lsp#get_position()
call s:_switch(l:target_bufname)
for l:text_edit in s:_normalize(a:text_edits)
call s:_apply(bufnr(l:target_bufname), l:text_edit, l:cursor_position)
endfor
call s:_switch(l:current_bufname)
if bufnr(l:current_bufname) == bufnr(l:target_bufname)
call cursor(lsp#utils#position#lsp_to_vim('%', l:cursor_position))
endif
endfunction
" @summary Use this to convert textedit to vim list that is compatible with
" quickfix and locllist items
" @param uri = DocumentUri
" @param text_edit = TextEdit | TextEdit[]
" @returns []
function! lsp#utils#text_edit#_lsp_to_vim_list(uri, text_edit) abort
let l:result = []
let l:cache = {}
if type(a:text_edit) == type([]) " TextEdit[]
for l:text_edit in a:text_edit
let l:vim_loc = s:lsp_text_edit_item_to_vim(a:uri, l:text_edit, l:cache)
if !empty(l:vim_loc)
call add(l:result, l:vim_loc)
endif
endfor
else " TextEdit
let l:vim_loc = s:lsp_text_edit_item_to_vim(a:uri, a:text_edit, l:cache)
if !empty(l:vim_loc)
call add(l:result, l:vim_loc)
endif
endif
return l:result
endfunction
" @param uri = DocumentUri
" @param text_edit = TextEdit
" @param cache = {} empty dict
" @returns {
" 'filename',
" 'lnum',
" 'col',
" 'text',
" }
function! s:lsp_text_edit_item_to_vim(uri, text_edit, cache) abort
if !lsp#utils#is_file_uri(a:uri)
return v:null
endif
let l:path = lsp#utils#uri_to_path(a:uri)
let l:range = a:text_edit['range']
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start'])
let l:index = l:line - 1
if has_key(a:cache, l:path)
let l:text = a:cache[l:path][l:index]
else
let l:contents = getbufline(l:path, 1, '$')
if !empty(l:contents)
let l:text = get(l:contents, l:index, '')
else
let l:contents = readfile(l:path)
let a:cache[l:path] = l:contents
let l:text = get(l:contents, l:index, '')
endif
endif
return {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': l:text
\ }
endfunction
"
" _apply
"
function! s:_apply(bufnr, text_edit, cursor_position) abort
" create before/after line.
let l:start_line = getline(a:text_edit['range']['start']['line'] + 1)
let l:end_line = getline(a:text_edit['range']['end']['line'] + 1)
let l:before_line = strcharpart(l:start_line, 0, a:text_edit['range']['start']['character'])
let l:after_line = strcharpart(l:end_line, a:text_edit['range']['end']['character'], strchars(l:end_line) - a:text_edit['range']['end']['character'])
" create new lines.
let l:new_lines = lsp#utils#_split_by_eol(a:text_edit['newText'])
let l:new_lines[0] = l:before_line . l:new_lines[0]
let l:new_lines[-1] = l:new_lines[-1] . l:after_line
" save length.
let l:new_lines_len = len(l:new_lines)
let l:range_len = (a:text_edit['range']['end']['line'] - a:text_edit['range']['start']['line']) + 1
" fixendofline
let l:buffer_length = len(getbufline(a:bufnr, '^', '$'))
let l:should_fixendofline = lsp#utils#buffer#_get_fixendofline(a:bufnr)
let l:should_fixendofline = l:should_fixendofline && l:new_lines[-1] ==# ''
let l:should_fixendofline = l:should_fixendofline && l:buffer_length <= a:text_edit['range']['end']['line']
let l:should_fixendofline = l:should_fixendofline && a:text_edit['range']['end']['character'] == 0
if l:should_fixendofline
call remove(l:new_lines, -1)
endif
" fix cursor pos
if a:text_edit['range']['end']['line'] < a:cursor_position['line']
" fix cursor line
let a:cursor_position['line'] += l:new_lines_len - l:range_len
elseif a:text_edit['range']['end']['line'] == a:cursor_position['line'] && a:text_edit['range']['end']['character'] <= a:cursor_position['character']
" fix cursor line and col
let a:cursor_position['line'] += l:new_lines_len - l:range_len
let l:end_character = strchars(l:new_lines[-1]) - strchars(l:after_line)
let l:end_offset = a:cursor_position['character'] - a:text_edit['range']['end']['character']
let a:cursor_position['character'] = l:end_character + l:end_offset
endif
" append or delete lines.
if l:new_lines_len > l:range_len
call append(a:text_edit['range']['start']['line'], repeat([''], l:new_lines_len - l:range_len))
elseif l:new_lines_len < l:range_len
let l:offset = l:range_len - l:new_lines_len
call s:delete(a:bufnr, a:text_edit['range']['start']['line'] + 1, a:text_edit['range']['start']['line'] + l:offset)
endif
" set lines.
call setline(a:text_edit['range']['start']['line'] + 1, l:new_lines)
endfunction
"
" _normalize
"
function! s:_normalize(text_edits) abort
let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits]
let l:text_edits = filter(copy(l:text_edits), { _, text_edit -> type(text_edit) == type({}) })
let l:text_edits = s:_range(l:text_edits)
let l:text_edits = sort(copy(l:text_edits), function('s:_compare', [], {}))
let l:text_edits = s:_check(l:text_edits)
return reverse(l:text_edits)
endfunction
"
" _range
"
function! s:_range(text_edits) abort
for l:text_edit in a:text_edits
if l:text_edit.range.start.line > l:text_edit.range.end.line || (
\ l:text_edit.range.start.line == l:text_edit.range.end.line &&
\ l:text_edit.range.start.character > l:text_edit.range.end.character
\ )
let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start }
endif
endfor
return a:text_edits
endfunction
"
" _check
"
" LSP Spec says `multiple text edits can not overlap those ranges`.
" This function check it. But does not throw error.
"
function! s:_check(text_edits) abort
if len(a:text_edits) > 1
let l:range = a:text_edits[0].range
for l:text_edit in a:text_edits[1 : -1]
if l:range.end.line > l:text_edit.range.start.line || (
\ l:range.end.line == l:text_edit.range.start.line &&
\ l:range.end.character > l:text_edit.range.start.character
\ )
call lsp#log('text_edit: range overlapped.')
endif
let l:range = l:text_edit.range
endfor
endif
return a:text_edits
endfunction
"
" _compare
"
function! s:_compare(text_edit1, text_edit2) abort
let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line
if l:diff == 0
return a:text_edit1.range.start.character - a:text_edit2.range.start.character
endif
return l:diff
endfunction
"
" _switch
"
function! s:_switch(path) abort
if bufnr(a:path) >= 0
execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path))
else
execute printf('keepalt keepjumps edit! %s', fnameescape(a:path))
endif
endfunction
"
" delete
"
function! s:delete(bufnr, start, end) abort
if exists('*deletebufline')
call deletebufline(a:bufnr, a:start, a:end)
else
let l:foldenable = &foldenable
setlocal nofoldenable
execute printf('%s,%sdelete _', a:start, a:end)
let &foldenable = l:foldenable
endif
endfunction

View File

@@ -0,0 +1,295 @@
" This file is part of an installation of vim-yggdrasil, a vim/neovim tree viewer library.
" The source code of vim-yggdrasil is available at https://github.com/m-pilia/vim-yggdrasil
"
" vim-yggdrasil is free software, distributed under the MIT license.
" The full license is available at https://github.com/m-pilia/vim-yggdrasil/blob/master/LICENSE
"
" Yggdrasil version (git SHA-1): 043d0ab53dcdd0d91b7c7cd205791d64d4ed9624
"
" This installation was generated on 2020-03-15T14:47:27-0700 with the following vim command:
" :YggdrasilPlant -plugin_dir=./ -namespace=lsp/utils
scriptencoding utf-8
" Callback to retrieve the tree item representation of an object.
function! s:node_get_tree_item_cb(node, object, status, tree_item) abort
if a:status ==? 'success'
let l:new_node = s:node_new(a:node.tree, a:object, a:tree_item, a:node)
call add(a:node.children, l:new_node)
call s:tree_render(l:new_node.tree)
endif
endfunction
" Callback to retrieve the children objects of a node.
function! s:node_get_children_cb(node, status, childObjectList) abort
for l:childObject in a:childObjectList
let l:Callback = function('s:node_get_tree_item_cb', [a:node, l:childObject])
call a:node.tree.provider.getTreeItem(l:Callback, l:childObject)
endfor
endfunction
" Set the node to be collapsed or expanded.
"
" When {collapsed} evaluates to 0 the node is expanded, when it is 1 the node is
" collapsed, when it is equal to -1 the node is toggled (it is expanded if it
" was collapsed, and vice versa).
function! s:node_set_collapsed(collapsed) dict abort
let l:self.collapsed = a:collapsed < 0 ? !l:self.collapsed : !!a:collapsed
endfunction
" Given a funcref {Condition}, return a list of all nodes in the subtree of
" {node} for which {Condition} evaluates to v:true.
function! s:search_subtree(node, Condition) abort
if a:Condition(a:node)
return [a:node]
endif
if len(a:node.children) < 1
return []
endif
let l:result = []
for l:child in a:node.children
let l:result = l:result + s:search_subtree(l:child, a:Condition)
endfor
return l:result
endfunction
" Execute the action associated to a node
function! s:node_exec() dict abort
if has_key(l:self.tree_item, 'command')
call l:self.tree_item.command()
endif
endfunction
" Return the depth level of the node in the tree. The level is defined
" recursively: the root has depth 0, and each node has depth equal to the depth
" of its parent increased by 1.
function! s:node_level() dict abort
if l:self.parent == {}
return 0
endif
return 1 + l:self.parent.level()
endf
" Return the string representation of the node. The {level} argument represents
" the depth level of the node in the tree and it is passed for convenience, to
" simplify the implementation and to avoid re-computing the depth.
function! s:node_render(level) dict abort
let l:indent = repeat(' ', 2 * a:level)
let l:mark = '• '
if len(l:self.children) > 0 || l:self.lazy_open != v:false
let l:mark = l:self.collapsed ? '▸ ' : '▾ '
endif
let l:label = split(l:self.tree_item.label, "\n")
call extend(l:self.tree.index, map(range(len(l:label)), 'l:self'))
let l:repr = l:indent . l:mark . l:label[0]
\ . join(map(l:label[1:], {_, l -> "\n" . l:indent . ' ' . l}))
let l:lines = [l:repr]
if !l:self.collapsed
if l:self.lazy_open
let l:self.lazy_open = v:false
let l:Callback = function('s:node_get_children_cb', [l:self])
call l:self.tree.provider.getChildren(l:Callback, l:self.object)
endif
for l:child in l:self.children
call add(l:lines, l:child.render(a:level + 1))
endfor
endif
return join(l:lines, "\n")
endfunction
" Insert a new node in the tree, internally represented by a unique progressive
" integer identifier {id}. The node represents a certain {object} (children of
" {parent}) belonging to a given {tree}, having an associated action to be
" triggered on execution defined by the function object {exec}. If {collapsed}
" is true the node will be rendered as collapsed in the view. If {lazy_open} is
" true, the children of the node will be fetched when the node is expanded by
" the user.
function! s:node_new(tree, object, tree_item, parent) abort
let a:tree.maxid += 1
return {
\ 'id': a:tree.maxid,
\ 'tree': a:tree,
\ 'object': a:object,
\ 'tree_item': a:tree_item,
\ 'parent': a:parent,
\ 'collapsed': a:tree_item.collapsibleState ==? 'collapsed',
\ 'lazy_open': a:tree_item.collapsibleState !=? 'none',
\ 'children': [],
\ 'level': function('s:node_level'),
\ 'exec': function('s:node_exec'),
\ 'set_collapsed': function('s:node_set_collapsed'),
\ 'render': function('s:node_render'),
\ }
endfunction
" Callback that sets the root node of a given {tree}, creating a new node
" with a {tree_item} representation for the given {object}. If {status} is
" equal to 'success', the root node is set and the tree view is updated
" accordingly, otherwise nothing happens.
function! s:tree_set_root_cb(tree, object, status, tree_item) abort
if a:status ==? 'success'
let a:tree.maxid = -1
let a:tree.root = s:node_new(a:tree, a:object, a:tree_item, {})
call s:tree_render(a:tree)
endif
endfunction
" Return the node currently under the cursor from the given {tree}.
function! s:get_node_under_cursor(tree) abort
let l:index = min([line('.'), len(a:tree.index) - 1])
return a:tree.index[l:index]
endfunction
" Expand or collapse the node under cursor, and render the tree.
" Please refer to *s:node_set_collapsed()* for details about the
" arguments and behaviour.
function! s:tree_set_collapsed_under_cursor(collapsed) dict abort
let l:node = s:get_node_under_cursor(l:self)
call l:node.set_collapsed(a:collapsed)
call s:tree_render(l:self)
endfunction
" Run the action associated to the node currently under the cursor.
function! s:tree_exec_node_under_cursor() dict abort
call s:get_node_under_cursor(l:self).exec()
endfunction
" Render the {tree}. This will replace the content of the buffer with the
" tree view. Clear the index, setting it to a list containing a guard
" value for index 0 (line numbers are one-based).
function! s:tree_render(tree) abort
if &filetype !=# 'lsp-tree'
return
endif
let l:cursor = getpos('.')
let a:tree.index = [-1]
let l:text = a:tree.root.render(0)
setlocal modifiable
silent 1,$delete _
silent 0put=l:text
$d
setlocal nomodifiable
call setpos('.', l:cursor)
endfunction
" If {status} equals 'success', update all nodes of {tree} representing
" an {obect} with given {tree_item} representation.
function! s:node_update(tree, object, status, tree_item) abort
if a:status !=? 'success'
return
endif
for l:node in s:search_subtree(a:tree.root, {n -> n.object == a:object})
let l:node.tree_item = a:tree_item
let l:node.children = []
let l:node.lazy_open = a:tree_item.collapsibleState !=? 'none'
endfor
call s:tree_render(a:tree)
endfunction
" Update the view if nodes have changed. If called with no arguments,
" update the whole tree. If called with an {object} as argument, update
" all the subtrees of nodes corresponding to {object}.
function! s:tree_update(...) dict abort
if a:0 < 1
call l:self.provider.getChildren({status, obj ->
\ l:self.provider.getTreeItem(function('s:tree_set_root_cb', [l:self, obj[0]]), obj[0])})
else
call l:self.provider.getTreeItem(function('s:node_update', [l:self, a:1]), a:1)
endif
endfunction
" Destroy the tree view. Wipe out the buffer containing it.
function! s:tree_wipe() dict abort
execute 'bwipeout' . l:self.bufnr
endfunction
" Apply syntax to an lsp-tree buffer
function! s:filetype_syntax() abort
syntax clear
syntax match LspTreeMarkLeaf "•" contained
syntax match LspTreeMarkCollapsed "▸" contained
syntax match LspTreeMarkExpanded "▾" contained
syntax match LspTreeNode "\v^(\s|[▸▾•])*.*"
\ contains=LspTreeMarkLeaf,LspTreeMarkCollapsed,LspTreeMarkExpanded
highlight def link LspTreeMarkLeaf Type
highlight def link LspTreeMarkExpanded Type
highlight def link LspTreeMarkCollapsed Macro
endfunction
" Apply local settings to an lsp-tree buffer
function! s:filetype_settings() abort
setlocal bufhidden=wipe
setlocal buftype=nofile
setlocal foldcolumn=0
setlocal foldmethod=manual
setlocal nobuflisted
setlocal nofoldenable
setlocal nohlsearch
setlocal nolist
setlocal nomodifiable
setlocal nonumber
setlocal nospell
setlocal noswapfile
setlocal nowrap
nnoremap <silent> <buffer> <Plug>(lsp-tree-toggle-node)
\ :call b:lsp_tree.set_collapsed_under_cursor(-1)<cr>
nnoremap <silent> <buffer> <Plug>(lsp-tree-open-node)
\ :call b:lsp_tree.set_collapsed_under_cursor(v:false)<cr>
nnoremap <silent> <buffer> <Plug>(lsp-tree-close-node)
\ :call b:lsp_tree.set_collapsed_under_cursor(v:true)<cr>
nnoremap <silent> <buffer> <Plug>(lsp-tree-execute-node)
\ :call b:lsp_tree.exec_node_under_cursor()<cr>
nnoremap <silent> <buffer> <Plug>(lsp-tree-wipe-tree)
\ :call b:lsp_tree.wipe()<cr>
if !exists('g:lsp_tree_no_default_maps')
nmap <silent> <buffer> o <Plug>(lsp-tree-toggle-node)
nmap <silent> <buffer> <cr> <Plug>(lsp-tree-execute-node)
nmap <silent> <buffer> q <Plug>(lsp-tree-wipe-tree)
endif
endfunction
" Turns the current buffer into an lsp-tree tree view. Tree data is retrieved
" from the given {provider}, and the state of the tree is stored in a
" buffer-local variable called b:lsp_tree.
"
" The {bufnr} stores the buffer number of the view, {maxid} is the highest
" known internal identifier of the nodes. The {index} is a list that
" maps line numbers to nodes.
function! lsp#utils#tree#new(provider) abort
let b:lsp_tree = {
\ 'bufnr': bufnr('%'),
\ 'maxid': -1,
\ 'root': {},
\ 'index': [],
\ 'provider': a:provider,
\ 'set_collapsed_under_cursor': function('s:tree_set_collapsed_under_cursor'),
\ 'exec_node_under_cursor': function('s:tree_exec_node_under_cursor'),
\ 'update': function('s:tree_update'),
\ 'wipe': function('s:tree_wipe'),
\ }
augroup vim_lsp_tree
autocmd!
autocmd FileType lsp-tree call s:filetype_syntax() | call s:filetype_settings()
autocmd BufEnter <buffer> call s:tree_render(b:lsp_tree)
augroup END
setlocal filetype=lsp-tree
call b:lsp_tree.update()
endfunction

View File

@@ -0,0 +1,14 @@
function! lsp#utils#workspace_config#get_value(server_name, item) abort
try
let l:server_info = lsp#get_server_info(a:server_name)
let l:config = l:server_info['workspace_config']
for l:section in split(a:item['section'], '\.')
let l:config = l:config[l:section]
endfor
return l:config
catch
return v:null
endtry
endfunction

View File

@@ -0,0 +1,27 @@
" Applies WorkspaceEdit changes.
function! lsp#utils#workspace_edit#apply_workspace_edit(workspace_edit) abort
let l:loclist_items = []
if has_key(a:workspace_edit, 'documentChanges')
for l:text_document_edit in a:workspace_edit['documentChanges']
let l:loclist_items += s:_apply(l:text_document_edit['textDocument']['uri'], l:text_document_edit['edits'])
endfor
elseif has_key(a:workspace_edit, 'changes')
for [l:uri, l:text_edits] in items(a:workspace_edit['changes'])
let l:loclist_items += s:_apply(l:uri, l:text_edits)
endfor
endif
if g:lsp_show_workspace_edits
call setloclist(0, l:loclist_items, 'r')
execute 'lopen'
endif
endfunction
"
" _apply
"
function! s:_apply(uri, text_edits) abort
call lsp#utils#text_edit#apply_text_edits(a:uri, a:text_edits)
return lsp#utils#text_edit#_lsp_to_vim_list(a:uri, a:text_edits)
endfunction

View File

@@ -0,0 +1,9 @@
let s:_plugin_name = expand('<sfile>:t:r')
function! vital#{s:_plugin_name}#new() abort
return vital#{s:_plugin_name[1:]}#new()
endfunction
function! vital#{s:_plugin_name}#function(funcname) abort
silent! return function(a:funcname)
endfunction

View File

@@ -0,0 +1,66 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_lsp#VS#LSP#MarkupContent#import() abort', printf("return map({'_vital_depends': '', 'normalize': '', '_vital_loaded': ''}, \"vital#_lsp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
"
" _vital_loaded
"
function! s:_vital_loaded(V) abort
let s:Text = a:V.import('VS.LSP.Text')
endfunction
"
" _vital_depends
"
function! s:_vital_depends() abort
return ['VS.LSP.Text']
endfunction
"
" normalize
"
function! s:normalize(markup_content, ...) abort
let l:option = get(a:000, 0, {})
let l:option.compact = get(l:option, 'compact', v:true)
let l:normalized = ''
if type(a:markup_content) == type('')
let l:normalized = a:markup_content
elseif type(a:markup_content) == type([])
let l:normalized = join(a:markup_content, "\n")
elseif type(a:markup_content) == type({})
let l:normalized = a:markup_content.value
if has_key(a:markup_content, 'language')
let l:normalized = join([
\ '```' . a:markup_content.language,
\ l:normalized,
\ '```'
\ ], "\n")
endif
endif
let l:normalized = s:Text.normalize_eol(l:normalized)
let l:normalized = s:_format(l:normalized, l:option.compact)
return l:normalized
endfunction
"
" _format
"
function! s:_format(string, compact) abort
let l:string = a:string
if a:compact
let l:string = substitute(l:string, "\\%(\\s\\|\n\\)*```\\s*\\(\\w\\+\\)\\%(\\s\\|\n\\)\\+", "\n\n```\\1 ", 'g')
let l:string = substitute(l:string, "\\%(\\s\\|\n\\)\\+```\\%(\\s*\\%(\\%$\\|\n\\)\\)\\+", " ```\n\n", 'g')
else
let l:string = substitute(l:string, "```\n\\zs\\%(\\s\\|\n\\)\\+", "", 'g')
endif
let l:string = substitute(l:string, "\\%^\\%(\\s\\|\n\\)*", '', 'g')
let l:string = substitute(l:string, "\\%(\\s\\|\n\\)*\\%$", '', 'g')
return l:string
endfunction

View File

@@ -0,0 +1,23 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_lsp#VS#LSP#Text#import() abort', printf("return map({'normalize_eol': '', 'split_by_eol': ''}, \"vital#_lsp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
"
" normalize_eol
"
function! s:normalize_eol(text) abort
return substitute(a:text, "\r\n\\|\r", "\n", 'g')
endfunction
"
" split_by_eol
"
function! s:split_by_eol(text) abort
return split(a:text, "\r\n\\|\r\\|\n", v:true)
endfunction

View File

@@ -0,0 +1,140 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_lsp#VS#Vim#Buffer#import() abort', printf("return map({'add': '', 'do': '', 'create': '', 'get_line_count': '', 'pseudo': '', 'ensure': '', 'load': ''}, \"vital#_lsp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
let s:Do = { -> {} }
let g:___VS_Vim_Buffer_id = get(g:, '___VS_Vim_Buffer_id', 0)
"
" get_line_count
"
if exists('*nvim_buf_line_count')
function! s:get_line_count(bufnr) abort
return nvim_buf_line_count(a:bufnr)
endfunction
elseif has('patch-8.2.0019')
function! s:get_line_count(bufnr) abort
return getbufinfo(a:bufnr)[0].linecount
endfunction
else
function! s:get_line_count(bufnr) abort
if bufnr('%') == bufnr(a:bufnr)
return line('$')
endif
return len(getbufline(a:bufnr, '^', '$'))
endfunction
endif
"
" create
"
function! s:create(...) abort
let g:___VS_Vim_Buffer_id += 1
let l:bufname = printf('VS.Vim.Buffer: %s: %s',
\ g:___VS_Vim_Buffer_id,
\ get(a:000, 0, 'VS.Vim.Buffer.Default')
\ )
return s:load(l:bufname)
endfunction
"
" ensure
"
function! s:ensure(expr) abort
if !bufexists(a:expr)
if type(a:expr) == type(0)
throw printf('VS.Vim.Buffer: `%s` is not valid expr.', a:expr)
endif
call s:add(a:expr)
endif
return bufnr(a:expr)
endfunction
"
" add
"
if exists('*bufadd')
function! s:add(name) abort
let l:bufnr = bufadd(a:name)
call setbufvar(l:bufnr, '&buflisted', 1)
endfunction
else
function! s:add(name) abort
badd `=a:name`
endfunction
endif
"
" load
"
if exists('*bufload')
function! s:load(expr) abort
let l:bufnr = s:ensure(a:expr)
if !bufloaded(l:bufnr)
call bufload(l:bufnr)
endif
return l:bufnr
endfunction
else
function! s:load(expr) abort
let l:curr_bufnr = bufnr('%')
try
let l:bufnr = s:ensure(a:expr)
execute printf('keepalt keepjumps silent %sbuffer', l:bufnr)
catch /.*/
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
finally
execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr)
endtry
return l:bufnr
endfunction
endif
"
" do
"
function! s:do(bufnr, func) abort
let l:curr_bufnr = bufnr('%')
if l:curr_bufnr == a:bufnr
call a:func()
return
endif
try
execute printf('noautocmd keepalt keepjumps silent %sbuffer', a:bufnr)
call a:func()
catch /.*/
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
finally
execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr)
endtry
endfunction
"
" pseudo
"
function! s:pseudo(filepath) abort
if !filereadable(a:filepath)
throw printf('VS.Vim.Buffer: `%s` is not valid filepath.', a:filepath)
endif
" create pseudo buffer
let l:bufname = printf('VSVimBufferPseudo://%s', a:filepath)
if bufexists(l:bufname)
return s:ensure(l:bufname)
endif
let l:bufnr = s:ensure(l:bufname)
let l:group = printf('VS_Vim_Buffer_pseudo:%s', l:bufnr)
execute printf('augroup %s', l:group)
execute printf('autocmd BufReadCmd <buffer=%s> call setline(1, readfile(bufname("%")[20 : -1])) | try | filetype detect | catch /.*/ | endtry | augroup %s | autocmd! | augroup END', l:bufnr, l:group)
augroup END
return l:bufnr
endfunction

View File

@@ -0,0 +1,155 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_lsp#VS#Vim#Syntax#Markdown#import() abort', printf("return map({'apply': ''}, \"vital#_lsp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
"
" apply
"
" TODO: Refactor
"
function! s:apply(...) abort
let l:args = get(a:000, 0, {})
let l:text = has_key(l:args, 'text') ? l:args.text : getbufline('%', 1, '$')
let l:text = type(l:text) == v:t_list ? join(l:text, "\n") : l:text
call s:_execute('syntax sync clear')
if !exists('b:___VS_Vim_Syntax_Markdown')
" Avoid automatic highlighting by built-in runtime syntax.
if !has_key(g:, 'markdown_fenced_languages')
call s:_execute('runtime! syntax/markdown.vim')
else
let l:markdown_fenced_languages = g:markdown_fenced_languages
unlet g:markdown_fenced_languages
call s:_execute('runtime! syntax/markdown.vim')
let g:markdown_fenced_languages = l:markdown_fenced_languages
endif
" Remove markdownCodeBlock because we support it manually.
call s:_clear('markdownCodeBlock') " runtime
call s:_clear('mkdCode') " plasticboy/vim-markdown
" Modify markdownCode (`codes...`)
call s:_clear('markdownCode')
syntax region markdownCode matchgroup=Conceal start=/\%(``\)\@!`/ matchgroup=Conceal end=/\%(``\)\@!`/ containedin=TOP keepend concealends
" Modify markdownEscape (_bold\_text_) @see nvim's syntax/lsp_markdown.vim
call s:_clear('markdownEscape')
syntax region markdownEscape matchgroup=markdownEscape start=/\\\ze[\\\x60*{}\[\]()#+\-,.!_>~|"$%&'\/:;<=?@^ ]/ end=/./ containedin=ALL keepend oneline concealends
" Add syntax for basic html entities.
syntax match vital_vs_vim_syntax_markdown_entities_lt /&lt;/ containedin=ALL conceal cchar=<
syntax match vital_vs_vim_syntax_markdown_entities_gt /&gt;/ containedin=ALL conceal cchar=>
syntax match vital_vs_vim_syntax_markdown_entities_amp /&amp;/ containedin=ALL conceal cchar=&
syntax match vital_vs_vim_syntax_markdown_entities_quot /&quot;/ containedin=ALL conceal cchar="
syntax match vital_vs_vim_syntax_markdown_entities_nbsp /&nbsp;/ containedin=ALL conceal cchar=
let b:___VS_Vim_Syntax_Markdown = {}
let b:___VS_Vim_Syntax_Markdown.marks = {}
let b:___VS_Vim_Syntax_Markdown.filetypes = {}
endif
for [l:mark, l:filetype] in items(s:_get_filetype_map(l:text))
try
let l:mark_group = substitute(toupper(l:mark), '\.', '_', 'g')
if has_key(b:___VS_Vim_Syntax_Markdown.marks, l:mark_group)
continue
endif
let b:___VS_Vim_Syntax_Markdown.marks[l:mark_group] = v:true
let l:filetype_group = substitute(toupper(l:filetype), '\.', '_', 'g')
if !has_key(b:___VS_Vim_Syntax_Markdown.filetypes, l:filetype_group)
call s:_execute('syntax include @%s syntax/%s.vim', l:filetype_group, l:filetype)
let b:___VS_Vim_Syntax_Markdown.filetypes[l:filetype_group] = v:true
endif
call s:_execute('syntax region %s matchgroup=Conceal start=/%s/ matchgroup=Conceal end=/%s/ contains=@%s containedin=TOP keepend concealends',
\ l:mark_group,
\ printf('```\s*%s\s*', l:mark),
\ '```\s*\%(' . "\n" . '\|$\)',
\ l:filetype_group
\ )
catch /.*/
unsilent echomsg printf('Fail to apply "syntax/%s.vim". Add "let g:markdown_fenced_languages = ["%s=$FILETYPE"]" to enable syntax', l:filetype, l:filetype)
endtry
endfor
endfunction
"
" _clear
"
function! s:_clear(group) abort
try
execute printf('silent! syntax clear %s', a:group)
catch /.*/
endtry
endfunction
"
" _execute
"
function! s:_execute(command, ...) abort
let b:current_syntax = ''
unlet b:current_syntax
let g:main_syntax = ''
unlet g:main_syntax
execute call('printf', [a:command] + a:000)
endfunction
"
" _get_filetype_map
"
function! s:_get_filetype_map(text) abort
let l:filetype_map = {}
for l:mark in s:_find_marks(a:text)
let l:filetype_map[l:mark] = s:_get_filetype_from_mark(l:mark)
endfor
return l:filetype_map
endfunction
"
" _find_marks
"
function! s:_find_marks(text) abort
let l:marks = {}
" find from buffer contents.
let l:text = a:text
let l:pos = 0
while 1
let l:match = matchstrpos(l:text, '```\s*\zs\w\+', l:pos, 1)
if empty(l:match[0])
break
endif
let l:marks[l:match[0]] = v:true
let l:pos = l:match[2]
endwhile
return keys(l:marks)
endfunction
"
" _get_filetype_from_mark
"
function! s:_get_filetype_from_mark(mark) abort
for l:config in get(g:, 'markdown_fenced_languages', [])
if l:config !~# '='
if l:config ==# a:mark
return a:mark
endif
else
let l:config = split(l:config, '=')
if l:config[0] ==# a:mark
return l:config[1]
endif
endif
endfor
return a:mark
endfunction

View File

@@ -0,0 +1,157 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_lsp#VS#Vim#Window#import() abort', printf("return map({'info': '', 'do': '', 'is_floating': '', 'find': '', 'scroll': '', 'screenpos': ''}, \"vital#_lsp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
let s:Do = { -> {} }
"
" do
"
function! s:do(winid, func) abort
let l:curr_winid = win_getid()
if l:curr_winid == a:winid
call a:func()
return
endif
if !has('nvim') && exists('*win_execute')
let s:Do = a:func
try
noautocmd keepalt keepjumps call win_execute(a:winid, 'call s:Do()')
catch /.*/
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
endtry
unlet s:Do
return
endif
noautocmd keepalt keepjumps call win_gotoid(a:winid)
try
call a:func()
catch /.*/
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
endtry
noautocmd keepalt keepjumps call win_gotoid(l:curr_winid)
endfunction
"
" info
"
if has('nvim')
function! s:info(winid) abort
let l:info = getwininfo(a:winid)[0]
return {
\ 'width': l:info.width,
\ 'height': l:info.height,
\ 'topline': l:info.topline,
\ }
endfunction
else
function! s:info(winid) abort
if s:is_floating(a:winid)
let l:info = popup_getpos(a:winid)
return {
\ 'width': l:info.width,
\ 'height': l:info.height,
\ 'topline': l:info.firstline
\ }
endif
let l:ctx = {}
let l:ctx.info = {}
function! l:ctx.callback() abort
let self.info.width = winwidth(0)
let self.info.height = winheight(0)
let self.info.topline = line('w0')
endfunction
call s:do(a:winid, { -> l:ctx.callback() })
return l:ctx.info
endfunction
endif
"
" find
"
function! s:find(callback) abort
let l:winids = []
let l:winids += map(range(1, tabpagewinnr(tabpagenr(), '$')), 'win_getid(v:val)')
let l:winids += s:_get_visible_popup_winids()
return filter(l:winids, 'a:callback(v:val)')
endfunction
"
" is_floating
"
if has('nvim')
function! s:is_floating(winid) abort
let l:config = nvim_win_get_config(a:winid)
return empty(l:config) || !empty(get(l:config, 'relative', ''))
endfunction
else
function! s:is_floating(winid) abort
return winheight(a:winid) != -1 && win_id2win(a:winid) == 0
endfunction
endif
"
" scroll
"
function! s:scroll(winid, topline) abort
let l:ctx = {}
function! l:ctx.callback(winid, topline) abort
let l:wininfo = s:info(a:winid)
let l:topline = a:topline
let l:topline = min([l:topline, line('$') - l:wininfo.height + 1])
let l:topline = max([l:topline, 1])
if l:topline == l:wininfo.topline
return
endif
if !has('nvim') && s:is_floating(a:winid)
call popup_setoptions(a:winid, {
\ 'firstline': l:topline,
\ })
else
let l:delta = l:topline - l:wininfo.topline
let l:key = l:delta > 0 ? "\<C-e>" : "\<C-y>"
execute printf('noautocmd silent normal! %s', repeat(l:key, abs(l:delta)))
endif
endfunction
call s:do(a:winid, { -> l:ctx.callback(a:winid, a:topline) })
endfunction
"
" screenpos
"
" @param {[number, number]} pos - position on the current buffer.
"
function! s:screenpos(pos) abort
let l:y = a:pos[0]
let l:x = a:pos[1] + get(a:pos, 2, 0)
let l:view = winsaveview()
let l:scroll_x = l:view.leftcol
let l:scroll_y = l:view.topline
let l:winpos = win_screenpos(win_getid())
let l:y = l:winpos[0] + l:y - l:scroll_y
let l:x = l:winpos[1] + l:x - l:scroll_x
return [l:y, l:x + (wincol() - virtcol('.')) - 1]
endfunction
"
" _get_visible_popup_winids
"
function! s:_get_visible_popup_winids() abort
if !exists('*popup_list')
return []
endif
return filter(popup_list(), 'popup_getpos(v:val).visible')
endfunction

View File

@@ -0,0 +1,515 @@
" ___vital___
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
" Do not modify the code nor insert new lines before '" ___vital___'
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_lsp#VS#Vim#Window#FloatingWindow#import() abort', printf("return map({'_vital_depends': '', 'is_available': '', 'new': '', '_vital_loaded': ''}, \"vital#_lsp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
delfunction s:_SID
" ___vital___
"
" _vital_loaded
"
function! s:_vital_loaded(V) abort
let s:Window = a:V.import('VS.Vim.Window')
endfunction
"
" _vital_depends
"
function! s:_vital_depends() abort
return ['VS.Vim.Window']
endfunction
"
" managed floating windows.
"
let s:floating_windows = {}
"
" is_available
"
function! s:is_available() abort
if has('nvim')
return v:true
endif
return exists('*popup_create') && exists('*popup_close') && exists('*popup_move') && exists('*popup_getpos')
endfunction
"
" new
"
function! s:new(...) abort
call s:_init()
return s:FloatingWindow.new(get(a:000, 0, {}))
endfunction
"
" _notify_opened
"
" @param {number} winid
" @param {VS.Vim.Window.FloatingWindow} floating_window
"
function! s:_notify_opened(winid, floating_window) abort
let s:floating_windows[a:winid] = a:floating_window
call a:floating_window._on_opened()
endfunction
"
" _notify_closed
"
function! s:_notify_closed() abort
for [l:winid, l:floating_window] in items(s:floating_windows)
if winheight(l:winid) == -1
call l:floating_window._on_closed()
unlet s:floating_windows[l:winid]
endif
endfor
endfunction
let s:FloatingWindow = {}
"
" new
"
" @param {function?} args.on_opened
" @param {function?} args.on_closed
"
function! s:FloatingWindow.new(args) abort
return extend(deepcopy(s:FloatingWindow), {
\ '_winid': v:null,
\ '_bufnr': v:null,
\ '_vars': {},
\ '_on_opened': get(a:args, 'on_opened', { -> {} }),
\ '_on_closed': get(a:args, 'on_closed', { -> {} }),
\ })
endfunction
"
" get_size
"
" @param {number?} args.minwidth
" @param {number?} args.maxwidth
" @param {number?} args.minheight
" @param {number?} args.maxheight
" @param {boolean?} args.wrap
"
function! s:FloatingWindow.get_size(args) abort
if self._bufnr is# v:null
throw 'VS.Vim.Window.FloatingWindow: Failed to detect bufnr.'
endif
let l:maxwidth = get(a:args, 'maxwidth', -1)
let l:minwidth = get(a:args, 'minwidth', -1)
let l:maxheight = get(a:args, 'maxheight', -1)
let l:minheight = get(a:args, 'minheight', -1)
let l:lines = getbufline(self._bufnr, '^', '$')
" width
let l:width = 0
for l:line in l:lines
let l:width = max([l:width, strdisplaywidth(l:line)])
endfor
let l:width = l:minwidth == -1 ? l:width : max([l:minwidth, l:width])
let l:width = l:maxwidth == -1 ? l:width : min([l:maxwidth, l:width])
" height
if get(a:args, 'wrap', get(self._vars, '&wrap', 0))
let l:height = 0
for l:line in l:lines
let l:height += max([1, float2nr(ceil(strdisplaywidth(l:line) / str2float('' . l:width)))])
endfor
else
let l:height = len(l:lines)
endif
let l:height = l:minheight == -1 ? l:height : max([l:minheight, l:height])
let l:height = l:maxheight == -1 ? l:height : min([l:maxheight, l:height])
return {
\ 'width': max([1, l:width]),
\ 'height': max([1, l:height]),
\ }
endfunction
"
" set_bufnr
"
" @param {number} bufnr
"
function! s:FloatingWindow.set_bufnr(bufnr) abort
let self._bufnr = a:bufnr
endfunction
"
" get_bufnr
"
function! s:FloatingWindow.get_bufnr() abort
return self._bufnr
endfunction
"
" get_winid
"
function! s:FloatingWindow.get_winid() abort
if self.is_visible()
return self._winid
endif
return v:null
endfunction
"
" info
"
function! s:FloatingWindow.info() abort
if self.is_visible()
return s:_info(self._winid)
endif
return v:null
endfunction
"
" set_var
"
" @param {string} key
" @param {unknown} value
"
function! s:FloatingWindow.set_var(key, value) abort
let self._vars[a:key] = a:value
if self.is_visible()
call setwinvar(self._winid, a:key, a:value)
endif
endfunction
"
" get_var
"
" @param {string} key
"
function! s:FloatingWindow.get_var(key) abort
return self._vars[a:key]
endfunction
"
" open
"
" @param {number} args.row 0-based indexing
" @param {number} args.col 0-based indexing
" @param {number} args.width
" @param {number} args.height
" @param {boolean|[string]?} args.border - boolean, or list of characters
" clockwise from top-left (same as nvim_open_win() in neovim)
" @param {number?} args.topline
" @param {string?} args.origin - topleft/topright/botleft/botright
"
function! s:FloatingWindow.open(args) abort
let l:style = {
\ 'row': a:args.row,
\ 'col': a:args.col,
\ 'width': a:args.width,
\ 'height': a:args.height,
\ 'border': get(a:args, 'border', v:false),
\ 'topline': get(a:args, 'topline', 1),
\ 'origin': get(a:args, 'origin', 'topleft'),
\ }
let l:will_move = self.is_visible()
if l:will_move
let self._winid = s:_move(self, self._winid, self._bufnr, l:style)
else
let self._winid = s:_open(self._bufnr, l:style, { -> self._on_closed() })
endif
for [l:key, l:value] in items(self._vars)
call setwinvar(self._winid, l:key, l:value)
endfor
if !l:will_move
call s:_notify_opened(self._winid, self)
endif
endfunction
"
" close
"
function! s:FloatingWindow.close() abort
if self.is_visible()
call s:_close(self._winid)
endif
let self._winid = v:null
endfunction
"
" enter
"
function! s:FloatingWindow.enter() abort
call s:_enter(self._winid)
endfunction
"
" is_visible
"
function! s:FloatingWindow.is_visible() abort
return s:_exists(self._winid) ? v:true : v:false
endfunction
"
" open
"
if has('nvim')
function! s:_open(bufnr, style, callback) abort
let l:winid = nvim_open_win(a:bufnr, v:false, s:_style(a:style))
call s:Window.scroll(l:winid, a:style.topline)
return l:winid
endfunction
else
function! s:_open(bufnr, style, callback) abort
return popup_create(a:bufnr, extend(s:_style(a:style), {
\ 'callback': a:callback,
\ }, 'force'))
endfunction
endif
"
" close
"
if has('nvim')
function! s:_close(winid) abort
call nvim_win_close(a:winid, v:true)
call s:_notify_closed()
endfunction
else
function! s:_close(winid) abort
call popup_close(a:winid)
endfunction
endif
"
" move
"
if has('nvim')
function! s:_move(self, winid, bufnr, style) abort
call nvim_win_set_config(a:winid, s:_style(a:style))
if a:bufnr != nvim_win_get_buf(a:winid)
call nvim_win_set_buf(a:winid, a:bufnr)
endif
call s:Window.scroll(a:winid, a:style.topline)
return a:winid
endfunction
else
function! s:_move(self, winid, bufnr, style) abort
" vim's popup window can't change bufnr so open new popup in here.
if a:bufnr != winbufnr(a:winid)
let l:On_closed = a:self._on_closed
let a:self._on_closed = { -> {} }
call s:_close(a:winid)
let a:self._on_closed = l:On_closed
return s:_open(a:bufnr, a:style, { -> a:self._on_closed() })
endif
let l:style = s:_style(a:style)
call popup_move(a:winid, l:style)
call popup_setoptions(a:winid, l:style)
return a:winid
endfunction
endif
"
" enter
"
if has('nvim')
function! s:_enter(winid) abort
call win_gotoid(a:winid)
endfunction
else
function! s:_enter(winid) abort
" not supported.
endfunction
endif
"
" exists
"
if has('nvim')
function! s:_exists(winid) abort
try
return type(a:winid) == type(0) && nvim_win_is_valid(a:winid) && nvim_win_get_number(a:winid) != -1
catch /.*/
return v:false
endtry
endfunction
else
function! s:_exists(winid) abort
return type(a:winid) == type(0) && winheight(a:winid) != -1
endfunction
endif
"
" info
"
if has('nvim')
function! s:_info(winid) abort
let l:info = getwininfo(a:winid)[0]
return {
\ 'row': l:info.winrow,
\ 'col': l:info.wincol,
\ 'width': l:info.width,
\ 'height': l:info.height,
\ 'topline': l:info.topline,
\ }
endfunction
else
function! s:_info(winid) abort
let l:pos = popup_getpos(a:winid)
return {
\ 'row': l:pos.core_line,
\ 'col': l:pos.core_col,
\ 'width': l:pos.core_width,
\ 'height': l:pos.core_height,
\ 'topline': l:pos.firstline,
\ }
endfunction
endif
"
" style
"
if has('nvim')
function! s:_style(style) abort
let l:style = s:_resolve_origin(a:style)
let l:style = s:_resolve_border(l:style)
let l:style = {
\ 'relative': 'editor',
\ 'row': l:style.row - 1,
\ 'col': l:style.col - 1,
\ 'width': l:style.width,
\ 'height': l:style.height,
\ 'focusable': v:true,
\ 'style': 'minimal',
\ 'border': has_key(l:style, 'border') ? l:style.border : 'none',
\ }
if !exists('*win_execute') " We can't detect neovim features via patch version so we try it by function existence.
unlet l:style.border
endif
return l:style
endfunction
else
function! s:_style(style) abort
let l:style = s:_resolve_origin(a:style)
let l:style = s:_resolve_border(l:style)
return {
\ 'line': l:style.row,
\ 'col': l:style.col,
\ 'pos': 'topleft',
\ 'wrap': v:false,
\ 'moved': [0, 0, 0],
\ 'scrollbar': 0,
\ 'maxwidth': l:style.width,
\ 'maxheight': l:style.height,
\ 'minwidth': l:style.width,
\ 'minheight': l:style.height,
\ 'tabpage': 0,
\ 'firstline': l:style.topline,
\ 'padding': [0, 0, 0, 0],
\ 'border': has_key(l:style, 'border') ? [1, 1, 1, 1] : [0, 0, 0, 0],
\ 'borderchars': get(l:style, 'border', []),
\ 'fixed': v:true,
\ }
endfunction
endif
"
" _resolve_origin
"
function! s:_resolve_origin(style) abort
if index(['topleft', 'topright', 'bottomleft', 'bottomright', 'topcenter', 'bottomcenter'], a:style.origin) == -1
let a:style.origin = 'topleft'
endif
if a:style.origin ==# 'topleft'
let a:style.col = a:style.col
let a:style.row = a:style.row
elseif a:style.origin ==# 'topright'
let a:style.col = a:style.col - (a:style.width - 1)
let a:style.row = a:style.row
elseif a:style.origin ==# 'bottomleft'
let a:style.col = a:style.col
let a:style.row = a:style.row - (a:style.height - 1)
elseif a:style.origin ==# 'bottomright'
let a:style.col = a:style.col - (a:style.width - 1)
let a:style.row = a:style.row - (a:style.height - 1)
elseif a:style.origin ==# 'topcenter'
let a:style.col = a:style.col - float2nr(a:style.width / 2)
let a:style.row = a:style.row
elseif a:style.origin ==# 'bottomcenter'
let a:style.col = a:style.col - float2nr(a:style.width / 2)
let a:style.row = a:style.row - (a:style.height - 1)
elseif a:style.origin ==# 'centercenter'
let a:style.col = a:style.col - float2nr(a:style.width / 2)
let a:style.row = a:style.row - float2nr(a:style.height / 2)
endif
return a:style
endfunction
if has('nvim')
function! s:_resolve_border(style) abort
let l:border = get(a:style, 'border', v:null)
if !empty(l:border)
if type(l:border) != type([])
if &ambiwidth ==# 'single'
let a:style.border = ['┌', '─', '┐', '│', '┘', '─', '└', '│']
else
let a:style.border = ['+', '-', '+', '|', '+', '-', '+', '|']
endif
endif
elseif has_key(a:style, 'border')
unlet a:style.border
endif
return a:style
endfunction
else
function! s:_resolve_border(style) abort
let l:border = get(a:style, 'border', v:null)
if !empty(get(a:style, 'border', v:null))
if type(l:border) != type([])
if &ambiwidth ==# 'single'
let a:style.border = ['─', '│', '─', '│', '┌', '┐', '┘', '└']
else
let a:style.border = ['-', '|', '-', '|', '+', '+', '+', '+']
endif
else
" Emulate nvim behavior for lists of 1/2/4 elements
let l:topleft = l:border[0]
let l:top = get(l:border, 1, l:topleft)
let l:topright = get(l:border, 2, l:topleft)
let l:right = get(l:border, 3, l:top)
let l:bottomright = get(l:border, 4, l:topleft)
let l:bottom = get(l:border, 5, l:top)
let l:bottomleft = get(l:border, 6, l:topright)
let l:left = get(l:border, 7, l:right)
let a:style.border = [
\ l:top, l:right, l:bottom, l:left,
\ l:topleft, l:topright, l:bottomright, l:bottomleft,
\ ]
endif
elseif has_key(a:style, 'border')
unlet a:style.border
endif
return a:style
endfunction
endif
"
" init
"
let s:has_init = v:false
let s:filepath = expand('<sfile>:p')
function! s:_init() abort
if s:has_init || !has('nvim')
return
endif
let s:has_init = v:true
execute printf('augroup VS_Vim_Window_FloatingWindow:%s', s:filepath)
autocmd!
autocmd WinEnter * call <SID>_notify_closed()
augroup END
endfunction

View File

@@ -0,0 +1,334 @@
let s:plugin_name = expand('<sfile>:t:r')
let s:vital_base_dir = expand('<sfile>:h')
let s:project_root = expand('<sfile>:h:h:h')
let s:is_vital_vim = s:plugin_name is# 'vital'
let s:loaded = {}
let s:cache_sid = {}
function! vital#{s:plugin_name}#new() abort
return s:new(s:plugin_name)
endfunction
function! vital#{s:plugin_name}#import(...) abort
if !exists('s:V')
let s:V = s:new(s:plugin_name)
endif
return call(s:V.import, a:000, s:V)
endfunction
let s:Vital = {}
function! s:new(plugin_name) abort
let base = deepcopy(s:Vital)
let base._plugin_name = a:plugin_name
return base
endfunction
function! s:vital_files() abort
if !exists('s:vital_files')
let s:vital_files = map(
\ s:is_vital_vim ? s:_global_vital_files() : s:_self_vital_files(),
\ 'fnamemodify(v:val, ":p:gs?[\\\\/]?/?")')
endif
return copy(s:vital_files)
endfunction
let s:Vital.vital_files = function('s:vital_files')
function! s:import(name, ...) abort dict
let target = {}
let functions = []
for a in a:000
if type(a) == type({})
let target = a
elseif type(a) == type([])
let functions = a
endif
unlet a
endfor
let module = self._import(a:name)
if empty(functions)
call extend(target, module, 'keep')
else
for f in functions
if has_key(module, f) && !has_key(target, f)
let target[f] = module[f]
endif
endfor
endif
return target
endfunction
let s:Vital.import = function('s:import')
function! s:load(...) abort dict
for arg in a:000
let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg]
let target = split(join(as, ''), '\W\+')
let dict = self
let dict_type = type({})
while !empty(target)
let ns = remove(target, 0)
if !has_key(dict, ns)
let dict[ns] = {}
endif
if type(dict[ns]) == dict_type
let dict = dict[ns]
else
unlet dict
break
endif
endwhile
if exists('dict')
call extend(dict, self._import(name))
endif
unlet arg
endfor
return self
endfunction
let s:Vital.load = function('s:load')
function! s:unload() abort dict
let s:loaded = {}
let s:cache_sid = {}
unlet! s:vital_files
endfunction
let s:Vital.unload = function('s:unload')
function! s:exists(name) abort dict
if a:name !~# '\v^\u\w*%(\.\u\w*)*$'
throw 'vital: Invalid module name: ' . a:name
endif
return s:_module_path(a:name) isnot# ''
endfunction
let s:Vital.exists = function('s:exists')
function! s:search(pattern) abort dict
let paths = s:_extract_files(a:pattern, self.vital_files())
let modules = sort(map(paths, 's:_file2module(v:val)'))
return uniq(modules)
endfunction
let s:Vital.search = function('s:search')
function! s:plugin_name() abort dict
return self._plugin_name
endfunction
let s:Vital.plugin_name = function('s:plugin_name')
function! s:_self_vital_files() abort
let builtin = printf('%s/__%s__/', s:vital_base_dir, s:plugin_name)
let installed = printf('%s/_%s/', s:vital_base_dir, s:plugin_name)
let base = builtin . ',' . installed
return split(globpath(base, '**/*.vim', 1), "\n")
endfunction
function! s:_global_vital_files() abort
let pattern = 'autoload/vital/__*__/**/*.vim'
return split(globpath(&runtimepath, pattern, 1), "\n")
endfunction
function! s:_extract_files(pattern, files) abort
let tr = {'.': '/', '*': '[^/]*', '**': '.*'}
let target = substitute(a:pattern, '\.\|\*\*\?', '\=tr[submatch(0)]', 'g')
let regexp = printf('autoload/vital/[^/]\+/%s.vim$', target)
return filter(a:files, 'v:val =~# regexp')
endfunction
function! s:_file2module(file) abort
let filename = fnamemodify(a:file, ':p:gs?[\\/]?/?')
let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$')
return join(split(tail, '[\\/]\+'), '.')
endfunction
" @param {string} name e.g. Data.List
function! s:_import(name) abort dict
if has_key(s:loaded, a:name)
return copy(s:loaded[a:name])
endif
let module = self._get_module(a:name)
if has_key(module, '_vital_created')
call module._vital_created(module)
endif
let export_module = filter(copy(module), 'v:key =~# "^\\a"')
" Cache module before calling module._vital_loaded() to avoid cyclic
" dependences but remove the cache if module._vital_loaded() fails.
" let s:loaded[a:name] = export_module
let s:loaded[a:name] = export_module
if has_key(module, '_vital_loaded')
try
call module._vital_loaded(vital#{s:plugin_name}#new())
catch
unlet s:loaded[a:name]
throw 'vital: fail to call ._vital_loaded(): ' . v:exception . " from:\n" . s:_format_throwpoint(v:throwpoint)
endtry
endif
return copy(s:loaded[a:name])
endfunction
let s:Vital._import = function('s:_import')
function! s:_format_throwpoint(throwpoint) abort
let funcs = []
let stack = matchstr(a:throwpoint, '^function \zs.*, .\{-} \d\+$')
for line in split(stack, '\.\.')
let m = matchlist(line, '^\(.\+\)\%(\[\(\d\+\)\]\|, .\{-} \(\d\+\)\)$')
if !empty(m)
let [name, lnum, lnum2] = m[1:3]
if empty(lnum)
let lnum = lnum2
endif
let info = s:_get_func_info(name)
if !empty(info)
let attrs = empty(info.attrs) ? '' : join([''] + info.attrs)
let flnum = info.lnum == 0 ? '' : printf(' Line:%d', info.lnum + lnum)
call add(funcs, printf('function %s(...)%s Line:%d (%s%s)',
\ info.funcname, attrs, lnum, info.filename, flnum))
continue
endif
endif
" fallback when function information cannot be detected
call add(funcs, line)
endfor
return join(funcs, "\n")
endfunction
" @vimlint(EVL102, 1, l:_)
" @vimlint(EVL102, 1, l:__)
function! s:_get_func_info(name) abort
let name = a:name
if a:name =~# '^\d\+$' " is anonymous-function
let name = printf('{%s}', a:name)
elseif a:name =~# '^<lambda>\d\+$' " is lambda-function
let name = printf("{'%s'}", a:name)
endif
if !exists('*' . name)
return {}
endif
let body = execute(printf('verbose function %s', name))
let lines = split(body, "\n")
let signature = matchstr(lines[0], '^\s*\zs.*')
let [_, file, lnum; __] = matchlist(lines[1],
\ '^\t\%(Last set from\|.\{-}:\)\s*\zs\(.\{-}\)\%( \S\+ \(\d\+\)\)\?$')
return {
\ 'filename': substitute(file, '[/\\]\+', '/', 'g'),
\ 'lnum': 0 + lnum,
\ 'funcname': a:name,
\ 'arguments': split(matchstr(signature, '(\zs.*\ze)'), '\s*,\s*'),
\ 'attrs': filter(['dict', 'abort', 'range', 'closure'], 'signature =~# (").*" . v:val)'),
\ }
endfunction
" @vimlint(EVL102, 0, l:__)
" @vimlint(EVL102, 0, l:_)
" s:_get_module() returns module object wihch has all script local functions.
function! s:_get_module(name) abort dict
let funcname = s:_import_func_name(self.plugin_name(), a:name)
try
return call(funcname, [])
catch /^Vim\%((\a\+)\)\?:E117:/
return s:_get_builtin_module(a:name)
endtry
endfunction
function! s:_get_builtin_module(name) abort
return s:sid2sfuncs(s:_module_sid(a:name))
endfunction
if s:is_vital_vim
" For vital.vim, we can use s:_get_builtin_module directly
let s:Vital._get_module = function('s:_get_builtin_module')
else
let s:Vital._get_module = function('s:_get_module')
endif
function! s:_import_func_name(plugin_name, module_name) abort
return printf('vital#_%s#%s#import', a:plugin_name, s:_dot_to_sharp(a:module_name))
endfunction
function! s:_module_sid(name) abort
let path = s:_module_path(a:name)
if !filereadable(path)
throw 'vital: module not found: ' . a:name
endif
let vital_dir = s:is_vital_vim ? '__\w\+__' : printf('_\{1,2}%s\%%(__\)\?', s:plugin_name)
let base = join([vital_dir, ''], '[/\\]\+')
let p = base . substitute('' . a:name, '\.', '[/\\\\]\\+', 'g')
let sid = s:_sid(path, p)
if !sid
call s:_source(path)
let sid = s:_sid(path, p)
if !sid
throw printf('vital: cannot get <SID> from path: %s', path)
endif
endif
return sid
endfunction
function! s:_module_path(name) abort
return get(s:_extract_files(a:name, s:vital_files()), 0, '')
endfunction
function! s:_module_sid_base_dir() abort
return s:is_vital_vim ? &rtp : s:project_root
endfunction
function! s:_dot_to_sharp(name) abort
return substitute(a:name, '\.', '#', 'g')
endfunction
function! s:_source(path) abort
execute 'source' fnameescape(a:path)
endfunction
" @vimlint(EVL102, 1, l:_)
" @vimlint(EVL102, 1, l:__)
function! s:_sid(path, filter_pattern) abort
let unified_path = s:_unify_path(a:path)
if has_key(s:cache_sid, unified_path)
return s:cache_sid[unified_path]
endif
for line in filter(split(execute(':scriptnames'), "\n"), 'v:val =~# a:filter_pattern')
let [_, sid, path; __] = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$')
if s:_unify_path(path) is# unified_path
let s:cache_sid[unified_path] = sid
return s:cache_sid[unified_path]
endif
endfor
return 0
endfunction
if filereadable(expand('<sfile>:r') . '.VIM') " is case-insensitive or not
let s:_unify_path_cache = {}
" resolve() is slow, so we cache results.
" Note: On windows, vim can't expand path names from 8.3 formats.
" So if getting full path via <sfile> and $HOME was set as 8.3 format,
" vital load duplicated scripts. Below's :~ avoid this issue.
function! s:_unify_path(path) abort
if has_key(s:_unify_path_cache, a:path)
return s:_unify_path_cache[a:path]
endif
let value = tolower(fnamemodify(resolve(fnamemodify(
\ a:path, ':p')), ':~:gs?[\\/]?/?'))
let s:_unify_path_cache[a:path] = value
return value
endfunction
else
function! s:_unify_path(path) abort
return resolve(fnamemodify(a:path, ':p:gs?[\\/]?/?'))
endfunction
endif
" copied and modified from Vim.ScriptLocal
let s:SNR = join(map(range(len("\<SNR>")), '"[\\x" . printf("%0x", char2nr("\<SNR>"[v:val])) . "]"'), '')
function! s:sid2sfuncs(sid) abort
let fs = split(execute(printf(':function /^%s%s_', s:SNR, a:sid)), "\n")
let r = {}
let pattern = printf('\m^function\s<SNR>%d_\zs\w\{-}\ze(', a:sid)
for fname in map(fs, 'matchstr(v:val, pattern)')
let r[fname] = function(s:_sfuncname(a:sid, fname))
endfor
return r
endfunction
"" Return funcname of script local functions with SID
function! s:_sfuncname(sid, funcname) abort
return printf('<SNR>%s_%s', a:sid, a:funcname)
endfunction

View File

@@ -0,0 +1,8 @@
lsp
b1e91b41f5028d65fa3d31a425ff21591d5d957f
VS.LSP.MarkupContent
VS.Vim.Window.FloatingWindow
VS.Vim.Syntax.Markdown
VS.Vim.Buffer
VS.Vim.Window