1" Vim indent file 2" Language: Ruby 3" Maintainer: Gavin Sinclair <gsinclair at soyabean.com.au> 4" Developer: Nikolai Weibull <source at pcppopper.org> 5" Info: $Id$ 6" URL: http://vim-ruby.rubyforge.org/ 7" Anon CVS: See above site 8" Licence: GPL (http://www.gnu.org) 9" Disclaimer: 10" This program is distributed in the hope that it will be useful, 11" but WITHOUT ANY WARRANTY; without even the implied warranty of 12" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13" GNU General Public License for more details. 14" ---------------------------------------------------------------------------- 15 16" 0. Initialization {{{1 17" ================= 18 19" Only load this indent file when no other was loaded. 20if exists("b:did_indent") 21 finish 22endif 23let b:did_indent = 1 24 25" Now, set up our indentation expression and keys that trigger it. 26setlocal indentexpr=GetRubyIndent() 27setlocal indentkeys=0{,0},0),0],!^F,o,O,e 28setlocal indentkeys+==end,=elsif,=when,=ensure,=rescue,==begin,==end 29 30" Only define the function once. 31if exists("*GetRubyIndent") 32 finish 33endif 34 35let s:cpo_save = &cpo 36set cpo&vim 37 38" 1. Variables {{{1 39" ============ 40 41" Regex of syntax group names that are or delimit string or are comments. 42let s:syng_strcom = '\<ruby\%(String\|StringDelimiter\|ASCIICode' . 43 \ '\|Interpolation\|NoInterpolation\|Escape\|Comment\|Documentation\)\>' 44 45" Regex of syntax group names that are strings or comments. 46let s:syng_strcom2 = '\<ruby\%(String' . 47 \ '\|Interpolation\|NoInterpolation\|Escape\|Comment\|Documentation\)\>' 48 49" Regex of syntax group names that are strings. 50let s:syng_string = 51 \ '\<ruby\%(String\|Interpolation\|NoInterpolation\|Escape\)\>' 52 53" Regex of syntax group names that are strings or documentation. 54let s:syng_stringdoc = 55 \'\<ruby\%(String\|Interpolation\|NoInterpolation\|Escape\|Documentation\)\>' 56 57" Expression used to check whether we should skip a match with searchpair(). 58let s:skip_expr = 59 \ "synIDattr(synID(line('.'),col('.'),0),'name') =~ '".s:syng_strcom."'" 60 61" Regex used for words that, at the start of a line, add a level of indent. 62let s:ruby_indent_keywords = '^\s*\zs\<\%(module\|class\|def\|if\|for' . 63 \ '\|while\|until\|else\|elsif\|case\|when\|unless\|begin\|ensure' . 64 \ '\|rescue\)\>' . 65 \ '\|\%([*+/,=:-]\|<<\|>>\)\s*\zs' . 66 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\)\>' 67 68" Regex used for words that, at the start of a line, remove a level of indent. 69let s:ruby_deindent_keywords = 70 \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|end\)\>' 71 72" Regex that defines the start-match for the 'end' keyword. 73"let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>' 74" TODO: the do here should be restricted somewhat (only at end of line)? 75let s:end_start_regex = '^\s*\zs\<\%(module\|class\|def\|if\|for' . 76 \ '\|while\|until\|case\|unless\|begin\)\>' . 77 \ '\|\%([*+/,=:-]\|<<\|>>\)\s*\zs' . 78 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\)\>' . 79 \ '\|\<do\>' 80 81" Regex that defines the middle-match for the 'end' keyword. 82let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue\>\|when\|elsif\)\>' 83 84" Regex that defines the end-match for the 'end' keyword. 85let s:end_end_regex = '\%(^\|[^.:]\)\@<=\<end\>' 86 87" Expression used for searchpair() call for finding match for 'end' keyword. 88let s:end_skip_expr = s:skip_expr . 89 \ ' || (expand("<cword>") == "do"' . 90 \ ' && getline(".") =~ "^\\s*\\<while\\|until\\|for\\>")' 91 92" Regex that defines continuation lines, not including (, {, or [. 93let s:continuation_regex = '\%([\\*+/.,=:-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$' 94 95" Regex that defines continuation lines. 96" TODO: this needs to deal with if ...: and so on 97let s:continuation_regex2 = 98 \ '\%([\\*+/.,=:({[-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$' 99 100" Regex that defines blocks. 101let s:block_regex = 102 \ '\%(\<do\>\|{\)\s*\%(|\%([*@]\=\h\w*,\=\s*\)\%(,\s*[*@]\=\h\w*\)*|\)\=\s*\%(#.*\)\=$' 103 104" 2. Auxiliary Functions {{{1 105" ====================== 106 107" Check if the character at lnum:col is inside a string, comment, or is ascii. 108function s:IsInStringOrComment(lnum, col) 109 return synIDattr(synID(a:lnum, a:col, 0), 'name') =~ s:syng_strcom 110endfunction 111 112" Check if the character at lnum:col is inside a string or comment. 113function s:IsInStringOrComment2(lnum, col) 114 return synIDattr(synID(a:lnum, a:col, 0), 'name') =~ s:syng_strcom2 115endfunction 116 117" Check if the character at lnum:col is inside a string. 118function s:IsInString(lnum, col) 119 return synIDattr(synID(a:lnum, a:col, 0), 'name') =~ s:syng_string 120endfunction 121 122" Check if the character at lnum:col is inside a string or documentation. 123function s:IsInStringOrDocumentation(lnum, col) 124 return synIDattr(synID(a:lnum, a:col, 0), 'name') =~ s:syng_stringdoc 125endfunction 126 127" Find line above 'lnum' that isn't empty, in a comment, or in a string. 128function s:PrevNonBlankNonString(lnum) 129 let in_block = 0 130 let lnum = prevnonblank(a:lnum) 131 while lnum > 0 132 " Go in and out of blocks comments as necessary. 133 " If the line isn't empty (with opt. comment) or in a string, end search. 134 let line = getline(lnum) 135 if line =~ '^=begin$' 136 if in_block 137 let in_block = 0 138 else 139 break 140 endif 141 elseif !in_block && line =~ '^=end$' 142 let in_block = 1 143 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1) 144 \ && s:IsInStringOrComment(lnum, strlen(line))) 145 break 146 endif 147 let lnum = prevnonblank(lnum - 1) 148 endwhile 149 return lnum 150endfunction 151 152" Find line above 'lnum' that started the continuation 'lnum' may be part of. 153function s:GetMSL(lnum) 154 " Start on the line we're at and use its indent. 155 let msl = a:lnum 156 let lnum = s:PrevNonBlankNonString(a:lnum - 1) 157 while lnum > 0 158 " If we have a continuation line, or we're in a string, use line as MSL. 159 " Otherwise, terminate search as we have found our MSL already. 160 let line = getline(lnum) 161 let col = match(line, s:continuation_regex2) + 1 162 if (col > 0 && !s:IsInStringOrComment(lnum, col)) 163 \ || s:IsInString(lnum, strlen(line)) 164 let msl = lnum 165 else 166 break 167 endif 168 let lnum = s:PrevNonBlankNonString(lnum - 1) 169 endwhile 170 return msl 171endfunction 172 173" Check if line 'lnum' has more opening brackets than closing ones. 174function s:LineHasOpeningBrackets(lnum) 175 let open_0 = 0 176 let open_2 = 0 177 let open_4 = 0 178 let line = getline(a:lnum) 179 let pos = match(line, '[][(){}]', 0) 180 while pos != -1 181 if !s:IsInStringOrComment(a:lnum, pos + 1) 182 let idx = stridx('(){}[]', line[pos]) 183 if idx % 2 == 0 184 let open_{idx} = open_{idx} + 1 185 else 186 let open_{idx - 1} = open_{idx - 1} - 1 187 endif 188 endif 189 let pos = match(line, '[][(){}]', pos + 1) 190 endwhile 191 return (open_0 > 0) . (open_2 > 0) . (open_4 > 0) 192endfunction 193 194function s:Match(lnum, regex) 195 let col = match(getline(a:lnum), a:regex) + 1 196 return col > 0 && !s:IsInStringOrComment(a:lnum, col) ? col : 0 197endfunction 198 199function s:MatchLast(lnum, regex) 200 let line = getline(a:lnum) 201 let col = match(line, '.*\zs' . a:regex) 202 while col != -1 && s:IsInStringOrComment(a:lnum, col) 203 let line = strpart(line, 0, col) 204 let col = match(line, '.*' . a:regex) 205 endwhile 206 return col + 1 207endfunction 208 209" 3. GetRubyIndent Function {{{1 210" ========================= 211 212function GetRubyIndent() 213 " 3.1. Setup {{{2 214 " ---------- 215 216 " Set up variables for restoring position in file. Could use v:lnum here. 217 let vcol = col('.') 218 219 " 3.2. Work on the current line {{{2 220 " ----------------------------- 221 222 " Get the current line. 223 let line = getline(v:lnum) 224 let ind = -1 225 226 " If we got a closing bracket on an empty line, find its match and indent 227 " according to it. For parentheses we indent to its column - 1, for the 228 " others we indent to the containing line's MSL's level. Return -1 if fail. 229 let col = matchend(line, '^\s*[]})]') 230 if col > 0 && !s:IsInStringOrComment(v:lnum, col) 231 call cursor(v:lnum, col) 232 let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2) 233 if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0 234 let ind = line[col-1]==')' ? virtcol('.')-1 : indent(s:GetMSL(line('.'))) 235 endif 236 return ind 237 endif 238 239 " If we have a =begin or =end set indent to first column. 240 if match(line, '^\s*\%(=begin\|=end\)$') != -1 241 return 0 242 endif 243 244 " If we have a deindenting keyword, find its match and indent to its level. 245 " TODO: this is messy 246 if s:Match(v:lnum, s:ruby_deindent_keywords) 247 call cursor(v:lnum, 1) 248 if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW', 249 \ s:end_skip_expr) > 0 250 let line = getline('.') 251 if strpart(line, 0, col('.') - 1) =~ '=\s*$' && 252 \ strpart(line, col('.') - 1, 2) !~ 'do' 253 let ind = virtcol('.') - 1 254 else 255 let ind = indent('.') 256 endif 257 endif 258 return ind 259 endif 260 261 " If we are in a multi-line string or line-comment, don't do anything to it. 262 if s:IsInStringOrDocumentation(v:lnum, matchend(line, '^\s*') + 1) 263 return indent('.') 264 endif 265 266 " 3.3. Work on the previous line. {{{2 267 " ------------------------------- 268 269 " Find a non-blank, non-multi-line string line above the current line. 270 let lnum = s:PrevNonBlankNonString(v:lnum - 1) 271 272 " At the start of the file use zero indent. 273 if lnum == 0 274 return 0 275 endif 276 277 " Set up variables for current line. 278 let line = getline(lnum) 279 let ind = indent(lnum) 280 281 " If the previous line ended with a block opening, add a level of indent. 282 if s:Match(lnum, s:block_regex) 283 return indent(s:GetMSL(lnum)) + &sw 284 endif 285 286 " If the previous line contained an opening bracket, and we are still in it, 287 " add indent depending on the bracket type. 288 if line =~ '[[({]' 289 let counts = s:LineHasOpeningBrackets(lnum) 290 if counts[0] == '1' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0 291 return virtcol('.') 292 elseif counts[1] == '1' || counts[2] == '1' 293 return ind + &sw 294 else 295 call cursor(v:lnum, vcol) 296 end 297 endif 298 299 " If the previous line ended with an "end", match that "end"s beginning's 300 " indent. 301 let col = s:Match(lnum, '\%(^\|[^.]\)\<end\>\s*\%(#.*\)\=$') 302 if col > 0 303 call cursor(lnum, col) 304 if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW', 305 \ s:end_skip_expr) > 0 306 let n = line('.') 307 let ind = indent('.') 308 let msl = s:GetMSL(n) 309 if msl != n 310 let ind = indent(msl) 311 end 312 return ind 313 endif 314 end 315 316 let col = s:Match(lnum, s:ruby_indent_keywords) 317 if col > 0 318 call cursor(lnum, col) 319 let ind = virtcol('.') - 1 + &sw 320" let ind = indent(lnum) + &sw 321 " TODO: make this better (we need to count them) (or, if a searchpair 322 " fails, we know that something is lacking an end and thus we indent a 323 " level 324 if s:Match(lnum, s:end_end_regex) 325 let ind = indent('.') 326 endif 327 return ind 328 endif 329 330 " 3.4. Work on the MSL line. {{{2 331 " -------------------------- 332 333 " Set up variables to use and search for MSL to the previous line. 334 let p_lnum = lnum 335 let lnum = s:GetMSL(lnum) 336 337 " If the previous line wasn't a MSL and is continuation return its indent. 338 " TODO: the || s:IsInString() thing worries me a bit. 339 if p_lnum != lnum 340 if s:Match(p_lnum,s:continuation_regex)||s:IsInString(p_lnum,strlen(line)) 341 return ind 342 endif 343 endif 344 345 " Set up more variables, now that we know we wasn't continuation bound. 346 let line = getline(lnum) 347 let msl_ind = indent(lnum) 348 349 " If the MSL line had an indenting keyword in it, add a level of indent. 350 " TODO: this does not take into account contrived things such as 351 " module Foo; class Bar; end 352 if s:Match(lnum, s:ruby_indent_keywords) 353 let ind = msl_ind + &sw 354 if s:Match(lnum, s:end_end_regex) 355 let ind = ind - &sw 356 endif 357 return ind 358 endif 359 360 " If the previous line ended with [*+/.-=], indent one extra level. 361 if s:Match(lnum, s:continuation_regex) 362 if lnum == p_lnum 363 let ind = msl_ind + &sw 364 else 365 let ind = msl_ind 366 endif 367 endif 368 369 " }}}2 370 371 return ind 372endfunction 373 374" }}}1 375 376let &cpo = s:cpo_save 377unlet s:cpo_save 378