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