add initial support for type hierarchy (#641)

This commit is contained in:
Prabir Shrestha
2019-12-31 17:12:55 -08:00
committed by GitHub
parent 2afe76df7b
commit 67caa411c8
8 changed files with 407 additions and 1 deletions

View File

@@ -33,3 +33,15 @@ their own copyright notices and license terms:
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* vim-yggdrasil - https://github.com/m-pilia/vim-yggdrasil
* autoload/utils/tree.vim
====================================================================
Copyright 2019 Martino Pilia <martino.pilia@gmail.com>
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.

View File

@@ -115,6 +115,7 @@ Refer to `:h vim-lsp-semantic` for more info.
|`:LspRename`| Rename symbol |
|`:LspStatus` | Show the status of the language server |
|`:LspTypeDefinition`| Go to the type definition of the word under the cursor, and open in the current window |
|`:LspTypeHierarchy`| View type hierarchy of the symbol under the cursor |
|`:LspWorkspaceSymbol`| Search/Show workspace symbol |
### Diagnostics

View File

@@ -451,7 +451,8 @@ function! lsp#default_get_supported_capabilities(server_info) abort
\ },
\ 'semanticHighlightingCapabilities': {
\ 'semanticHighlighting': lsp#ui#vim#semantic#is_enabled()
\ }
\ },
\ 'typeHierarchy': v:false,
\ }
\ }
endfunction

View File

@@ -69,6 +69,10 @@ function! lsp#capabilities#has_type_definition_provider(server_name) abort
return s:has_bool_provider(a:server_name, 'typeDefinitionProvider')
endfunction
function! lsp#capabilities#has_type_hierarchy_provider(server_name) abort
return s:has_bool_provider(a:server_name, 'typeHierarchyProvider')
endfunction
function! lsp#capabilities#has_document_highlight_provider(server_name) abort
return s:has_bool_provider(a:server_name, 'documentHighlightProvider')
endfunction

View File

@@ -52,6 +52,32 @@ function! lsp#ui#vim#type_definition(in_preview) abort
echo 'Retrieving type definition ...'
endfunction
function! lsp#ui#vim#type_hierarchy() abort
let l:servers = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_type_hierarchy_provider(v:val)')
let s:last_req_id = s:last_req_id + 1
if len(l:servers) == 0
call s:not_supported('Retrieving type hierarchy')
return
endif
let l:ctx = { 'counter': len(l:servers), 'list':[], 'last_req_id': s:last_req_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! lsp#ui#vim#declaration(in_preview) abort
let l:servers = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_declaration_provider(v:val)')
let s:last_req_id = s:last_req_id + 1
@@ -630,6 +656,84 @@ function! s:handle_code_action(server, last_req_id, type, data) abort
endif
endfunction
function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_req_id}
if a:ctx['last_req_id'] != s:last_req_id
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
let l:path = lsp#utils#uri_to_path(a:hierarchyitem['uri'])
let l:line = a:hierarchyitem['range']['start']['line'] + 1
let l:char = a:hierarchyitem['range']['start']['character']
let l:col = lsp#utils#to_col(l:path, l:line, l:char)
let l:buffer = bufnr(l:path)
if &modified && !&hidden
let l:cmd = l:buffer !=# -1 ? 'sb ' . l:buffer : 'split ' . fnameescape(l:path)
else
echom 'edit'
let l:cmd = l:buffer !=# -1 ? 'b ' . l:buffer : 'edit ' . fnameescape(l:path)
endif
execute l:cmd . ' | call cursor('.l:line.','.l:col.')'
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
" @params
" server - string
" comand_or_code_action - Command | CodeAction

276
autoload/lsp/utils/tree.vim Normal file
View File

@@ -0,0 +1,276 @@
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
" Apply syntax to an LspTree 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 LspTree 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>
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)
nnoremap <silent> <buffer> q :q<cr>
endif
endfunction
" Turns the current buffer into an LspTree 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'),
\ }
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

@@ -78,6 +78,7 @@ CONTENTS *vim-lsp-contents*
LspRename |:LspRename|
LspSemanticScopes |:LspSemanticScopes|
LspTypeDefinition |:LspTypeDefinition|
LspTypeHierarchy |:LspTypeHierarchy|
LspWorkspaceSymbol |:LspWorkspaceSymbol|
LspStatus |:LspStatus|
LspStopServer |:LspStopServer|
@@ -1049,6 +1050,10 @@ LspTypeDefinition *:LspTypeDefinition*
Go to the type definition.
LspTypeHierarchy *:LspTypeHierarchy*
View type hierarchy for the symbol under cursor.
Also see |:LspPeekTypeDefinition|.
LspWorkspaceSymbol *:LspWorkspaceSymbol*
@@ -1153,6 +1158,7 @@ Available plug mappings are following:
nnoremap <plug>(lsp-peek-implementation)
nnoremap <plug>(lsp-type-definition)
nnoremap <plug>(lsp-peek-type-definition)
nnoremap <plug>(lsp-type-hierarchy)
nnoremap <plug>(lsp-status)
nnoremap <plug>(lsp-signature-help)

View File

@@ -69,6 +69,7 @@ command! LspPreviousDiagnostic call lsp#ui#vim#diagnostics#previous_diagnostic()
command! LspReferences call lsp#ui#vim#references()
command! LspRename call lsp#ui#vim#rename()
command! LspTypeDefinition call lsp#ui#vim#type_definition(0)
command! LspTypeHierarchy call lsp#ui#vim#type_hierarchy()
command! LspPeekTypeDefinition call lsp#ui#vim#type_definition(1)
command! LspWorkspaceSymbol call lsp#ui#vim#workspace_symbol()
command! -range LspDocumentFormat call lsp#ui#vim#document_format()
@@ -105,6 +106,7 @@ nnoremap <plug>(lsp-previous-diagnostic) :<c-u>call lsp#ui#vim#diagnostics#previ
nnoremap <plug>(lsp-references) :<c-u>call lsp#ui#vim#references()<cr>
nnoremap <plug>(lsp-rename) :<c-u>call lsp#ui#vim#rename()<cr>
nnoremap <plug>(lsp-type-definition) :<c-u>call lsp#ui#vim#type_definition(0)<cr>
nnoremap <plug>(lsp-type-hierarchy) :<c-u>call lsp#ui#vim#type_hierarchy()<cr>
nnoremap <plug>(lsp-peek-type-definition) :<c-u>call lsp#ui#vim#type_definition(1)<cr>
nnoremap <plug>(lsp-workspace-symbol) :<c-u>call lsp#ui#vim#workspace_symbol()<cr>
nnoremap <plug>(lsp-document-format) :<c-u>call lsp#ui#vim#document_format()<cr>