Skip to content

Commit 698ecca

Browse files
Implement folding (#445)
* Implement folding * Allow disabling folding globally
1 parent 9153561 commit 698ecca

File tree

6 files changed

+263
-0
lines changed

6 files changed

+263
-0
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ At the moment, you have two options:
5353

5454
For more information, refer to the readme and documentation of the respective plugins.
5555

56+
## Folding
57+
58+
You can let the language server automatically handle folding for you. To enable this, you have to set `'foldmethod'`, `'foldexpr'` and (optionally) `'foldtext'`:
59+
60+
```vim
61+
set foldmethod=expr
62+
\ foldexpr=lsp#ui#vim#folding#foldexpr()
63+
\ foldtext=lsp#ui#vim#folding#foldtext()
64+
```
65+
66+
If you would like to disable folding globally, you can add this to your configuration:
67+
68+
```vim
69+
let g:lsp_fold_enabled = 0
70+
```
71+
72+
Also see `:h vim-lsp-folding`.
73+
5674
## Supported commands
5775

5876
**Note:**

autoload/lsp.vim

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ function! lsp#default_get_supported_capabilities(server_info) abort
394394
\ 'symbolKind': {
395395
\ 'valueSet': lsp#ui#vim#utils#get_symbol_kinds()
396396
\ }
397+
\ },
398+
\ 'foldingRange': {
399+
\ 'lineFoldingOnly': v:true
397400
\ }
398401
\ }
399402
\ }
@@ -529,6 +532,7 @@ function! s:ensure_changed(buf, server_name, cb) abort
529532
\ 'contentChanges': s:text_changes(a:buf, a:server_name),
530533
\ }
531534
\ })
535+
call lsp#ui#vim#folding#send_request(a:server_name, a:buf, 0)
532536

533537
let l:msg = s:new_rpc_success('textDocument/didChange sent', { 'server_name': a:server_name, 'path': l:path })
534538
call lsp#log(l:msg)
@@ -567,6 +571,8 @@ function! s:ensure_open(buf, server_name, cb) abort
567571
\ },
568572
\ })
569573

574+
call lsp#ui#vim#folding#send_request(a:server_name, a:buf, 0)
575+
570576
let l:msg = s:new_rpc_success('textDocument/open sent', { 'server_name': a:server_name, 'path': l:path, 'filetype': getbufvar(a:buf, '&filetype') })
571577
call lsp#log(l:msg)
572578
call a:cb(l:msg)

autoload/lsp/capabilities.vim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ function! lsp#capabilities#has_document_highlight_provider(server_name) abort
7373
return s:has_bool_provider(a:server_name, 'documentHighlightProvider')
7474
endfunction
7575

76+
function! lsp#capabilities#has_folding_range_provider(server_name) abort
77+
return s:has_bool_provider(a:server_name, 'foldingRangeProvider')
78+
endfunction
79+
7680
" [supports_did_save (boolean), { 'includeText': boolean }]
7781
function! lsp#capabilities#get_text_document_save_registration_options(server_name) abort
7882
let l:capabilities = lsp#get_server_capabilities(a:server_name)

autoload/lsp/ui/vim/folding.vim

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
let s:folding_ranges = {}
2+
let s:textprop_name = 'vim-lsp-folding-linenr'
3+
4+
function! s:find_servers() abort
5+
return filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_folding_range_provider(v:val)')
6+
endfunction
7+
8+
function! lsp#ui#vim#folding#fold(sync) abort
9+
let l:servers = s:find_servers()
10+
11+
if len(l:servers) == 0
12+
call lsp#utils#error('Folding not supported for ' . &filetype)
13+
return
14+
endif
15+
16+
let l:server = l:servers[0]
17+
call lsp#ui#vim#folding#send_request(l:server, bufnr('%'), a:sync)
18+
endfunction
19+
20+
function! s:set_textprops(buf) abort
21+
" Use zero-width text properties to act as a sort of "mark" in the buffer.
22+
" This is used to remember the line numbers at the time the request was
23+
" sent. We will let Vim handle updating the line numbers when the user
24+
" inserts or deletes text.
25+
26+
" Create text property, if not already defined
27+
silent! call prop_type_add(s:textprop_name, {'bufnr': a:buf})
28+
29+
" First, clear all markers from the previous run
30+
call prop_remove({'type': s:textprop_name, 'bufnr': a:buf})
31+
32+
" Add markers to each line
33+
let l:i = 1
34+
while l:i <= line('$')
35+
call prop_add(l:i, 1, {'bufnr': a:buf, 'type': s:textprop_name, 'id': l:i})
36+
let l:i += 1
37+
endwhile
38+
endfunction
39+
40+
function! lsp#ui#vim#folding#send_request(server_name, buf, sync) abort
41+
if !lsp#capabilities#has_folding_range_provider(a:server_name)
42+
return
43+
endif
44+
45+
if !g:lsp_fold_enabled
46+
call lsp#log('Skip sending fold request: folding was disabled explicitly')
47+
return
48+
endif
49+
50+
if has('textprop')
51+
call s:set_textprops(a:buf)
52+
endif
53+
54+
call lsp#send_request(a:server_name, {
55+
\ 'method': 'textDocument/foldingRange',
56+
\ 'params': {
57+
\ 'textDocument': lsp#get_text_document_identifier(a:buf)
58+
\ },
59+
\ 'on_notification': function('s:handle_fold_request', [a:server_name]),
60+
\ 'sync': a:sync
61+
\ })
62+
endfunction
63+
64+
function! s:foldexpr(server, buf, linenr) abort
65+
let l:foldlevel = 0
66+
let l:prefix = ''
67+
68+
for l:folding_range in s:folding_ranges[a:server][a:buf]
69+
if type(l:folding_range) == type({}) &&
70+
\ has_key(l:folding_range, 'startLine') &&
71+
\ has_key(l:folding_range, 'endLine')
72+
let l:start = l:folding_range['startLine'] + 1
73+
let l:end = l:folding_range['endLine'] + 1
74+
75+
if (l:start <= a:linenr) && (a:linenr <= l:end)
76+
let l:foldlevel += 1
77+
endif
78+
79+
if l:start == a:linenr
80+
let l:prefix = '>'
81+
elseif l:end == a:linenr
82+
let l:prefix = '<'
83+
endif
84+
endif
85+
endfor
86+
87+
" Only return marker if a fold starts/ends at this line.
88+
" Otherwise, return '='.
89+
return (l:prefix ==# '') ? '=' : (l:prefix . l:foldlevel)
90+
endfunction
91+
92+
" Searches for text property of the correct type on the given line.
93+
" Returns the original linenr on success, or -1 if no textprop of the correct
94+
" type is associated with this line.
95+
function! s:get_textprop_line(linenr) abort
96+
let l:props = filter(prop_list(a:linenr), {idx, prop -> prop['type'] ==# s:textprop_name})
97+
98+
if empty(l:props)
99+
return -1
100+
else
101+
return l:props[0]['id']
102+
endif
103+
endfunction
104+
105+
function! lsp#ui#vim#folding#foldexpr() abort
106+
let l:servers = s:find_servers()
107+
108+
if len(l:servers) == 0
109+
return
110+
endif
111+
112+
let l:server = l:servers[0]
113+
114+
if has('textprop')
115+
" Does the current line have a textprop with original line info?
116+
let l:textprop_line = s:get_textprop_line(v:lnum)
117+
118+
if l:textprop_line == -1
119+
" No information for current line available, so use indent for
120+
" previous line.
121+
return '='
122+
else
123+
" Info available, use foldexpr as it would be with original line
124+
" number
125+
return s:foldexpr(l:server, bufnr('%'), l:textprop_line)
126+
endif
127+
else
128+
return s:foldexpr(l:server, bufnr('%'), v:lnum)
129+
endif
130+
endfunction
131+
132+
function! lsp#ui#vim#folding#foldtext() abort
133+
let l:num_lines = v:foldend - v:foldstart + 1
134+
let l:summary = getline(v:foldstart) . '...'
135+
136+
" Join all lines in the fold
137+
let l:combined_lines = ''
138+
let l:i = v:foldstart
139+
while l:i <= v:foldend
140+
let l:combined_lines .= getline(l:i) . ' '
141+
let l:i += 1
142+
endwhile
143+
144+
" Check if we're in a comment
145+
let l:comment_regex = '\V' . substitute(&l:commentstring, '%s', '\\.\\*', '')
146+
if l:combined_lines =~? l:comment_regex
147+
let l:summary = l:combined_lines
148+
endif
149+
150+
return l:summary . ' (' . l:num_lines . ' ' . (l:num_lines == 1 ? 'line' : 'lines') . ') '
151+
endfunction
152+
153+
function! s:handle_fold_request(server, data) abort
154+
if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result')
155+
return
156+
endif
157+
158+
let l:result = a:data['response']['result']
159+
160+
if type(l:result) != type([])
161+
return
162+
endif
163+
164+
let l:uri = a:data['request']['params']['textDocument']['uri']
165+
let l:path = lsp#utils#uri_to_path(l:uri)
166+
let l:bufnr = bufnr(l:path)
167+
168+
if l:bufnr < 0
169+
return
170+
endif
171+
172+
if !has_key(s:folding_ranges, a:server)
173+
let s:folding_ranges[a:server] = {}
174+
endif
175+
let s:folding_ranges[a:server][l:bufnr] = l:result
176+
177+
" Set 'foldmethod' back to 'expr', which forces a re-evaluation of
178+
" 'foldexpr'. Only do this if the user hasn't changed 'foldmethod',
179+
" and this is the correct buffer.
180+
let l:current_window = winnr()
181+
windo if &l:foldmethod ==# 'expr' && bufnr('%') == l:bufnr | let &l:foldmethod = 'expr' | endif
182+
execute l:current_window . 'wincmd w'
183+
endfunction

doc/vim-lsp.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ CONTENTS *vim-lsp-contents*
3333
g:lsp_preview_max_width |g:lsp_preview_max_width|
3434
g:lsp_preview_max_height |g:lsp_preview_max_height|
3535
g:lsp_signature_help_enabled |g:lsp_signature_help_enabled|
36+
g:lsp_fold_enabled |g:lsp_fold_enabled|
3637
Functions |vim-lsp-functions|
3738
enable |vim-lsp-enable|
3839
disable |vim-lsp-disable|
@@ -47,6 +48,8 @@ CONTENTS *vim-lsp-contents*
4748
LspDocumentDiagnostics |LspDocumentDiagnostics|
4849
LspDeclaration |LspDeclaration|
4950
LspDefinition |LspDefinition|
51+
LspDocumentFold |LspDocumentFold|
52+
LspDocumentFoldSync |LspDocumentFoldSync|
5053
LspDocumentFormat |LspDocumentFormat|
5154
LspDocumentFormatSync |LspDocumentFormatSync|
5255
LspDocumentRangeFormat |LspDocumentRangeFormat|
@@ -75,6 +78,7 @@ CONTENTS *vim-lsp-contents*
7578
omnifunc |vim-lsp-omnifunc|
7679
asyncomplete.vim |vim-lsp-asyncomplete|
7780
Snippets |vim-lsp-snippets|
81+
Folding |vim-lsp-folding|
7882
License |vim-lsp-license|
7983

8084

@@ -481,6 +485,13 @@ g:lsp_signature_help_enabled *g:lsp_signature_help_enabled*
481485
let g:lsp_signature_help_enabled = 1
482486
let g:lsp_signature_help_enabled = 0
483487
488+
g:lsp_fold_enabled *g:lsp_fold_enabled*
489+
Type: |Number|
490+
Default: `1`
491+
492+
Determines whether or not folding is enabled globally. Set to `0` to
493+
disable sending requests.
494+
484495
===============================================================================
485496
FUNCTIONS *vim-lsp-functions*
486497

@@ -723,6 +734,14 @@ Go to definition.
723734

724735
Also see |LspPeekDefinition|.
725736

737+
LspDocumentFold *LspDocumentFold*
738+
739+
Recalculate folds for the current buffer.
740+
741+
LspDocumentFoldSync *LspDocumentFoldSync*
742+
743+
Same as |LspDocumentFold|, but synchronous.
744+
726745
LspDocumentFormat *LspDocumentFormat*
727746

728747
Format the entire document.
@@ -937,6 +956,36 @@ https://github.com/thomasfaingnaert/vim-lsp-neosnippet
937956
Refer to the readme and docs of vim-lsp-ultisnips and vim-lsp-neosnippet for
938957
more information and configuration options.
939958

959+
===============================================================================
960+
Folding *vim-lsp-folding*
961+
962+
You can also let the language server handle folding for you. To enable this
963+
feature, you will have to set |'foldmethod'|, |'foldexpr'| and |'foldtext'| (the
964+
latter is optional) correctly:
965+
966+
set foldmethod=expr
967+
\ foldexpr=lsp#ui#vim#folding#foldexpr()
968+
\ foldtext=lsp#ui#vim#folding#foldtext()
969+
970+
Also, make sure you have not disabled folding globally, see
971+
|g:lsp_fold_enabled|.
972+
973+
You may want to enable this only for certain filetypes, e.g. for Javascript
974+
only:
975+
976+
augroup lsp_folding
977+
autocmd!
978+
autocmd FileType javascript setlocal
979+
\ foldmethod=expr
980+
\ foldexpr=lsp#ui#vim#folding#foldexpr()
981+
\ foldtext=lsp#ui#vim#folding#foldtext()
982+
augroup end
983+
984+
To display open and closed folds at the side of the window, see
985+
|'foldcolumn'|.
986+
If you want to remove the dashes at the end of the folds, you can change
987+
the fold item of |'fillchars'|.
988+
940989
===============================================================================
941990
License *vim-lsp-license*
942991

plugin/lsp.vim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ let g:lsp_peek_alignment = get(g:, 'lsp_peek_alignment', 'center')
3535
let g:lsp_preview_max_width = get(g:, 'lsp_preview_max_width', -1)
3636
let g:lsp_preview_max_height = get(g:, 'lsp_preview_max_height', -1)
3737
let g:lsp_signature_help_enabled = get(g:, 'lsp_signature_help_enabled', 1)
38+
let g:lsp_fold_enabled = get(g:, 'lsp_fold_enabled', 1)
3839

3940
let g:lsp_get_vim_completion_item = get(g:, 'lsp_get_vim_completion_item', [function('lsp#omni#default_get_vim_completion_item')])
4041
let g:lsp_get_supported_capabilities = get(g:, 'lsp_get_supported_capabilities', [function('lsp#default_get_supported_capabilities')])
@@ -72,6 +73,8 @@ command! -nargs=0 LspStatus echo lsp#get_server_status()
7273
command! LspNextReference call lsp#ui#vim#references#jump(+1)
7374
command! LspPreviousReference call lsp#ui#vim#references#jump(-1)
7475
command! -nargs=? -complete=customlist,lsp#utils#empty_complete LspSignatureHelp call lsp#ui#vim#signature_help#get_signature_help_under_cursor()
76+
command! LspDocumentFold call lsp#ui#vim#folding#fold(0)
77+
command! LspDocumentFoldSync call lsp#ui#vim#folding#fold(1)
7578

7679
nnoremap <plug>(lsp-code-action) :<c-u>call lsp#ui#vim#code_action()<cr>
7780
nnoremap <plug>(lsp-declaration) :<c-u>call lsp#ui#vim#declaration(0)<cr>

0 commit comments

Comments
 (0)