diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY index 3693f9495..8c97603ca 100644 --- a/LICENSE-THIRD-PARTY +++ b/LICENSE-THIRD-PARTY @@ -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 + +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. diff --git a/README.md b/README.md index 8247bd8e5..301fabefb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/autoload/lsp.vim b/autoload/lsp.vim index ccbfd711a..0bc545d6b 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -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 diff --git a/autoload/lsp/capabilities.vim b/autoload/lsp/capabilities.vim index 2ad5a99b5..c604b7ebb 100644 --- a/autoload/lsp/capabilities.vim +++ b/autoload/lsp/capabilities.vim @@ -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 diff --git a/autoload/lsp/ui/vim.vim b/autoload/lsp/ui/vim.vim index 15ad1e506..2e5a90d05 100644 --- a/autoload/lsp/ui/vim.vim +++ b/autoload/lsp/ui/vim.vim @@ -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 diff --git a/autoload/lsp/utils/tree.vim b/autoload/lsp/utils/tree.vim new file mode 100644 index 000000000..149b79954 --- /dev/null +++ b/autoload/lsp/utils/tree.vim @@ -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 (lsp-tree-toggle-node) + \ :call b:lsp_tree.set_collapsed_under_cursor(-1) + + nnoremap (lsp-tree-open-node) + \ :call b:lsp_tree.set_collapsed_under_cursor(v:false) + + nnoremap (lsp-tree-close-node) + \ :call b:lsp_tree.set_collapsed_under_cursor(v:true) + + nnoremap (lsp-tree-execute-node) + \ :call b:lsp_tree.exec_node_under_cursor() + + if !exists('g:lsp_tree_no_default_maps') + nmap o (lsp-tree-toggle-node) + nmap (lsp-tree-execute-node) + + nnoremap q :q + 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 call s:tree_render(b:lsp_tree) + augroup END + + setlocal filetype=lsp-tree + + call b:lsp_tree.update() +endfunction diff --git a/doc/vim-lsp.txt b/doc/vim-lsp.txt index d7df18521..6df3d27c8 100644 --- a/doc/vim-lsp.txt +++ b/doc/vim-lsp.txt @@ -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 (lsp-peek-implementation) nnoremap (lsp-type-definition) nnoremap (lsp-peek-type-definition) + nnoremap (lsp-type-hierarchy) nnoremap (lsp-status) nnoremap (lsp-signature-help) diff --git a/plugin/lsp.vim b/plugin/lsp.vim index 18f1792fb..2ea71a929 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -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 (lsp-previous-diagnostic) :call lsp#ui#vim#diagnostics#previ nnoremap (lsp-references) :call lsp#ui#vim#references() nnoremap (lsp-rename) :call lsp#ui#vim#rename() nnoremap (lsp-type-definition) :call lsp#ui#vim#type_definition(0) +nnoremap (lsp-type-hierarchy) :call lsp#ui#vim#type_hierarchy() nnoremap (lsp-peek-type-definition) :call lsp#ui#vim#type_definition(1) nnoremap (lsp-workspace-symbol) :call lsp#ui#vim#workspace_symbol() nnoremap (lsp-document-format) :call lsp#ui#vim#document_format()