1" Vim indent file 2" Language: Ruby 3" Maintainer: Nikolai Weibull <now at bitwi.se> 4" URL: https://github.com/vim-ruby/vim-ruby 5" Release Coordinator: Doug Kearns <[email protected]> 6 7" 0. Initialization {{{1 8" ================= 9 10" Only load this indent file when no other was loaded. 11if exists("b:did_indent") 12 finish 13endif 14let b:did_indent = 1 15 16setlocal nosmartindent 17 18" Now, set up our indentation expression and keys that trigger it. 19setlocal indentexpr=GetRubyIndent(v:lnum) 20setlocal indentkeys=0{,0},0),0],!^F,o,O,e 21setlocal indentkeys+==end,=else,=elsif,=when,=ensure,=rescue,==begin,==end 22 23" Only define the function once. 24if exists("*GetRubyIndent") 25 finish 26endif 27 28let s:cpo_save = &cpo 29set cpo&vim 30 31" 1. Variables {{{1 32" ============ 33 34" Regex of syntax group names that are or delimit strings/symbols or are comments. 35let s:syng_strcom = '\<ruby\%(Regexp\|RegexpDelimiter\|RegexpEscape' . 36 \ '\|Symbol\|String\|StringDelimiter\|StringEscape\|ASCIICode' . 37 \ '\|Interpolation\|NoInterpolation\|Comment\|Documentation\)\>' 38 39" Regex of syntax group names that are strings. 40let s:syng_string = 41 \ '\<ruby\%(String\|Interpolation\|NoInterpolation\|StringEscape\)\>' 42 43" Regex of syntax group names that are strings or documentation. 44let s:syng_stringdoc = 45 \'\<ruby\%(String\|Interpolation\|NoInterpolation\|StringEscape\|Documentation\)\>' 46 47" Expression used to check whether we should skip a match with searchpair(). 48let s:skip_expr = 49 \ "synIDattr(synID(line('.'),col('.'),1),'name') =~ '".s:syng_strcom."'" 50 51" Regex used for words that, at the start of a line, add a level of indent. 52let s:ruby_indent_keywords = '^\s*\zs\<\%(module\|class\|def\|if\|for' . 53 \ '\|while\|until\|else\|elsif\|case\|when\|unless\|begin\|ensure' . 54 \ '\|rescue\):\@!\>' . 55 \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' . 56 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>' 57 58" Regex used for words that, at the start of a line, remove a level of indent. 59let s:ruby_deindent_keywords = 60 \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|end\):\@!\>' 61 62" Regex that defines the start-match for the 'end' keyword. 63"let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>' 64" TODO: the do here should be restricted somewhat (only at end of line)? 65let s:end_start_regex = 66 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' . 67 \ '\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\):\@!\>' . 68 \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>' 69 70" Regex that defines the middle-match for the 'end' keyword. 71let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|elsif\):\@!\>' 72 73" Regex that defines the end-match for the 'end' keyword. 74let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>' 75 76" Expression used for searchpair() call for finding match for 'end' keyword. 77let s:end_skip_expr = s:skip_expr . 78 \ ' || (expand("<cword>") == "do"' . 79 \ ' && getline(".") =~ "^\\s*\\<\\(while\\|until\\|for\\):\\@!\\>")' 80 81" Regex that defines continuation lines, not including (, {, or [. 82let s:non_bracket_continuation_regex = '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$' 83 84" Regex that defines continuation lines. 85" TODO: this needs to deal with if ...: and so on 86let s:continuation_regex = 87 \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$' 88 89" Regex that defines bracket continuations 90let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$' 91 92" Regex that defines the first part of a splat pattern 93let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$' 94 95" Regex that defines blocks. 96" 97" Note that there's a slight problem with this regex and s:continuation_regex. 98" Code like this will be matched by both: 99" 100" method_call do |(a, b)| 101" 102" The reason is that the pipe matches a hanging "|" operator. 103" 104let s:block_regex = 105 \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|\s*(*\s*\%([*@&]\=\h\w*,\=\s*\)\%(,\s*(*\s*[*@&]\=\h\w*\s*)*\s*\)*|\)\=\s*\%(#.*\)\=$' 106 107let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex 108 109" 2. Auxiliary Functions {{{1 110" ====================== 111 112" Check if the character at lnum:col is inside a string, comment, or is ascii. 113function s:IsInStringOrComment(lnum, col) 114 return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_strcom 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, 1), '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, 1), 'name') =~ s:syng_stringdoc 125endfunction 126 127" Check if the character at lnum:col is inside a string delimiter 128function s:IsInStringDelimiter(lnum, col) 129 return synIDattr(synID(a:lnum, a:col, 1), 'name') == 'rubyStringDelimiter' 130endfunction 131 132" Find line above 'lnum' that isn't empty, in a comment, or in a string. 133function s:PrevNonBlankNonString(lnum) 134 let in_block = 0 135 let lnum = prevnonblank(a:lnum) 136 while lnum > 0 137 " Go in and out of blocks comments as necessary. 138 " If the line isn't empty (with opt. comment) or in a string, end search. 139 let line = getline(lnum) 140 if line =~ '^=begin' 141 if in_block 142 let in_block = 0 143 else 144 break 145 endif 146 elseif !in_block && line =~ '^=end' 147 let in_block = 1 148 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1) 149 \ && s:IsInStringOrComment(lnum, strlen(line))) 150 break 151 endif 152 let lnum = prevnonblank(lnum - 1) 153 endwhile 154 return lnum 155endfunction 156 157" Find line above 'lnum' that started the continuation 'lnum' may be part of. 158function s:GetMSL(lnum) 159 " Start on the line we're at and use its indent. 160 let msl = a:lnum 161 let msl_body = getline(msl) 162 let lnum = s:PrevNonBlankNonString(a:lnum - 1) 163 while lnum > 0 164 " If we have a continuation line, or we're in a string, use line as MSL. 165 " Otherwise, terminate search as we have found our MSL already. 166 let line = getline(lnum) 167 168 if s:Match(lnum, s:splat_regex) 169 " If the above line looks like the "*" of a splat, use the current one's 170 " indentation. 171 " 172 " Example: 173 " Hash[* 174 " method_call do 175 " something 176 " 177 return msl 178 elseif s:Match(line, s:non_bracket_continuation_regex) && 179 \ s:Match(msl, s:non_bracket_continuation_regex) 180 " If the current line is a non-bracket continuation and so is the 181 " previous one, keep its indent and continue looking for an MSL. 182 " 183 " Example: 184 " method_call one, 185 " two, 186 " three 187 " 188 let msl = lnum 189 elseif s:Match(lnum, s:non_bracket_continuation_regex) && 190 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex)) 191 " If the current line is a bracket continuation or a block-starter, but 192 " the previous is a non-bracket one, respect the previous' indentation, 193 " and stop here. 194 " 195 " Example: 196 " method_call one, 197 " two { 198 " three 199 " 200 return lnum 201 elseif s:Match(lnum, s:bracket_continuation_regex) && 202 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex)) 203 " If both lines are bracket continuations (the current may also be a 204 " block-starter), use the current one's and stop here 205 " 206 " Example: 207 " method_call( 208 " other_method_call( 209 " foo 210 return msl 211 elseif s:Match(lnum, s:block_regex) && 212 \ !s:Match(msl, s:continuation_regex) && 213 \ !s:Match(msl, s:block_continuation_regex) 214 " If the previous line is a block-starter and the current one is 215 " mostly ordinary, use the current one as the MSL. 216 " 217 " Example: 218 " method_call do 219 " something 220 " something_else 221 return msl 222 else 223 let col = match(line, s:continuation_regex) + 1 224 if (col > 0 && !s:IsInStringOrComment(lnum, col)) 225 \ || s:IsInString(lnum, strlen(line)) 226 let msl = lnum 227 else 228 break 229 endif 230 endif 231 232 let msl_body = getline(msl) 233 let lnum = s:PrevNonBlankNonString(lnum - 1) 234 endwhile 235 return msl 236endfunction 237 238" Check if line 'lnum' has more opening brackets than closing ones. 239function s:ExtraBrackets(lnum) 240 let opening = {'parentheses': [], 'braces': [], 'brackets': []} 241 let closing = {'parentheses': [], 'braces': [], 'brackets': []} 242 243 let line = getline(a:lnum) 244 let pos = match(line, '[][(){}]', 0) 245 246 " Save any encountered opening brackets, and remove them once a matching 247 " closing one has been found. If a closing bracket shows up that doesn't 248 " close anything, save it for later. 249 while pos != -1 250 if !s:IsInStringOrComment(a:lnum, pos + 1) 251 if line[pos] == '(' 252 call add(opening.parentheses, {'type': '(', 'pos': pos}) 253 elseif line[pos] == ')' 254 if empty(opening.parentheses) 255 call add(closing.parentheses, {'type': ')', 'pos': pos}) 256 else 257 let opening.parentheses = opening.parentheses[0:-2] 258 endif 259 elseif line[pos] == '{' 260 call add(opening.braces, {'type': '{', 'pos': pos}) 261 elseif line[pos] == '}' 262 if empty(opening.braces) 263 call add(closing.braces, {'type': '}', 'pos': pos}) 264 else 265 let opening.braces = opening.braces[0:-2] 266 endif 267 elseif line[pos] == '[' 268 call add(opening.brackets, {'type': '[', 'pos': pos}) 269 elseif line[pos] == ']' 270 if empty(opening.brackets) 271 call add(closing.brackets, {'type': ']', 'pos': pos}) 272 else 273 let opening.brackets = opening.brackets[0:-2] 274 endif 275 endif 276 endif 277 278 let pos = match(line, '[][(){}]', pos + 1) 279 endwhile 280 281 " Find the rightmost brackets, since they're the ones that are important in 282 " both opening and closing cases 283 let rightmost_opening = {'type': '(', 'pos': -1} 284 let rightmost_closing = {'type': ')', 'pos': -1} 285 286 for opening in opening.parentheses + opening.braces + opening.brackets 287 if opening.pos > rightmost_opening.pos 288 let rightmost_opening = opening 289 endif 290 endfor 291 292 for closing in closing.parentheses + closing.braces + closing.brackets 293 if closing.pos > rightmost_closing.pos 294 let rightmost_closing = closing 295 endif 296 endfor 297 298 return [rightmost_opening, rightmost_closing] 299endfunction 300 301function s:Match(lnum, regex) 302 let col = match(getline(a:lnum), '\C'.a:regex) + 1 303 return col > 0 && !s:IsInStringOrComment(a:lnum, col) ? col : 0 304endfunction 305 306function s:MatchLast(lnum, regex) 307 let line = getline(a:lnum) 308 let col = match(line, '.*\zs' . a:regex) 309 while col != -1 && s:IsInStringOrComment(a:lnum, col) 310 let line = strpart(line, 0, col) 311 let col = match(line, '.*' . a:regex) 312 endwhile 313 return col + 1 314endfunction 315 316" 3. GetRubyIndent Function {{{1 317" ========================= 318 319function GetRubyIndent(...) 320 " 3.1. Setup {{{2 321 " ---------- 322 323 " For the current line, use the first argument if given, else v:lnum 324 let clnum = a:0 ? a:1 : v:lnum 325 326 " Set up variables for restoring position in file. Could use clnum here. 327 let vcol = col('.') 328 329 " 3.2. Work on the current line {{{2 330 " ----------------------------- 331 332 " Get the current line. 333 let line = getline(clnum) 334 let ind = -1 335 336 " If we got a closing bracket on an empty line, find its match and indent 337 " according to it. For parentheses we indent to its column - 1, for the 338 " others we indent to the containing line's MSL's level. Return -1 if fail. 339 let col = matchend(line, '^\s*[]})]') 340 if col > 0 && !s:IsInStringOrComment(clnum, col) 341 call cursor(clnum, col) 342 let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2) 343 if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0 344 if line[col-1]==')' && col('.') != col('$') - 1 345 let ind = virtcol('.') - 1 346 else 347 let ind = indent(s:GetMSL(line('.'))) 348 endif 349 endif 350 return ind 351 endif 352 353 " If we have a =begin or =end set indent to first column. 354 if match(line, '^\s*\%(=begin\|=end\)$') != -1 355 return 0 356 endif 357 358 " If we have a deindenting keyword, find its match and indent to its level. 359 " TODO: this is messy 360 if s:Match(clnum, s:ruby_deindent_keywords) 361 call cursor(clnum, 1) 362 if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW', 363 \ s:end_skip_expr) > 0 364 let msl = s:GetMSL(line('.')) 365 let line = getline(line('.')) 366 367 if strpart(line, 0, col('.') - 1) =~ '=\s*$' && 368 \ strpart(line, col('.') - 1, 2) !~ 'do' 369 let ind = virtcol('.') - 1 370 elseif getline(msl) =~ '=\s*\(#.*\)\=$' 371 let ind = indent(line('.')) 372 else 373 let ind = indent(msl) 374 endif 375 endif 376 return ind 377 endif 378 379 " If we are in a multi-line string or line-comment, don't do anything to it. 380 if s:IsInStringOrDocumentation(clnum, matchend(line, '^\s*') + 1) 381 return indent('.') 382 endif 383 384 " If we are at the closing delimiter of a "<<" heredoc-style string, set the 385 " indent to 0. 386 if line =~ '^\k\+\s*$' 387 \ && s:IsInStringDelimiter(clnum, 1) 388 \ && search('\V<<'.line, 'nbW') > 0 389 return 0 390 endif 391 392 " 3.3. Work on the previous line. {{{2 393 " ------------------------------- 394 395 " Find a non-blank, non-multi-line string line above the current line. 396 let lnum = s:PrevNonBlankNonString(clnum - 1) 397 398 " If the line is empty and inside a string, use the previous line. 399 if line =~ '^\s*$' && lnum != prevnonblank(clnum - 1) 400 return indent(prevnonblank(clnum)) 401 endif 402 403 " At the start of the file use zero indent. 404 if lnum == 0 405 return 0 406 endif 407 408 " Set up variables for the previous line. 409 let line = getline(lnum) 410 let ind = indent(lnum) 411 412 " If the previous line ended with a block opening, add a level of indent. 413 if s:Match(lnum, s:block_regex) 414 return indent(s:GetMSL(lnum)) + &sw 415 endif 416 417 " If the previous line ended with the "*" of a splat, add a level of indent 418 if line =~ s:splat_regex 419 return indent(lnum) + &sw 420 endif 421 422 " If the previous line contained unclosed opening brackets and we are still 423 " in them, find the rightmost one and add indent depending on the bracket 424 " type. 425 " 426 " If it contained hanging closing brackets, find the rightmost one, find its 427 " match and indent according to that. 428 if line =~ '[[({]' || line =~ '[])}]\s*\%(#.*\)\=$' 429 let [opening, closing] = s:ExtraBrackets(lnum) 430 431 if opening.pos != -1 432 if opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0 433 if col('.') + 1 == col('$') 434 return ind + &sw 435 else 436 return virtcol('.') 437 endif 438 else 439 let nonspace = matchend(line, '\S', opening.pos + 1) - 1 440 return nonspace > 0 ? nonspace : ind + &sw 441 endif 442 elseif closing.pos != -1 443 call cursor(lnum, closing.pos + 1) 444 normal! % 445 446 if s:Match(line('.'), s:ruby_indent_keywords) 447 return indent('.') + &sw 448 else 449 return indent('.') 450 endif 451 else 452 call cursor(clnum, vcol) 453 end 454 endif 455 456 " If the previous line ended with an "end", match that "end"s beginning's 457 " indent. 458 let col = s:Match(lnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$') 459 if col > 0 460 call cursor(lnum, col) 461 if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW', 462 \ s:end_skip_expr) > 0 463 let n = line('.') 464 let ind = indent('.') 465 let msl = s:GetMSL(n) 466 if msl != n 467 let ind = indent(msl) 468 end 469 return ind 470 endif 471 end 472 473 let col = s:Match(lnum, s:ruby_indent_keywords) 474 if col > 0 475 call cursor(lnum, col) 476 let ind = virtcol('.') - 1 + &sw 477 " TODO: make this better (we need to count them) (or, if a searchpair 478 " fails, we know that something is lacking an end and thus we indent a 479 " level 480 if s:Match(lnum, s:end_end_regex) 481 let ind = indent('.') 482 endif 483 return ind 484 endif 485 486 " 3.4. Work on the MSL line. {{{2 487 " -------------------------- 488 489 " Set up variables to use and search for MSL to the previous line. 490 let p_lnum = lnum 491 let lnum = s:GetMSL(lnum) 492 493 " If the previous line wasn't a MSL and is continuation return its indent. 494 " TODO: the || s:IsInString() thing worries me a bit. 495 if p_lnum != lnum 496 if s:Match(p_lnum, s:non_bracket_continuation_regex) || s:IsInString(p_lnum,strlen(line)) 497 return ind 498 endif 499 endif 500 501 " Set up more variables, now that we know we wasn't continuation bound. 502 let line = getline(lnum) 503 let msl_ind = indent(lnum) 504 505 " If the MSL line had an indenting keyword in it, add a level of indent. 506 " TODO: this does not take into account contrived things such as 507 " module Foo; class Bar; end 508 if s:Match(lnum, s:ruby_indent_keywords) 509 let ind = msl_ind + &sw 510 if s:Match(lnum, s:end_end_regex) 511 let ind = ind - &sw 512 endif 513 return ind 514 endif 515 516 " If the previous line ended with [*+/.,-=], but wasn't a block ending or a 517 " closing bracket, indent one extra level. 518 if s:Match(lnum, s:non_bracket_continuation_regex) && !s:Match(lnum, '^\s*\([\])}]\|end\)') 519 if lnum == p_lnum 520 let ind = msl_ind + &sw 521 else 522 let ind = msl_ind 523 endif 524 return ind 525 endif 526 527 " }}}2 528 529 return ind 530endfunction 531 532" }}}1 533 534let &cpo = s:cpo_save 535unlet s:cpo_save 536 537" vim:set sw=2 sts=2 ts=8 et: 538