├── .gitignore ├── LICENSE ├── README.md ├── autoload └── foldout.vim ├── doc └── foldout.txt └── plugin └── foldout.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Matt Superdock 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-foldout 2 | 3 | foldout is an outline-based folding plugin for vim & neovim. Its unique feature 4 | is that folds are determined by Markdown-style headings within comments, and 5 | these headings are automatically highlighted. foldout also provides a suite of 6 | functions for manipulating and navigating between headings. 7 | 8 | foldout uses vim's `commentstring` option to compute default heading patterns. 9 | As a result, if you're using a filetype-specific plugin that sets 10 | `commentstring`, no further configuration is necessary. Just type the comment 11 | prefix, then a string of heading symbols (by default, `#`), and foldout will 12 | recognize a heading. 13 | 14 | ## Installation 15 | 16 | Use your preferred installation method. It's recommended to also install 17 | [FastFold](https://github.com/Konfekt/FastFold/blob/master/plugin/fastfold.vim); 18 | this is optional but will make folding operations faster. For example, with 19 | [vim-plug](https://github.com/junegunn/vim-plug), use: 20 | 21 | ``` 22 | Plug 'Konfekt/FastFold' 23 | Plug 'msuperdock/vim-foldout' 24 | ``` 25 | 26 | If your `.vimrc` uses `mkview` or `loadview` to save and restore view data, 27 | remove these commands and set `g:foldout_save`. (The `mkview` and `loadview` 28 | commands need to be called in certain sequence with foldout commands, and 29 | foldout handles this for you.) For example: 30 | 31 | ``` 32 | let g:foldout_save = 1 33 | ``` 34 | 35 | By default, foldout is automatically enabled in all buffers whose filenames 36 | contain a dot (see option `g:foldout_files` to customize this), and can be 37 | manually enabled & disabled using `:call foldout#enable()`, 38 | `:call foldout#disable()`, and `call foldout#toggle()`. 39 | 40 | ## Functions 41 | 42 | foldout provides the following functions. You can bind a key to a function in 43 | your `.vimrc` using, for example: 44 | 45 | ``` 46 | noremap :call foldout#toggle_fold() 47 | ``` 48 | 49 | This binds `` to the `foldout#toggle_fold()` function in normal, visual, 50 | select, and operator-pending modes. The `` flag tells vim to call the 51 | function without first displaying the keybinding. 52 | 53 | ### Enable 54 | 55 | | function | description | 56 | | --- | --- | 57 | | `foldout#enable()` | Enable foldout in the current buffer. | 58 | | `foldout#disable()` | Disable foldout in the current buffer. | 59 | | `foldout#toggle()` | Toggle foldout in the current buffer. | 60 | 61 | ### Headings 62 | 63 | | function | description | 64 | | --- | --- | 65 | | `foldout#level()` | Get level of heading, or 0 if not at a heading. | 66 | | `foldout#demote()` | Demote current heading. Don't change children. | 67 | | `foldout#promote()` | Promote current heading. Don't change children. | 68 | | `foldout#tab()` | Demote if at heading, else simulate tab. | 69 | | `foldout#shift_tab()` | Promote if at heading, else simulate shift-tab. | 70 | 71 | The `foldout#tab()` and `foldout#shift_tab()` functions are designed to be 72 | bound to tab and shift-tab in insert mode; for example: 73 | 74 | ``` 75 | inoremap :silent call foldout#tab() 76 | inoremap :silent call foldout#shift_tab() 77 | ``` 78 | 79 | ### Navigation 80 | 81 | | function | description | 82 | | --- | --- | 83 | | `foldout#parent()` | Go to parent heading. | 84 | | `foldout#down()` | Go to next sibling heading. | 85 | | `foldout#up()` | Go to previous sibling heading. | 86 | | `foldout#down_graphical()` | Go to next visible heading of any level. | 87 | | `foldout#up_graphical()` | Go to previous visible heading of any level. | 88 | | `foldout#top()` | Go to first sibling heading. | 89 | | `foldout#bottom()` | Go to last sibling heading. | 90 | | `foldout#child()` | Go to first child heading. | 91 | | `foldout#goto(name, level)` | Go to heading with given name & level. | 92 | 93 | ### Folding 94 | 95 | | function | description | 96 | | --- | --- | 97 | | `foldout#toggle_fold()` | Toggle fold at cursor. | 98 | | `foldout#show()` | Open all folds. | 99 | | `foldout#focus()` | Close all folds but those needed to see cursor. | 100 | | `foldout#center()` | Redraw so that cursor is vertically centered. | 101 | 102 | ### Insertion 103 | 104 | | function | description | 105 | | --- | --- | 106 | | `foldout#append()` | Go to end of current section, enter insert mode. | 107 | | `foldout#open()` | Create heading below and enter insert mode. | 108 | 109 | ### Query 110 | 111 | | function | description | 112 | | --- | --- | 113 | | `foldout#syntax()` | View the stack of syntax groups at the cursor. | 114 | 115 | ## Options 116 | 117 | foldout provides the following option variables for configuration. The prefix 118 | column indicates whether the option is a global option, a buffer-local option, 119 | or both. Buffer-local options override global options where both are present. 120 | You can set an option in your `.vimrc` using, for example: 121 | 122 | ``` 123 | let g:foldout_heading_symbol = '*' 124 | ``` 125 | 126 | | prefix | variable | default | description | 127 | | --- | --- | --- | --- | 128 | | `g` | `foldout_files` | `'*.*'` | Pattern determining whether to enable foldout. | 129 | | `g` | `foldout_save` | 0 | Allow foldout to save & restore view data. | 130 | | `b, g` | `foldout_heading_symbol` | `'#'` | Repeated symbol indicating heading level. | 131 | | `b, g` | `foldout_max_level` | 6 | Maximum allowed heading level. | 132 | | `b, g` | `foldout_min_fold` | 1 | Minimum level at which to enable folding. | 133 | | `b, g` | `foldout_append_pattern` | `'\@!'` | Pattern determining whether to insert empty line in `foldout#append()`. | 134 | | `b, g` | `foldout_append_text` | `''` | Prefix text to insert in `foldout#append()`. | 135 | | `b` | `foldout_heading_comment` | 1 (0 for markdown) | Highlight heading delimiters as comments. | 136 | | `b` | `foldout_heading_ignore` | `'\@!'` | Pattern determining whether to ignore section. | 137 | | `b` | `foldout_heading_string` | `commentstring` (`'%s'` for markdown) | Heading pattern, in `commentstring` format. | 138 | 139 | Note that `'\@!'` is a pattern which never matches. See the vim help files (e.g. 140 | `:h foldout_options`) for more documentation. 141 | 142 | ## Known issues 143 | 144 | If you see unexpected highlighting, use `:call foldout#syntax()` to see the 145 | stack of syntax groups at the cursor. If foldout is enabled, you should expect 146 | to see `foldoutFile`, followed by one or more foldout-related groups, followed 147 | by zero or more non-foldout-related groups, for example: 148 | 149 | ``` 150 | foldoutFile, foldoutChildren, foldoutBody1, foldoutContent, javaScriptIdentifier -> Identifier 151 | ``` 152 | 153 | If you do not see this general pattern, you are experiencing a foldout-related 154 | issue. The two known issues are: 155 | 156 | ### Files beginning with keywords 157 | 158 | If a file begins with a keyword, then no headings are detected. For example, 159 | consider the following JavaScript file: 160 | 161 | ``` 162 | var x = 2 163 | // # print 164 | console.log(x) 165 | ``` 166 | 167 | The `print` heading is not detected. The issue is that foldout relies on 168 | matching the entire file as a region, but `var` is declared as a keyword in 169 | vim's JavaScript syntax file, and keywords have higher precedence than regions. 170 | An easy fix is to add an empty line to the top of the file. 171 | 172 | For a more permanent fix, modify the syntax file to replace any `keyword` that 173 | may occur at the beginning of the file with a `match`. For example, for the 174 | JavaScript keyword `var`, download the default JavaScript syntax file 175 | ([link](https://github.com/vim/vim/blob/master/runtime/syntax/javascript.vim)) 176 | to `~/.vim/syntax/javascript.vim` (if using vim) or 177 | `~/.config/nvim/syntax/javascript.vim` (if using neovim). vim will now use the 178 | downloaded syntax file instead of the default syntax file. Find the line 179 | declaring `var` as a keyword: 180 | 181 | ``` 182 | syn keyword javaScriptIdentifier arguments this var let 183 | ``` 184 | 185 | Then remove `var` and add a line recognizing `var` using `match`: 186 | 187 | ``` 188 | syn keyword javaScriptIdentifier arguments this let 189 | syn match javaScriptIdentifier "\" 190 | ``` 191 | 192 | The heading is now detected, and the JavaScript highlighting is unchanged. 193 | 194 | ### Syntax files using `contains=ALL` or `contains=CONTAINED` 195 | 196 | Some syntax files use `contains=ALL` to indicate that within a match or region, 197 | all match groups are in scope, which may cause unexpected highlighting when used 198 | with foldout. The issue is that foldout relies on carefully controlling where 199 | its own syntax groups may match, which is impossible in the presence of the 200 | `contains=ALL` construct. (Similar remarks apply to `contains=CONTAINED`.) 201 | 202 | The better syntax files avoid these constructs in favor of explicit lists of 203 | contained clusters and syntax groups. If you encounter this issue, consider 204 | using a vim plugin that provides an alternative syntax file for the affected 205 | filetype, or modify the syntax file yourself to replace the `ALL` or `CONTAINED` 206 | keywords wherever they appear. 207 | 208 | ## Changelog 209 | 210 | ### [2.0] - Jul. 2020 211 | 212 | - Removed navigation mode. (If you relied on this, tell me by filing an issue.) 213 | - `g:foldout_files` now defaults to `*.*`, matching files with extensions. 214 | - `b:foldout_heading_comment` now defaults to `0` in Markdown files. 215 | - `b:foldout_heading_string` now defaults to `%s` in Markdown files. 216 | 217 | ### [3.0] - Nov. 2020 218 | 219 | - We no longer bundle FastFold; users are recommended to install it separately. 220 | - Added `b:foldout_heading_ignore` option. 221 | 222 | -------------------------------------------------------------------------------- /autoload/foldout.vim: -------------------------------------------------------------------------------- 1 | " ## Enable 2 | 3 | " Determine whether foldout is enabled in the current buffer. 4 | function foldout#enabled() 5 | return exists('b:foldout_enabled') && b:foldout_enabled 6 | endfunction 7 | 8 | " Enable foldout in the current buffer. If called while foldout is already 9 | " enabled, apply current values of the buffer option variables. 10 | function foldout#enable() 11 | " Set variable indicating that foldout is enabled. 12 | let b:foldout_enabled = 1 13 | 14 | " Set buffer-local variables to global defaults if not already defined. 15 | 16 | " Indicate whether to highlight heading delimiters as comments. 17 | if !exists('b:foldout_heading_comment') 18 | let b:foldout_heading_comment 19 | \ = &filetype == 'markdown' ? 0 : 1 20 | endif 21 | 22 | if !exists('b:foldout_heading_ignore') 23 | let b:foldout_heading_ignore = '\@!' 24 | endif 25 | 26 | " Use '%s' as default if comment string is unchanged or empty. 27 | if !exists('b:foldout_heading_string') 28 | let b:foldout_heading_string 29 | \ = &filetype == 'markdown' ? '%s' 30 | \ : &l:commentstring ==# '/*%s*/' ? '%s' 31 | \ : &l:commentstring ==# '' ? '%s' 32 | \ : &l:commentstring 33 | endif 34 | 35 | if !exists('b:foldout_heading_symbol') 36 | let b:foldout_heading_symbol = g:foldout_heading_symbol 37 | endif 38 | 39 | if !exists('b:foldout_append_text') 40 | let b:foldout_append_text = g:foldout_append_text 41 | endif 42 | 43 | if !exists('b:foldout_append_pattern') 44 | let b:foldout_append_pattern = g:foldout_append_pattern 45 | endif 46 | 47 | if !exists('b:foldout_max_level') 48 | let b:foldout_max_level = g:foldout_max_level 49 | endif 50 | 51 | if !exists('b:foldout_min_fold') 52 | let b:foldout_min_fold = g:foldout_min_fold 53 | endif 54 | 55 | " Save old values of `fillchars`, `foldmethod`, and `foldtext`. 56 | let s:fillchars_old = &l:fillchars 57 | let s:foldlevel_old = &l:foldlevel 58 | let s:foldmethod_old = &l:foldmethod 59 | let s:foldtext_old = &l:foldtext 60 | 61 | " Set values of fold-related options if necessary. 62 | if &l:fillchars !=# s:fillchars 63 | let &l:fillchars = s:fillchars 64 | endif 65 | 66 | if &l:foldlevel != s:foldlevel 67 | let &l:foldlevel = s:foldlevel 68 | endif 69 | 70 | if &l:foldmethod !=# s:foldmethod 71 | let &l:foldmethod = s:foldmethod 72 | endif 73 | 74 | if &l:foldtext !=# s:foldtext 75 | let &l:foldtext = s:foldtext 76 | endif 77 | 78 | augroup foldout 79 | autocmd! 80 | 81 | " Needed for multiple-line matches. 82 | autocmd Syntax * syntax sync fromstart 83 | 84 | " Compute list of sections up to maximum level. 85 | let l:heading_list = 'foldoutHeadingLine1' 86 | for l:i in range(2, b:foldout_max_level) 87 | let l:heading_list .= ',foldoutHeadingLine' . l:i 88 | endfor 89 | 90 | " Match the whole file, which contains foldoutContent & foldoutChildren. 91 | execute 'autocmd Syntax * syntax region foldoutFile start="\%^" end="\%$" ' 92 | \ . 'contains=foldoutContent,foldoutChildren' 93 | 94 | " Match the region from the beginning of a section, either after a heading 95 | " or at the beginning of the file, to the first subheading. 96 | execute 'autocmd Syntax * syntax region foldoutContent ' 97 | \ . 'start="\_." end=' . s:quote(s:zero_width(s:pattern_any())) . ' ' 98 | \ . 'contains=TOP contained keepend nextgroup=foldoutChildren' 99 | 100 | " Match the region from the first child heading through the end of the last 101 | " child section, including from the first top-level heading to end of file. 102 | execute 'autocmd Syntax * syntax region foldoutChildren ' 103 | \ . 'start=' . s:quote(s:pattern_any()) . ' end="\%$" ' 104 | \ . 'contains=' . l:heading_list . ' contained' 105 | 106 | " Match heading, which may be just the initial part of the heading line. 107 | execute 'autocmd Syntax * syntax region foldoutHeading ' 108 | \ . 'matchgroup=foldoutHeadingDelimiter ' 109 | \ . 'start=' . s:quote(s:pattern_start()) . ' ' 110 | \ . 'end=' . s:quote(s:pattern_end()) . ' ' 111 | \ . 'contained' 112 | 113 | " Highlight foldout headings as titles. 114 | execute 'autocmd Syntax * highlight! default link foldoutHeading Title' 115 | 116 | " Highlight delimiters of headings as comments, if option is set. 117 | if b:foldout_heading_comment 118 | execute 'autocmd Syntax * highlight! default link ' 119 | \ . 'foldoutHeadingDelimiter Comment' 120 | endif 121 | 122 | " Execute syntax commands to recognize various heading levels. 123 | for l:i in range(1, b:foldout_max_level) 124 | execute 'autocmd Syntax * syntax match foldoutHeadingLine' . l:i . ' ' 125 | \ . s:quote(s:pattern_exact('.*', l:i)) . ' ' 126 | \ . 'nextgroup=foldoutBody' . l:i . ' ' 127 | \ . 'contains=foldoutHeading ' 128 | \ . 'contained keepend skipnl' 129 | execute 'autocmd Syntax * syntax match foldoutHeadingLine' . l:i . ' ' 130 | \ . s:quote(s:pattern_exact(b:foldout_heading_ignore, l:i)) . ' ' 131 | \ . 'nextgroup=foldoutBodyPlain' . l:i . ' ' 132 | \ . 'contains=foldoutHeading ' 133 | \ . 'contained keepend skipnl' 134 | execute 'autocmd Syntax * syntax region foldoutBody' . l:i . ' ' 135 | \ . 'start="\_." end=' . s:quote(s:zero_width(s:pattern_max(l:i))) . ' ' 136 | \ . 'contains=foldoutContent' 137 | \ . (l:i < b:foldout_max_level ? ',foldoutChildren ' : ' ') 138 | \ . (l:i >= b:foldout_min_fold ? 'fold ' : '') 139 | \ . 'contained keepend' 140 | execute 'autocmd Syntax * syntax region foldoutBodyPlain' . l:i . ' ' 141 | \ . 'start="\_." end=' . s:quote(s:zero_width(s:pattern_max(l:i))) . ' ' 142 | \ . 'contains=NONE ' 143 | \ . (l:i >= b:foldout_min_fold ? 'fold ' : '') 144 | \ . 'contained keepend' 145 | endfor 146 | augroup end 147 | 148 | " Make sure syntax changes take effect. 149 | do Syntax 150 | endfunction 151 | 152 | " Disable foldout in the current buffer. 153 | function foldout#disable() 154 | " Set variable indicating that foldout is disabled. 155 | let b:foldout_enabled = 0 156 | 157 | " Unmap the binding entering navigation mode. (`unmap` doesn't seem to work.) 158 | nmap FoldoutNavigation 159 | 160 | " Restore old value of `fillchars` if unchanged. 161 | if exists(s:fillchars_old) 162 | \ && &l:fillchars !=# s:fillchars_old 163 | \ && &l:fillchars ==# s:fillchars 164 | 165 | let &l:fillchars = s:fillchars_old 166 | endif 167 | 168 | " Restore old value of `foldlevel` if unchanged. 169 | if exists(s:foldlevel_old) 170 | \ && &l:foldlevel != s:foldlevel_old 171 | \ && &l:foldlevel == s:foldlevel 172 | 173 | let &l:foldlevel = s:foldlevel_old 174 | endif 175 | 176 | " Restore old value of `foldmethod` if unchanged. 177 | if exists(s:foldmethod_old) 178 | \ && &l:foldmethod !=# s:foldmethod_old 179 | \ && &l:foldmethod ==# s:foldmethod 180 | 181 | let &l:foldmethod = s:foldmethod_old 182 | endif 183 | 184 | " Restore old value of `foldtext` if unchanged. 185 | if exists(s:foldtext_old) 186 | \ && &l:foldtext !=# s:foldtext_old 187 | \ && &l:foldtext ==# s:foldtext 188 | 189 | let &l:foldtext = s:foldtext_old 190 | endif 191 | 192 | " Remove autocommands related to syntax highlighting. 193 | augroup foldout 194 | autocmd! 195 | augroup END 196 | 197 | " Recompute syntax highlighting. 198 | do Syntax 199 | endfunction 200 | 201 | " Enable or disable foldout. 202 | function foldout#toggle() 203 | if b:foldout_enabled 204 | call foldout#disable() 205 | else 206 | call foldout#enable() 207 | endif 208 | endfunction 209 | 210 | " ## Headings 211 | 212 | " Determine the current heading level at the given line, or at the cursor if no 213 | " argument is given. Return 0 if not at a heading. 214 | function foldout#level(...) 215 | let l:match = matchend(getline(get(a:, 1, '.')), s:pattern_any(1)) 216 | return l:match >= 0 ? l:match - s:prefix_length() : 0 217 | endfunction 218 | 219 | " If at a heading, demote the heading. Do not change the child headings. 220 | function foldout#demote() 221 | let l:level = foldout#level() 222 | 223 | " Handle cases where heading cannot be demoted. 224 | if l:level == 0 225 | echo 'Not at a heading.' 226 | return 227 | elseif l:level == b:foldout_max_level 228 | echo 'Heading already at maximum level.' 229 | return 230 | end 231 | 232 | " Store current column before modifying heading. 233 | let l:col = col('.') 234 | 235 | " Add an additional heading symbol. 236 | let l:line = getline('.') 237 | let l:index = matchend(l:line, s:pattern_start()) 238 | call setline(line('.'), l:line[0 : l:index - 1] . b:foldout_heading_symbol 239 | \ . l:line[l:index :]) 240 | 241 | " Keep cursor in place if at left margin, otherwise move one space right. 242 | call cursor(line('.'), l:col == 1 ? 1 : l:col + 1) 243 | endfunction 244 | 245 | " If at a heading, promote the heading. Do not change the child headings. 246 | function foldout#promote() 247 | let l:level = foldout#level() 248 | 249 | " Handle cases where heading cannot be promoted. 250 | if l:level == 0 251 | echo 'Not at a heading.' 252 | return 253 | elseif l:level == 1 254 | echo 'Heading already at top level.' 255 | return 256 | end 257 | 258 | " Store current column before modifying heading. 259 | let l:col = col('.') 260 | 261 | " Delete one of the heading symbols. 262 | let l:line = getline('.') 263 | let l:index = matchend(l:line, s:pattern_start()) 264 | call setline(line('.'), l:line[0 : l:index - 2] . l:line[l:index :]) 265 | call cursor(line('.'), l:col - 1) 266 | endfunction 267 | 268 | " ## Navigation 269 | 270 | " Go to previous sibling heading, if at a heading and if there is one. 271 | function foldout#up() 272 | let l:level = foldout#level() 273 | if l:level 274 | if s:up_helper(l:level) 275 | echo 'No heading above.' 276 | endif 277 | else 278 | echo 'Not at a heading.' 279 | endif 280 | endfunction 281 | 282 | " Go to previous sibling heading, if at a heading and if there is one. 283 | " Assume cursor is at heading. Return 0 if successful, 1 if no heading found. 284 | function s:up_helper(level) 285 | let l:line = line('.') 286 | let l:col = col('.') 287 | call cursor(l:line, 1) 288 | let l:match = search(s:pattern_max(a:level), 'bnW') 289 | if l:match && foldout#level(l:match) == a:level 290 | call cursor(l:match, 1) 291 | else 292 | call cursor(l:line, l:col) 293 | return 1 294 | endif 295 | endfunction 296 | 297 | " Go to previous visible heading, if there is one. 298 | function foldout#up_graphical() 299 | let l:line = line('.') 300 | let l:col = col('.') 301 | 302 | " Move down to next line. 303 | call cursor(l:line - 1, 1) 304 | 305 | while 1 306 | let l:next = search(s:pattern_any(), 'bcW') 307 | 308 | " If no heading below, return. 309 | if l:next == 0 310 | call cursor(l:line, l:col) 311 | echo 'No heading above.' 312 | return 313 | endif 314 | 315 | let l:start = foldclosed(l:next) 316 | 317 | " If current heading is visible, return. 318 | if l:start < 0 319 | return 320 | else 321 | call cursor(l:start - 1, 1) 322 | endif 323 | endwhile 324 | endfunction 325 | 326 | " Go to next sibling heading, if at a heading and if there is one. If not at a 327 | " heading, go to the next heading if it is a first child. 328 | function foldout#down() 329 | let l:level = foldout#level() 330 | if l:level 331 | if s:down_helper(l:level) 332 | echo 'No heading below.' 333 | endif 334 | 335 | else 336 | let l:parent = search(s:pattern_any(), 'bnW') 337 | let l:heading = search(s:pattern_any(), 'nW') 338 | let l:parent_level = l:parent == 0 ? 0 : foldout#level(l:parent) 339 | let l:heading_level = foldout#level(l:heading) 340 | 341 | if l:heading == 0 342 | echo 'No heading below.' 343 | return 344 | endif 345 | 346 | if l:heading_level > l:parent_level 347 | call cursor(l:heading, 1) 348 | else 349 | echo 'No heading below.' 350 | endif 351 | 352 | endif 353 | endfunction 354 | 355 | " Go to the next sibling heading, if at a heading and if there is one. Assume 356 | " cursor is at heading. Return 0 if successful, 1 if no heading found. 357 | function s:down_helper(level) 358 | let l:match = search(s:pattern_max(a:level), 'nW') 359 | if l:match && foldout#level(l:match) == a:level 360 | call cursor(l:match, 1) 361 | else 362 | return 1 363 | endif 364 | endfunction 365 | 366 | " Go to next visible heading, if there is one. 367 | function foldout#down_graphical() 368 | let l:line = line('.') 369 | let l:col = col('.') 370 | 371 | " Move down to next line. 372 | call cursor(l:line + 1, 1) 373 | 374 | while 1 375 | let l:next = search(s:pattern_any(), 'cW') 376 | 377 | " If no heading below, return. 378 | if l:next == 0 379 | call cursor(l:line, l:col) 380 | echo 'No heading below.' 381 | return 382 | endif 383 | 384 | let l:end = foldclosedend(l:next) 385 | 386 | " If current heading is visible, return. 387 | if l:end < 0 388 | return 389 | else 390 | call cursor(l:end + 1, 1) 391 | endif 392 | endwhile 393 | endfunction 394 | 395 | " Go to first sibling if at a heading, else to beginning of section. 396 | function foldout#top() 397 | let l:level = foldout#level() 398 | 399 | " If at a heading, go to first sibling. 400 | if l:level 401 | while s:up_helper(level) == 0 402 | endwhile 403 | 404 | " Otherwise, go to beginning of section. 405 | else 406 | let l:line = line('.') 407 | let l:heading = search(s:pattern_any(), 'bnW') 408 | 409 | " Go to line after previous heading, or first line if no heading below. 410 | call cursor(l:heading ? l:heading + 1 : 1, 1) 411 | 412 | let l:match = search('^.', 'cnW') 413 | if l:match 414 | call cursor(l:match, 1) 415 | else 416 | call cursor(l:line, 1) 417 | endif 418 | 419 | endif 420 | endfunction 421 | 422 | " Go to last sibling if at a heading, else to end of section. 423 | function foldout#bottom() 424 | let l:level = foldout#level() 425 | 426 | " If at a heading, go to last sibling. 427 | if l:level 428 | while s:down_helper(level) == 0 429 | endwhile 430 | 431 | " Otherwise, go to end of section. 432 | else 433 | let l:line = line('.') 434 | let l:heading = search(s:pattern_any(), 'nW') 435 | 436 | " Go to line before next heading, or last line if no heading below. 437 | if l:heading 438 | call cursor(l:heading - 1, 1) 439 | else 440 | call cursor(search('\%$', 'nW'), 1) 441 | endif 442 | 443 | let l:match = search('^.', 'bcnW') 444 | if l:match 445 | call cursor(l:match, 1) 446 | else 447 | call cursor(l:line, 1) 448 | endif 449 | 450 | endif 451 | endfunction 452 | 453 | " Go to parent heading, if there is one. 454 | function foldout#parent() 455 | let l:level = foldout#level() 456 | 457 | " If cursor is not at a heading, go up to the nearest heading. 458 | if l:level == 0 459 | if search(s:pattern_any(), 'bW') == 0 460 | echo 'No parent heading.' 461 | endif 462 | 463 | " If cursor is at a heading not at the top level, go to its parent. 464 | elseif l:level > 1 465 | let l:line = line('.') 466 | let l:col = col('.') 467 | call cursor(l:line, 1) 468 | let l:match = search(s:pattern_max(l:level - 1), 'bnW') 469 | if l:match 470 | call cursor(l:match, 1) 471 | else 472 | call cursor(l:line, l:col) 473 | echo 'No parent heading.' 474 | endif 475 | 476 | " If cursor is at a top level heading, do nothing. 477 | else 478 | echo 'No parent heading.' 479 | 480 | endif 481 | endfunction 482 | 483 | " Go to first nonempty line inside a heading, if there is one. 484 | function foldout#child() 485 | let l:level = foldout#level() 486 | if l:level == 0 487 | echo 'Not at a heading.' 488 | return 489 | endif 490 | 491 | let l:match = search('^.', 'nW') 492 | if l:match == 0 493 | echo 'No content under heading.' 494 | return 495 | endif 496 | 497 | let l:heading = foldout#level(l:match) 498 | if l:heading == 0 || l:heading > l:level 499 | call cursor(l:match, 1) 500 | normal! zv 501 | else 502 | echo 'No content under heading.' 503 | endif 504 | endfunction 505 | 506 | " Search for the given heading at the given level; go to heading if found. 507 | " The optional argument indicates whether to enter the section. 508 | " Return 1 if heading is not found, 0 otherwise. 509 | function foldout#goto(name, level, ...) 510 | let [l:prefix, l:suffix] = s:heading_split() 511 | let l:pattern = '^' 512 | \ . s:escape(l:prefix) 513 | \ . repeat(b:foldout_heading_symbol, a:level) 514 | \ . '\s\+' . a:name . '\s*' 515 | \ . s:escape(l:suffix) 516 | \ . '.*$' 517 | 518 | if search(l:pattern, 'c') == 0 519 | echo a:name . ' section not found.' 520 | return 1 521 | elseif a:0 >= 1 && a:1 522 | call foldout#child() 523 | endif 524 | endfunction 525 | 526 | " ## Folding 527 | 528 | " Toggle current fold, moving down one line if at a header. 529 | function foldout#toggle_fold() 530 | call s:update() 531 | 532 | try 533 | execute 'silent normal! ' . (foldout#level() > 0 ? 'jzak' : 'za') 534 | catch 535 | echo 'No fold found.' 536 | endtry 537 | endfunction 538 | 539 | " Show all folds in buffer. 540 | function foldout#show() 541 | %foldopen! 542 | endfunction 543 | 544 | " Focus the cursor by closing all other folds. 545 | function foldout#focus() 546 | do Syntax 547 | silent! %foldclose! 548 | normal! zv 549 | 550 | if foldout#level() > 0 551 | call s:update() 552 | execute "silent normal! jzak" 553 | endif 554 | endfunction 555 | 556 | " Center the cursor vertically, without moving the cursor. 557 | function foldout#center() 558 | normal! zz 559 | endfunction 560 | 561 | " ## Insertion 562 | 563 | " Append a new line to the end of the current section, enter insert mode. 564 | function foldout#append() 565 | " Find `end`, the last line of the current section, and move cursor there. 566 | let l:heading = search(s:pattern_any(), 'nW') 567 | let l:end = l:heading ? l:heading - 1 : search('\%$', 'nW') 568 | call cursor(l:end, 1) 569 | 570 | " Find `text`, the last nonempty line up to and including `end`. 571 | let l:text = search('^.', 'bcW') 572 | 573 | " Determine whether we will need to add an extra line afterwards. 574 | let l:after = l:heading && l:text == l:end 575 | 576 | " Add an empty line if this line has text other than a list item. 577 | if l:text && getline('.') !~ b:foldout_append_pattern 578 | call append(l:text, '') 579 | let l:text += 1 580 | endif 581 | 582 | " Append new list item. 583 | call append(l:text, b:foldout_append_text) 584 | 585 | " Append extra line afterwards if necessary. 586 | if l:after 587 | call append(l:text + 1, '') 588 | endif 589 | 590 | " Enter insert mode, and put the cursor at the end of the new item. 591 | startinsert 592 | call cursor(l:text + 1, len(b:foldout_append_text) + 1) 593 | 594 | " Make sure cursor is visible; return cursor to end of the line. 595 | normal! zv 596 | call cursor(l:text + 1, len(b:foldout_append_text) + 1) 597 | endfunction 598 | 599 | " Open a new heading line, meant as the foldout analogue of `o`. 600 | " The optional argument indicates whether to always insert at the cursor. 601 | " The default behavior is to insert at the end of a section. 602 | function foldout#open(...) 603 | let l:cursor = a:0 >= 1 && a:1 604 | let l:line = line('.') 605 | let l:level = foldout#level() 606 | 607 | " If not at a heading, move cursor to previous heading. 608 | " `l:top` represents whether the cursor is above the first heading. 609 | let l:top = l:level == 0 && search(s:pattern_any(), 'bW') == 0 610 | 611 | " If above all headings, return without doing anything. 612 | if !l:cursor && l:top 613 | echo 'Not at a heading.' 614 | return 615 | endif 616 | 617 | " Compute level for the new heading. 618 | let l:new_level = l:level ? l:level : foldout#level() + 1 619 | 620 | " Make sure we don't create a heading with level over the allowed maximum. 621 | if l:new_level > b:foldout_max_level 622 | echo 'At maximum outline level.' 623 | return 624 | endif 625 | 626 | " Compute line below which to add the new heading, as `l:line`. 627 | if !l:cursor 628 | " Compute pattern marking end of the relevant section. 629 | let l:pattern = l:level ? s:pattern_max(l:level) : s:pattern_any() 630 | 631 | " Find the heading marking the end of the relevant section. 632 | let l:heading = search(l:pattern, 'nW') 633 | 634 | " Compute `line`, the last line of the relevant section. 635 | let l:line = l:heading ? l:heading - 1 : line('$') 636 | endif 637 | 638 | " Create new heading at bottom of section; add blank lines if appropriate. 639 | let l:before = getline(l:line) != '' 640 | let l:after = getline(l:line + 1) != '' 641 | call append(l:line, (l:before ? [''] : []) + [s:heading_text(l:new_level)] 642 | \ + (l:after ? [''] : [])) 643 | 644 | " Enter insert mode and move cursor to end of line. 645 | startinsert 646 | call cursor(l:line + (l:before ? 2 : 1), s:heading_pos(l:new_level)) 647 | endfunction 648 | 649 | " Demote heading if at a heading, otherwise simulate tab. 650 | " Designed to be bound to `` in insert mode. 651 | function foldout#tab() 652 | if foldout#enabled() 653 | \ && foldout#level() > 0 654 | \ && col('.') <= s:heading_pos(foldout#level()) 655 | call foldout#demote() 656 | else 657 | call feedkeys("\", 'n') 658 | endif 659 | endfunction 660 | 661 | " Promote heading if at a heading, otherwise simulate shift-tab. 662 | " Designed to be bound to `` in insert mode. 663 | function foldout#shift_tab() 664 | if foldout#enabled() 665 | \ && foldout#level() > 0 666 | \ && col('.') <= s:heading_pos(foldout#level()) 667 | call foldout#promote() 668 | else 669 | call feedkeys("\", 'n') 670 | endif 671 | endfunction 672 | 673 | " ## Query 674 | 675 | " View the stack of syntax groups at the cursor. Modified from 676 | " https://stackoverflow.com/questions/9464844/how-to-get-group-name-of-highlighting-under-cursor-in-vim. 677 | function foldout#syntax() 678 | if exists('*synstack') 679 | let l:group = synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name') 680 | let l:stack = map(synstack(line('.'), col('.')), 681 | \ {_, val -> synIDattr(val, "name")}) 682 | echo (l:group ==# '' ? '(none)' : join(l:stack, ', ') . ' -> ' . l:group) 683 | else 684 | echo '(none)' 685 | endif 686 | endfunction 687 | 688 | " ## Defaults 689 | 690 | " Store default foldout values of fillchars & foldtext. 691 | let s:fillchars = 'fold: ' 692 | let s:foldlevel = 99 693 | let s:foldmethod = 'syntax' 694 | let s:foldtext = 'foldout#fold_text()' 695 | 696 | " Trivial string-valued function for the `foldtext` option. Note that it is not 697 | " possible to make this a script-local function. 698 | function foldout#fold_text() 699 | return '' 700 | endfunction 701 | 702 | " ## Utilities 703 | 704 | " ### Wrapping strings 705 | 706 | " Create a pattern that matches exactly the given string, by escaping chars. 707 | function s:escape(str) 708 | " Backslash must come first to not interfere with other substitutions. 709 | let l:chars = ['\', '*', '^', '$', '.', '~', '[', ']'] 710 | 711 | " Convert special characters into escape sequences, one by one. 712 | let l:str = a:str 713 | for l:char in l:chars 714 | let l:str = substitute(l:str, '\' . l:char, '\\' . l:char, 'g') 715 | endfor 716 | 717 | return l:str 718 | endfunction 719 | 720 | " Wrap a pattern within an appropriate character, for syntax commands. 721 | function s:quote(pattern) 722 | " Characters usable as pattern delimiters, in rough order of preference. 723 | let l:chars = 724 | \ [ '"', "'", '`', '/', '+', '~', '!', '@', '#', '$', '%', '^', '&', '*' 725 | \ , '(', ')', '-', '_', '=', '[', '{', ']', '}', '|', ';', ':', ',', '<' 726 | \ , '.', '>', '?' 727 | \ ] 728 | 729 | " Find the first character that doesn't appear in the given string. 730 | for l:char in l:chars 731 | if match(a:pattern, '\V' . l:char) < 0 732 | return l:char . a:pattern . l:char 733 | endif 734 | endfor 735 | endfunction 736 | 737 | " Convert a pattern into a zero-width pattern. 738 | function s:zero_width(pattern) 739 | return '\ze\(' . a:pattern . '\)' 740 | endfunction 741 | 742 | " ### Heading text 743 | 744 | " Compute the heading prefix & suffix from the heading string, return as list. 745 | " Include a space after prefix if nonempty, and before suffix if nonempty. 746 | function s:heading_split() 747 | " Compute index after `%s` in the heading string. 748 | let l:split_index = matchend(b:foldout_heading_string, '%s') 749 | if l:split_index < 0 750 | throw "b:foldout_heading_string must contain '%s' substring." 751 | endif 752 | 753 | " Split the heading string into its prefix & suffix. 754 | let l:prefix = l:split_index > 2 755 | \ ? b:foldout_heading_string[: l:split_index - 3] : '' 756 | let l:suffix = b:foldout_heading_string[l:split_index :] 757 | 758 | " Add space after prefix if nonempty, and before suffix if nonempty. 759 | return 760 | \ [ l:prefix == '' || l:prefix[-1:] ==# ' ' ? l:prefix : l:prefix . ' ' 761 | \ , l:suffix == '' || l:suffix[0] ==# ' ' ? l:suffix : ' '. l:suffix 762 | \ ] 763 | endfunction 764 | 765 | " Compute the appropriate cursor position for editing new heading. 766 | function s:heading_pos(level) 767 | return s:prefix_length() + a:level + 2 768 | endfunction 769 | 770 | " Compute the text for a heading of the given level. 771 | function s:heading_text(level) 772 | let [l:prefix, l:suffix] = s:heading_split() 773 | return l:prefix . repeat(b:foldout_heading_symbol, a:level) . ' ' . l:suffix 774 | endfunction 775 | 776 | " Compute the length of the heading prefix. 777 | function s:prefix_length() 778 | return len(s:heading_split()[0]) 779 | endfunction 780 | 781 | " ### Heading patterns 782 | 783 | " Compute the heading prefix & suffix patterns, return as list. 784 | " The prefix pattern matches everything up to the heading symbols. 785 | " The suffix pattern matches everything after the heading symbols. 786 | " Include a space after prefix if nonempty, and before suffix if nonempty. 787 | " Takes a pattern to match the heading against. 788 | function s:pattern_split(heading) 789 | let [l:prefix, l:suffix] = s:heading_split() 790 | 791 | let l:prefix_pattern 792 | \ = '^\s*' 793 | \ . s:escape(l:prefix) 794 | let l:suffix_pattern 795 | \ = ' ' 796 | \ . a:heading 797 | \ . s:escape(l:suffix) 798 | \ . '.*$' 799 | 800 | return [l:prefix_pattern, l:suffix_pattern] 801 | endfunction 802 | 803 | " Compute a pattern representing a heading of exactly the given level. 804 | " The pattern expects a space character after the prefix if prefix nonempty. 805 | " The pattern expects a space character before the suffix if suffix nonempty. 806 | " Takes a pattern to match the heading against. 807 | " With optional flag, include a `\ze` after the caret. 808 | function s:pattern_exact(heading, level) 809 | let [l:prefix, l:suffix] = s:pattern_split(a:heading) 810 | return l:prefix . repeat(b:foldout_heading_symbol, a:level) . l:suffix 811 | endfunction 812 | 813 | " Compute a pattern representing a heading of at most the given level. 814 | " With optional flag, include a `\ze` after the heading symbols. 815 | function s:pattern_max(level, ...) 816 | let [l:prefix, l:suffix] = s:pattern_split('.*') 817 | let l:symbols = a:level > 1 818 | \ ? '\%[' . repeat(b:foldout_heading_symbol, a:level - 1) . ']' 819 | \ : '' 820 | return l:prefix 821 | \ . b:foldout_heading_symbol 822 | \ . l:symbols 823 | \ . (get(a:, 1, 0) ? '\ze' : '') 824 | \ . l:suffix 825 | endfunction 826 | 827 | " A pattern representing a heading of any level. 828 | " With optional flag, include a `\ze` after the heading symbols. 829 | function s:pattern_any(...) 830 | return s:pattern_max(b:foldout_max_level, get(a:, 1, 0)) 831 | endfunction 832 | 833 | " A pattern representing the start of a heading of any level, up to the title. 834 | function s:pattern_start() 835 | return s:pattern_split('.*')[0] 836 | \ . b:foldout_heading_symbol 837 | \ . '\%[' . repeat(b:foldout_heading_symbol, b:foldout_max_level - 1) . ']' 838 | \ . '\ze ' 839 | endfunction 840 | 841 | " A pattern representing the end of a heading of any level, after the title. 842 | function s:pattern_end() 843 | let [l:prefix, l:suffix] = s:heading_split() 844 | let l:pattern = l:suffix == '' ? '$' : ' \zs' . s:escape(l:suffix[1:]) 845 | return l:pattern 846 | endfunction 847 | 848 | " ### Updating folds 849 | 850 | " Update folds in the current buffer. 851 | function s:update() 852 | if exists('g:loaded_fastfold') 853 | FastFoldUpdate 854 | else 855 | execute 'syntax sync fromstart' 856 | endif 857 | endfunction 858 | 859 | -------------------------------------------------------------------------------- /doc/foldout.txt: -------------------------------------------------------------------------------- 1 | *foldout.txt* Outline-based folding for any filetype. 2 | 3 | Author: Matt Superdock 4 | License: MIT 5 | 6 | This plugin is only available if 'compatible' is not set. 7 | 8 | INTRODUCTION *foldout* 9 | 10 | |foldout| is an outline-based folding plugin for vim & neovim. Its unique 11 | feature is that folds are determined by Markdown-style headings within comments, 12 | and these headings are automatically highlighted. |foldout| also provides a 13 | suite of functions for manipulating and navigating between headings. 14 | 15 | |foldout| uses vim's 'commentstring' option to compute default heading patterns. 16 | As a result, if you're using a filetype-specific plugin that sets 17 | 'commentstring', no further configuration is necessary. Just type the comment 18 | prefix, then a string of heading symbols (by default, "#"), and |foldout| will 19 | recognize a heading. 20 | 21 | INSTALLATION *foldout-quickstart* 22 | 23 | Use your preferred installation method; for example, with vim-plug, use: 24 | 25 | `Plug 'msuperdock/vim-foldout'` 26 | 27 | If your .vimrc uses |:mkview| or |:loadview| to save and restore view data, 28 | remove these commands and set |g:foldout_save|. (The |:mkview| and |:loadview| 29 | commands need to be called in certain sequence with |foldout| commands, and 30 | |foldout| handles this for you.) For example: 31 | 32 | `let g:foldout_save = 1` 33 | 34 | By default, |foldout| is automatically enabled in all buffers whose filenames 35 | contain a dot (see |g:foldout_files| to customize this), and can be manually 36 | enabled & disabled using |foldout#enable()|, |foldout#disable()|, and 37 | |foldout#toggle()|. 38 | 39 | FUNCTIONS *foldout-functions* 40 | 41 | |foldout| provides the following functions. You can bind a key to a function in 42 | your .vimrc using, for example: 43 | 44 | `noremap :call foldout#toggle_fold()` 45 | 46 | This binds "" to the |foldout#toggle_fold()| function in normal, visual, 47 | select, and operator-pending modes. The "" flag tells vim to call the 48 | function without first displaying the keybinding. 49 | 50 | Enable: 51 | 52 | | function | description | 53 | | ------------------------ | ------------------------------------------------- | 54 | | |foldout#enable()| | Enable foldout in the current buffer. | 55 | | |foldout#disable()| | Disable foldout in the current buffer. | 56 | | |foldout#toggle()| | Toggle foldout in the current buffer. | 57 | 58 | Headings: 59 | 60 | | function | description | 61 | | ------------------------ | ------------------------------------------------- | 62 | | |foldout#level()| | Get level of heading, or 0 if not at a heading. | 63 | | |foldout#demote()| | Demote current heading. Don't change children. | 64 | | |foldout#promote()| | Promote current heading. Don't change children. | 65 | | |foldout#tab()| | Demote if at heading, else simulate tab. | 66 | | |foldout#shift_tab()| | Promote if at heading, else simulate shift-tab. | 67 | 68 | The |foldout#tab()| and |foldout#shift_tab()| functions are designed to be 69 | bound to tab and shift-tab in insert mode; for example: 70 | 71 | `inoremap :silent call foldout#tab()` 72 | `inoremap :silent call foldout#shift_tab()` 73 | 74 | Navigation: 75 | 76 | | function | description | 77 | | ------------------------ | ------------------------------------------------- | 78 | | |foldout#parent()| | Go to parent heading. | 79 | | |foldout#down()| | Go to next sibling heading. | 80 | | |foldout#up()| | Go to previous sibling heading. | 81 | | |foldout#down_graphical()| | Go to next visible heading of any level. | 82 | | |foldout#up_graphical()| | Go to previous visible heading of any level. | 83 | | |foldout#top()| | Go to first sibling heading. | 84 | | |foldout#bottom()| | Go to last sibling heading. | 85 | | |foldout#child()| | Go to first child heading. | 86 | | |foldout#goto()| | Go to heading with given name & level. | 87 | 88 | Folding: 89 | 90 | | |foldout#toggle_fold()| | Toggle fold at cursor. | 91 | | |foldout#show()| | Open all folds. | 92 | | |foldout#focus()| | Close all folds but those needed to see cursor. | 93 | | |foldout#center()| | Redraw so that cursor is vertically centered. | 94 | 95 | Insertion: 96 | 97 | | |foldout#append()| | Go to end of current section, enter insert mode. | 98 | | |foldout#open()| | Create heading below and enter insert mode. | 99 | 100 | Query: 101 | 102 | | |foldout#syntax()| | View the stack of syntax groups at the cursor. | 103 | 104 | OPTIONS *foldout-options* 105 | 106 | |foldout| provides the following option variables for configuration. The 107 | prefix column indicates whether the option is a global option, a buffer-local 108 | option, or both. Buffer-local options override global options where both are 109 | present. You can set an option in your .vimrc using, for example: 110 | 111 | `let g:foldout_heading_symbol = '*'` 112 | 113 | | prefix | variable | default | description | 114 | | ------ | ----------------------- | ------- | ------------------------------- | 115 | | g | |foldout_files| | "*.*" | Pattern determining whether to | 116 | | | | | enable |foldout|. | 117 | | g | |foldout_save| | 0 | Allow foldout to save & restore | 118 | | | | | view data. | 119 | | b, g | |foldout_heading_symbol| | "#" | Repeated symbol indicating | 120 | | | | | heading level. | 121 | | b, g | |foldout_max_level| | 6 | Maximum allowed heading level. | 122 | | | | | | 123 | | b, g | |foldout_min_fold| | 1 | Minimum level at which to | 124 | | | | | enable folding. | 125 | | b | |foldout_heading_comment| | 1 | Highlight heading delimiters as | 126 | | | | (0)* | comments. | 127 | | b | |foldout_heading_ignore| | "\@!" | Pattern determining whether to | 128 | | | | | ignore section. | 129 | | b | |foldout_heading_string| | ** | Heading pattern, in | 130 | | | | ("%s")* | 'commentstring' format. | 131 | 132 | * This is an alternative default, for Markdown files only. 133 | ** The default for |foldout_heading_string| is the value of 'commentstring'. 134 | 135 | Options governing the |foldout#append()| function: 136 | 137 | | b, g | |foldout_append_pattern| | "\@!" | Pattern determining whether to | 138 | | | | | insert empty line. | 139 | | b, g | |foldout_append_text| | "" | Prefix text to insert. | 140 | | | | | | 141 | 142 | Note that `'\@!'` is a pattern which never matches. 143 | 144 | KNOWN ISSUES *foldout-known-issues* 145 | 146 | If you see unexpected highlighting, use `:call foldout#syntax()` to see the 147 | stack of syntax groups at the cursor. If |foldout| is enabled, you should expect 148 | to see `foldoutFile`, followed by one or more |foldout|-related groups, followed 149 | by zero or more non-|foldout|-related groups, for example: 150 | 151 | `foldoutFile, foldoutChildren, foldoutBody1, foldoutContent, javaScriptIdentifier -> Identifier` 152 | 153 | If you do not see this general pattern, you are experiencing a |foldout|-related 154 | issue. The two known issues are: 155 | 156 | (1) Files beginning with keywords. 157 | 158 | If a file begins with a keyword, then no headings are detected. For example, 159 | consider the following JavaScript file: 160 | 161 | `var x = 2` 162 | `// # print` 163 | `console.log(x)` 164 | 165 | The `print` heading is not detected. The issue is that |foldout| relies on 166 | matching the entire file as a region, but `var` is declared as a keyword in 167 | vim's JavaScript syntax file, and keywords have higher precedence than regions. 168 | An easy fix is to add an empty line to the top of the file. 169 | 170 | For a more permanent fix, modify the syntax file to replace any `keyword` that 171 | may occur at the beginning of the file with a `match`. For example, for the 172 | JavaScript keyword `var`, download the default JavaScript syntax file 173 | (https://github.com/vim/vim/blob/master/runtime/syntax/javascript.vim) to 174 | `~/.vim/syntax/javascript.vim` (if using vim) or 175 | `~/.config/nvim/syntax/javascript.vim` (if using neovim). vim will now use the 176 | downloaded syntax file instead of the default syntax file. Find the line 177 | declaring `var` as a keyword: 178 | 179 | `syn keyword javaScriptIdentifier arguments this var let` 180 | 181 | Then remove `var` and add a line recognizing `var` using `match`: 182 | 183 | `syn keyword javaScriptIdentifier arguments this let` 184 | `syn match javaScriptIdentifier "\"` 185 | 186 | The heading is now detected, and the JavaScript highlighting is unchanged. 187 | 188 | (2) Syntax files using `contains=ALL` or `contains=CONTAINED`. 189 | 190 | Some syntax files use `contains=ALL` to indicate that within a match or region, 191 | all match groups are in scope, which may cause unexpected highlighting when used 192 | with |foldout|. The issue is that foldout relies on carefully controlling where 193 | its own syntax groups may match, which is impossible in the presence of the 194 | `contains=ALL` construct. (Similar remarks apply to `contains=CONTAINED`.) 195 | 196 | The better syntax files avoid these constructs in favor of explicit lists of 197 | contained clusters and syntax groups. If you encounter this issue, consider 198 | using a vim plugin that provides an alternative syntax file for the affected 199 | filetype, or modify the syntax file yourself to replace the `ALL` or `CONTAINED` 200 | keywords wherever they appear. 201 | 202 | CREDITS *foldout-credits* 203 | 204 | |foldout| relies critically on the FastFold plugin, which is slightly modified 205 | and packaged with |foldout|. 206 | 207 | DOCUMENTATION *foldout-documentation* 208 | 209 | Below is documentation for each option and function provided by |foldout|. 210 | 211 | Options: 212 | 213 | *g:foldout_files* 214 | g:foldout_files string (default "?*") 215 | Pattern matched against file names to determine whether to enable 216 | |foldout|. If the empty string, never automatically enable |foldout|. 217 | 218 | *g:foldout_save* 219 | g:foldout_save number (default 0) 220 | Indicates whether to let |foldout| handle saving & loading view data. 221 | 222 | If 1, |foldout| calls |:loadview| and |:mkview| at the appropriate 223 | times in buffers where |foldout| is enabled, to save view data (like 224 | folds & cursor position) according to the value of 'viewoptions'. 225 | 226 | If 0, |foldout| does not save or load view data. In this case, it is 227 | recommended not to use |:loadview| and |:mkview| at all, since calling 228 | these commands must be done in a particular order relative to 229 | |foldout| commands. 230 | 231 | *g:foldout_heading_symbol* 232 | *b:foldout_heading_symbol* 233 | g:foldout_heading_symbol string (default "#") 234 | b:foldout_heading_symbol local to buffer 235 | The one-character string to use as the repeated character in headings. 236 | 237 | *g:foldout_max_level* 238 | *b:foldout_max_level* 239 | g:foldout_max_level number (default 6) 240 | b:foldout_max_level local to buffer 241 | The upper limit on the number of heading levels. 242 | 243 | *g:foldout_min_fold* 244 | *b:foldout_min_fold* 245 | g:foldout_min_fold number (default 1) 246 | b:foldout_min_fold local to buffer 247 | The first level at which to enable folding. 248 | 249 | *b:foldout_heading_comment* 250 | b:foldout_heading_comment number (default 1) 251 | local to buffer 252 | Indicates whether to highlight heading delimiters as comments. 253 | 254 | *b:foldout_heading_ignore* 255 | b:foldout_heading_ignore string (default "\@!") 256 | local to buffer 257 | Pattern matched against headings. If it matches, no syntax highlighting 258 | is done in the subsequent section, and no subheadings are recognized. 259 | The default value is the null pattern, which never matches. 260 | 261 | *b:foldout_heading_string* 262 | b:foldout_heading_string string (defaults to value of 'commentstring') 263 | local to buffer 264 | A template for a heading, in the same format as 'commentstring'. The 265 | "%s" in the value is replaced with the heading symbols and title. 266 | 267 | *g:foldout_append_pattern* 268 | *b:foldout_append_pattern* 269 | g:foldout_append_pattern string (default "\@!") 270 | b:foldout_append_pattern local to buffer 271 | Pattern matched against last line of section in |foldout#append()|. If 272 | it matches, an empty line is inserted before appending text. The 273 | default value is the null pattern, to always insert an empty line. 274 | 275 | *g:foldout_append_text* 276 | *b:foldout_append_text* 277 | g:foldout_append_text string (default "") 278 | b:foldout_append_text local to buffer 279 | Prefix text to insert in |foldout#append()|. 280 | 281 | Functions: 282 | 283 | foldout#enable() *foldout#enable()* 284 | Enable |foldout| in the current buffer. If called while |foldout| is 285 | already enabled, apply current values of the buffer option variables. 286 | 287 | foldout#disable() *foldout#disable()* 288 | Disable |foldout| in the current buffer. 289 | 290 | foldout#toggle() *foldout#toggle()* 291 | Enable or disable |foldout|. 292 | 293 | foldout#level([{line}]) *foldout#level()* 294 | Determine the current heading level at {line}, or at the cursor if 295 | {line} is not given. Return 0 if not at a heading. 296 | 297 | foldout#demote() *foldout#demote()* 298 | If at a heading, demote the heading. Do not change the child headings. 299 | 300 | foldout#promote() *foldout#promote()* 301 | If at a heading, promote the heading. Do not change the child headings. 302 | 303 | foldout#tab() *foldout#tab()* 304 | Demote heading if at a heading, otherwise simulate tab. Designed to be 305 | bound to in insert mode. 306 | 307 | foldout#shift_tab() *foldout#shift_tab()* 308 | Promote heading if at a heading, otherwise simulate shift-tab. 309 | Designed to be bound to in insert mode. 310 | 311 | foldout#parent() *foldout#parent()* 312 | Go to parent heading, if there is one. 313 | 314 | foldout#down() *foldout#down()* 315 | Go to next sibling heading, if at a heading and if there is one. If 316 | not at a heading, go to the next heading if it is a first child. 317 | 318 | foldout#up() *foldout#up()* 319 | Go to previous sibling heading, if at a heading and if there is one. 320 | 321 | foldout#down_graphical() *foldout#down_graphical()* 322 | Go to next visible heading, if there is one. 323 | 324 | foldout#up_graphical() *foldout#up_graphical()* 325 | Go to previous visible heading, if there is one. 326 | 327 | foldout#top() *foldout#top()* 328 | Go to first sibling if at a heading, else to beginning of section. 329 | 330 | foldout#bottom() *foldout#bottom()* 331 | Go to last sibling if at a heading, else to end of section. 332 | 333 | foldout#child() *foldout#child()* 334 | Go to first nonempty line inside a heading, if there is one. 335 | 336 | foldout#goto({name}, {level} [, {enter}]) *foldout#goto()* 337 | Search for heading string {name} at level number {level}; go to heading 338 | if found. If {enter} is given, enter the section. Return 1 if heading is 339 | not found, 0 otherwise. 340 | 341 | foldout#toggle_fold() *foldout#toggle_fold()* 342 | Toggle current fold, moving down one line if at a header. 343 | 344 | foldout#show() *foldout#show()* 345 | Show all folds in buffer. 346 | 347 | foldout#focus() *foldout#focus()* 348 | Focus the cursor by closing all other folds. 349 | 350 | foldout#center() *foldout#center()* 351 | Center the cursor vertically, without moving the cursor. 352 | 353 | foldout#append() *foldout#append()* 354 | Append a new line to the end of the current section, enter insert mode. 355 | 356 | foldout#open([{cursor}]) *foldout#open()* 357 | Open a new heading line, meant as the foldout analogue of "o". If 358 | {cursor} is given, create heading at cursor, regardless of outline 359 | structure. Otherwise, create heading at the end of the section. 360 | 361 | foldout#syntax() *foldout#syntax()* 362 | View the stack of syntax groups at the cursor. 363 | 364 | -------------------------------------------------------------------------------- /plugin/foldout.vim: -------------------------------------------------------------------------------- 1 | " vim-foldout - Outline-based folding with syntax highlighting. 2 | " Maintainer: Matt Superdock 3 | " Version: 3.0 4 | " License: MIT 5 | 6 | if exists('g:foldout_loaded') 7 | finish 8 | else 9 | let g:foldout_loaded = 1 10 | endif 11 | 12 | " ## Global options 13 | 14 | " Pattern matched against file names to determine whether to enable foldout. If 15 | " the empty string, foldout is never automatically enabled. Defaults to `*.*`. 16 | if !exists('g:foldout_files') 17 | let g:foldout_files = '*.*' 18 | endif 19 | 20 | " Indicates whether to let foldout handle saving and loading view data. 21 | " If 1, foldout calls `loadview` and `mkview` at the appropriate times in 22 | " buffers where foldout is enabled, to save view data (like folds & cursor 23 | " position) according to the value of the `viewoptions` option. 24 | " If 0, foldout does not save or load view data. In this case, it is 25 | " recommended not to use `loadview` and `mkview` at all, since calling these 26 | " commands must be done in a particular order relative to foldout commands. 27 | " Defaults to 0. 28 | if !exists('g:foldout_save') 29 | let g:foldout_save = 0 30 | endif 31 | 32 | " ## Buffer options 33 | 34 | " Each of these options has a global variable and a buffer-local variable 35 | " (prefixed by `b` instead of `g`). The buffer-local variable takes precedence; 36 | " the global variable serves as a default across all buffers. 37 | 38 | " A one-character string representing the repeated character in headings. 39 | if !exists('g:foldout_heading_symbol') 40 | let g:foldout_heading_symbol = '#' 41 | elseif len(g:foldout_heading_symbol) != 1 42 | throw 'g:foldout_heading_symbol must have length 1.' 43 | endif 44 | 45 | " The upper limit on the number of heading levels; defaults to 6. 46 | if !exists('g:foldout_max_level') 47 | let g:foldout_max_level = 6 48 | endif 49 | 50 | " The first level at which to enable folding; defaults to folding all levels. 51 | if !exists('g:foldout_min_fold') 52 | let g:foldout_min_fold = 1 53 | endif 54 | 55 | " Pattern matched against last line of section in `foldout#append`. If it 56 | " matches, an empty line is inserted before appending text. The default value 57 | " is the null pattern, to always insert an empty line. 58 | if !exists('g:foldout_append_pattern') 59 | let g:foldout_append_pattern = '\@!' 60 | endif 61 | 62 | " Prefix text to insert in `foldout#append`; defaults to an empty line. 63 | if !exists('g:foldout_append_text') 64 | let g:foldout_append_text = '' 65 | endif 66 | 67 | " ## Enable 68 | 69 | " Enable foldout in files according to `g:foldout_files` setting. 70 | if g:foldout_files != '' 71 | execute 'autocmd BufWinEnter ' . g:foldout_files 72 | \ . ' call foldout#enable()' 73 | endif 74 | 75 | " Enable saving view data in files according to `g:foldout_files` setting. 76 | if g:foldout_files != '' && g:foldout_save 77 | " The `loadview` call must occur after foldout#enable(), or folds are lost. 78 | " We set `foldmethod` to `syntax`, since FastFold changes it to `manual`. 79 | execute 'autocmd BufWinEnter ' . g:foldout_files 80 | \ . ' silent! loadview' 81 | execute 'autocmd BufWinLeave ' . g:foldout_files 82 | \ . " let &l:foldmethod = 'syntax'" 83 | execute 'autocmd BufWinLeave ' . g:foldout_files 84 | \ . ' mkview' 85 | endif 86 | 87 | " Ensure that fastfold is always activated. 88 | let g:fastfold_minlines = 0 89 | 90 | --------------------------------------------------------------------------------