1" Vim indent file 2" Language: Shell Script 3" Maintainer: Christian Brabandt <[email protected]> 4" Original Author: Nikolai Weibull <[email protected]> 5" Previous Maintainer: Peter Aronoff <[email protected]> 6" Latest Revision: 2019-04-27 7" License: Vim (see :h license) 8" Repository: https://github.com/chrisbra/vim-sh-indent 9" Changelog: 10" 20190428 - De-indent fi correctly when typing with 11" https://github.com/chrisbra/vim-sh-indent/issues/15 12" 20190325 - Indent fi; correctly 13" https://github.com/chrisbra/vim-sh-indent/issues/14 14" 20190319 - Indent arrays (only zsh and bash) 15" https://github.com/chrisbra/vim-sh-indent/issues/13 16" 20190316 - Make use of searchpairpos for nested if sections 17" fixes https://github.com/chrisbra/vim-sh-indent/issues/11 18" 20190201 - Better check for closing if sections 19" 20180724 - make check for zsh syntax more rigid (needs word-boundaries) 20" 20180326 - better support for line continuation 21" 20180325 - better detection of function definitions 22" 20180127 - better support for zsh complex commands 23" 20170808: - better indent of line continuation 24" 20170502: - get rid of buffer-shiftwidth function 25" 20160912: - preserve indentation of here-doc blocks 26" 20160627: - detect heredocs correctly 27" 20160213: - detect function definition correctly 28" 20160202: - use shiftwidth() function 29" 20151215: - set b:undo_indent variable 30" 20150728: - add foreach detection for zsh 31 32if exists("b:did_indent") 33 finish 34endif 35let b:did_indent = 1 36 37setlocal indentexpr=GetShIndent() 38setlocal indentkeys+=0=then,0=do,0=else,0=elif,0=fi,0=esac,0=done,0=end,),0=;;,0=;& 39setlocal indentkeys+=0=fin,0=fil,0=fip,0=fir,0=fix 40setlocal indentkeys-=:,0# 41setlocal nosmartindent 42 43let b:undo_indent = 'setlocal indentexpr< indentkeys< smartindent<' 44 45if exists("*GetShIndent") 46 finish 47endif 48 49let s:cpo_save = &cpo 50set cpo&vim 51 52let s:sh_indent_defaults = { 53 \ 'default': function('shiftwidth'), 54 \ 'continuation-line': function('shiftwidth'), 55 \ 'case-labels': function('shiftwidth'), 56 \ 'case-statements': function('shiftwidth'), 57 \ 'case-breaks': 0 } 58 59function! s:indent_value(option) 60 let Value = exists('b:sh_indent_options') 61 \ && has_key(b:sh_indent_options, a:option) ? 62 \ b:sh_indent_options[a:option] : 63 \ s:sh_indent_defaults[a:option] 64 if type(Value) == type(function('type')) 65 return Value() 66 endif 67 return Value 68endfunction 69 70function! GetShIndent() 71 let curline = getline(v:lnum) 72 let lnum = prevnonblank(v:lnum - 1) 73 if lnum == 0 74 return 0 75 endif 76 let line = getline(lnum) 77 78 let pnum = prevnonblank(lnum - 1) 79 let pline = getline(pnum) 80 let ind = indent(lnum) 81 82 " Check contents of previous lines 83 if line =~ '^\s*\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>' || 84 \ (&ft is# 'zsh' && line =~ '\<\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>') 85 if !s:is_end_expression(line) 86 let ind += s:indent_value('default') 87 endif 88 elseif s:is_case_label(line, pnum) 89 if !s:is_case_ended(line) 90 let ind += s:indent_value('case-statements') 91 endif 92 " function definition 93 elseif s:is_function_definition(line) 94 if line !~ '}\s*\%(#.*\)\=$' 95 let ind += s:indent_value('default') 96 endif 97 " array (only works for zsh or bash) 98 elseif s:is_array(line) && line !~ ')\s*$' && (&ft is# 'zsh' || s:is_bash()) 99 let ind += s:indent_value('continuation-line') 100 " end of array 101 elseif curline =~ '^\s*)$' 102 let ind -= s:indent_value('continuation-line') 103 elseif s:is_continuation_line(line) 104 if pnum == 0 || !s:is_continuation_line(pline) 105 let ind += s:indent_value('continuation-line') 106 endif 107 elseif s:end_block(line) && !s:start_block(line) 108 let ind -= s:indent_value('default') 109 elseif pnum != 0 && 110 \ s:is_continuation_line(pline) && 111 \ !s:end_block(curline) && 112 \ !s:is_end_expression(curline) 113 " only add indent, if line and pline is in the same block 114 let i = v:lnum 115 let ind2 = indent(s:find_continued_lnum(pnum)) 116 while !s:is_empty(getline(i)) && i > pnum 117 let i -= 1 118 endw 119 if i == pnum 120 let ind += ind2 121 else 122 let ind = ind2 123 endif 124 endif 125 126 let pine = line 127 " Check content of current line 128 let line = curline 129 " Current line is a endif line, so get indent from start of "if condition" line 130 " TODO: should we do the same for other "end" lines? 131 if curline =~ '^\s*\%(fi\);\?\s*\%(#.*\)\=$' 132 let previous_line = searchpair('\<if\>', '', '\<fi\>\zs', 'bnW') 133 if previous_line > 0 134 let ind = indent(previous_line) 135 endif 136 elseif line =~ '^\s*\%(then\|do\|else\|elif\|done\|end\)\>' || s:end_block(line) 137 let ind -= s:indent_value('default') 138 elseif line =~ '^\s*esac\>' && s:is_case_empty(getline(v:lnum - 1)) 139 let ind -= s:indent_value('default') 140 elseif line =~ '^\s*esac\>' 141 let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ? 142 \ 0 : s:indent_value('case-statements')) + 143 \ s:indent_value('case-labels') 144 if s:is_case_break(pine) 145 let ind += s:indent_value('case-breaks') 146 endif 147 elseif s:is_case_label(line, lnum) 148 if s:is_case(pine) 149 let ind = indent(lnum) + s:indent_value('case-labels') 150 else 151 let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ? 152 \ 0 : s:indent_value('case-statements')) - 153 \ s:indent_value('case-breaks') 154 endif 155 elseif s:is_case_break(line) 156 let ind -= s:indent_value('case-breaks') 157 elseif s:is_here_doc(line) 158 let ind = 0 159 " statements, executed within a here document. Keep the current indent 160 elseif match(map(synstack(v:lnum, 1), 'synIDattr(v:val, "name")'), '\c\mheredoc') > -1 161 return indent(v:lnum) 162 elseif s:is_comment(line) && s:is_empty(getline(v:lnum-1)) 163 return indent(v:lnum) 164 endif 165 166 return ind > 0 ? ind : 0 167endfunction 168 169function! s:is_continuation_line(line) 170 " Comment, cannot be a line continuation 171 if a:line =~ '^\s*#' 172 return 0 173 else 174 " start-of-line 175 " \\ or && or || or | 176 " followed optionally by { or # 177 return a:line =~ '\%(\%(^\|[^\\]\)\\\|&&\|||\||\)' . 178 \ '\s*\({\s*\)\=\(#.*\)\=$' 179 endif 180endfunction 181 182function! s:find_continued_lnum(lnum) 183 let i = a:lnum 184 while i > 1 && s:is_continuation_line(getline(i - 1)) 185 let i -= 1 186 endwhile 187 return i 188endfunction 189 190function! s:is_function_definition(line) 191 return a:line =~ '^\s*\<\k\+\>\s*()\s*{' || 192 \ a:line =~ '^\s*{' || 193 \ a:line =~ '^\s*function\s*\w\S\+\s*\%(()\)\?\s*{' 194endfunction 195 196function! s:is_array(line) 197 return a:line =~ '^\s*\<\k\+\>=(' 198endfunction 199 200function! s:is_case_label(line, pnum) 201 if a:line !~ '^\s*(\=.*)' 202 return 0 203 endif 204 205 if a:pnum > 0 206 let pine = getline(a:pnum) 207 if !(s:is_case(pine) || s:is_case_ended(pine)) 208 return 0 209 endif 210 endif 211 212 let suffix = substitute(a:line, '^\s*(\=', "", "") 213 let nesting = 0 214 let i = 0 215 let n = strlen(suffix) 216 while i < n 217 let c = suffix[i] 218 let i += 1 219 if c == '\\' 220 let i += 1 221 elseif c == '(' 222 let nesting += 1 223 elseif c == ')' 224 if nesting == 0 225 return 1 226 endif 227 let nesting -= 1 228 endif 229 endwhile 230 return 0 231endfunction 232 233function! s:is_case(line) 234 return a:line =~ '^\s*case\>' 235endfunction 236 237function! s:is_case_break(line) 238 return a:line =~ '^\s*;[;&]' 239endfunction 240 241function! s:is_here_doc(line) 242 if a:line =~ '^\w\+$' 243 let here_pat = '<<-\?'. s:escape(a:line). '\$' 244 return search(here_pat, 'bnW') > 0 245 endif 246 return 0 247endfunction 248 249function! s:is_case_ended(line) 250 return s:is_case_break(a:line) || a:line =~ ';[;&]\s*\%(#.*\)\=$' 251endfunction 252 253function! s:is_case_empty(line) 254 if a:line =~ '^\s*$' || a:line =~ '^\s*#' 255 return s:is_case_empty(getline(v:lnum - 1)) 256 else 257 return a:line =~ '^\s*case\>' 258 endif 259endfunction 260 261function! s:escape(pattern) 262 return '\V'. escape(a:pattern, '\\') 263endfunction 264 265function! s:is_empty(line) 266 return a:line =~ '^\s*$' 267endfunction 268 269function! s:end_block(line) 270 return a:line =~ '^\s*}' 271endfunction 272 273function! s:start_block(line) 274 return a:line =~ '{\s*\(#.*\)\?$' 275endfunction 276 277function! s:find_start_block(lnum) 278 let i = a:lnum 279 while i > 1 && !s:start_block(getline(i)) 280 let i -= 1 281 endwhile 282 return i 283endfunction 284 285function! s:is_comment(line) 286 return a:line =~ '^\s*#' 287endfunction 288 289function! s:is_end_expression(line) 290 return a:line =~ '\<\%(fi\|esac\|done\|end\)\>\s*\%(#.*\)\=$' 291endfunction 292 293function! s:is_bash() 294 return get(g:, 'is_bash', 0) || get(b:, 'is_bash', 0) 295endfunction 296 297let &cpo = s:cpo_save 298unlet s:cpo_save 299