├── autoload └── folding_nvim.vim ├── README.md ├── LICENCE └── lua └── folding.lua /autoload/folding_nvim.vim: -------------------------------------------------------------------------------- 1 | function! folding_nvim#foldexpr() 2 | return luaeval(printf('require"folding".get_fold_indic(%d)', v:lnum)) 3 | endfunction 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LSP-Powered folding plugin for neovim. 2 | 3 | 4 | # Requirements 5 | 6 | Neovim nightly, `nvim_lsp` as well as the language servers you want to use this plugin with. 7 | 8 | 9 | # Installation 10 | 11 | Install this plugin using your favorite package manager, or clone the source code inside 12 | `/path_to_nvim_config_folder/pack/opt/*/` where `*` stands fot the standard wildcard. 13 | 14 | 15 | Example (assuming your plugins are stored inside `~/.config/nvim/pack/github/opt`) 16 | ```sh 17 | git clone https://github.com/pierreglaser/folding-nvim ~/.config/nvim/pack/github/opt/folding-nvim 18 | ``` 19 | 20 | And make sure the plugin is loaded at initialization by placing the following inside your `init.vim` 21 | 22 | ```vim 23 | packadd folding-nvim 24 | ``` 25 | 26 | # Configuration 27 | 28 | Neovim needs to add the `folding.on_attach` callback to each language server you want to use this plugin with. For instance, for 29 | `palantir/python-language-server`, add those following lua lines to your `vimrc`: 30 | ```lua 31 | lua << EOF 32 | function on_attach_callback(client, bufnr) 33 | -- If you use completion-nvim/diagnostic-nvim, uncomment those two lines. 34 | -- require('diagnostic').on_attach() 35 | -- require('completion').on_attach() 36 | require('folding').on_attach() 37 | end 38 | 39 | require'nvim_lsp'.pyls.setup{on_attach=on_attach_callback} 40 | EOF 41 | ``` 42 | 43 | 44 | # LICENCE 45 | 46 | BSD 3-Clause Licence. 47 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | This module was created by Pierre Glaser 2 | 3 | Copyright (c) 2020, Pierre Glaser 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of the University of California, Berkeley nor the 15 | names of its contributors may be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 25 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /lua/folding.lua: -------------------------------------------------------------------------------- 1 | local lsp =vim.lsp 2 | local api=vim.api 3 | 4 | local M = {} 5 | 6 | -- TODO: per-buffer fold table? 7 | M.current_buf_folds = {} 8 | 9 | -- Informative table keeping track of language servers that implement textDocument/foldingRange. 10 | -- Not used at runtime (capability is resolved dynamically) 11 | M.servers_supporting_folding = { 12 | pyls = true, 13 | pyright = false, 14 | sumneko_lua = true, 15 | texlab = true, 16 | clangd = false, 17 | julials = false, 18 | } 19 | 20 | M.active_folding_clients = {} 21 | 22 | 23 | function M.on_attach() 24 | M.setup_plugin() 25 | M.update_folds() 26 | end 27 | 28 | 29 | function M.setup_plugin() 30 | api.nvim_command("augroup FoldingCommand") 31 | api.nvim_command("autocmd! * ") 32 | api.nvim_command("autocmd BufEnter lua require'folding'.update_folds()") 33 | api.nvim_command("autocmd BufWritePost lua require'folding'.update_folds()") 34 | api.nvim_command("augroup end") 35 | 36 | local clients = vim.lsp.buf_get_clients() 37 | 38 | for _, client in pairs(clients) do 39 | local client_id = client['id'] 40 | if M.active_folding_clients[client_id] == nil then 41 | local server_supports_folding = client['server_capabilities']['foldingRangeProvider'] or false 42 | if not server_supports_folding then 43 | api.nvim_command(string.format('echom "lsp-folding: %s does not provide folding requests"', client['name'])) 44 | end 45 | 46 | M.active_folding_clients[client_id] = server_supports_folding 47 | end 48 | end 49 | end 50 | 51 | 52 | 53 | function M.update_folds() 54 | local current_window = api.nvim_get_current_win() 55 | local in_diff_mode = api.nvim_win_get_option(current_window, 'diff') 56 | if in_diff_mode then 57 | -- In diff mode, use diff folding. 58 | api.nvim_win_set_option(current_window, 'foldmethod', 'diff') 59 | else 60 | local clients = lsp.buf_get_clients(0) 61 | for client_id, client in pairs(clients) do 62 | if M.active_folding_clients[client_id] then 63 | -- XXX: better to pass callback in this method or add it directly in the config? 64 | -- client.config.callbacks['textDocument/foldingRange'] = M.fold_handler 65 | local current_bufnr = api.nvim_get_current_buf() 66 | local params = { uri = vim.uri_from_bufnr(current_bufnr) } 67 | client.request('textDocument/foldingRange', {textDocument = params}, M.fold_handler, current_bufnr) 68 | end 69 | end 70 | end 71 | end 72 | 73 | 74 | function M.debug_folds() 75 | for _, table in ipairs(M.current_buf_folds) do 76 | local start_line = table['startLine'] 77 | local end_line = table['endLine'] 78 | print('startline', start_line, 'endline', end_line) 79 | end 80 | end 81 | 82 | 83 | function M.fold_handler(err, result, ctx, config) 84 | -- params: err, method, result, client_id, bufnr 85 | -- XXX: handle err? 86 | local current_bufnr = api.nvim_get_current_buf() 87 | -- Discard the folding result if buffer focus has changed since the request was 88 | -- done. 89 | if current_bufnr == ctx.bufnr then 90 | if err == nil and result == nil then 91 | -- client wont return a valid result in early stages after initialization 92 | -- XXX: this is dirty 93 | vim.wait(100) 94 | M.update_folds() 95 | else 96 | for _, fold in ipairs(result) do 97 | fold['startLine'] = M.adjust_foldstart(fold['startLine']) 98 | fold['endLine'] = M.adjust_foldend(fold['endLine']) 99 | end 100 | table.sort(result, function(a, b) return a['startLine'] < b['startLine'] end) 101 | M.current_buf_folds = result 102 | local current_window = api.nvim_get_current_win() 103 | api.nvim_win_set_option(current_window, 'foldmethod', 'expr') 104 | api.nvim_win_set_option(current_window, 'foldexpr', 'folding_nvim#foldexpr()') 105 | end 106 | end 107 | end 108 | 109 | 110 | function M.adjust_foldstart(line_no) 111 | return line_no + 1 112 | end 113 | 114 | 115 | function M.adjust_foldend(line_no) 116 | local bufnr = api.nvim_get_current_buf() 117 | local filetype = api.nvim_buf_get_option(bufnr, 'filetype') 118 | if filetype == 'lua' then 119 | return line_no + 2 120 | else 121 | return line_no + 1 122 | end 123 | end 124 | 125 | 126 | function M.get_fold_indic(lnum) 127 | local fold_level = 0 128 | local is_foldstart = false 129 | local is_foldend = false 130 | 131 | for _, table in ipairs(M.current_buf_folds) do 132 | local start_line = table['startLine'] 133 | local end_line = table['endLine'] 134 | 135 | -- can exit early b/c folds get pre-orderered manually 136 | if lnum < start_line then 137 | break 138 | end 139 | 140 | if lnum >= start_line and lnum <= end_line then 141 | fold_level = fold_level + 1 142 | if lnum == start_line then 143 | is_foldstart = true 144 | end 145 | if lnum == end_line then 146 | is_foldend = true 147 | end 148 | end 149 | end 150 | 151 | if is_foldend and is_foldstart then 152 | -- If line marks both start and end of folds (like ``else`` statement), 153 | -- merge the two folds into one by returning the current foldlevel 154 | -- without any marker. 155 | return fold_level 156 | elseif is_foldstart then 157 | return string.format(">%d", fold_level) 158 | elseif is_foldend then 159 | return string.format("<%d", fold_level) 160 | else 161 | return fold_level 162 | end 163 | 164 | end 165 | 166 | 167 | return M 168 | --------------------------------------------------------------------------------