ruby.vim (31174B)
1 " Vim indent file 2 " Language: Ruby 3 " Maintainer: Andrew Radev <andrey.radev@gmail.com> 4 " Previous Maintainer: Nikolai Weibull <now at bitwi.se> 5 " URL: https://github.com/vim-ruby/vim-ruby 6 " Last Change: 2023 Dec 22 7 8 " 0. Initialization {{{1 9 " ================= 10 11 " Only load this indent file when no other was loaded. 12 if exists("b:did_indent") 13 finish 14 endif 15 let b:did_indent = 1 16 17 if !exists('g:ruby_indent_access_modifier_style') 18 " Possible values: "normal", "indent", "outdent" 19 let g:ruby_indent_access_modifier_style = 'normal' 20 endif 21 22 if !exists('g:ruby_indent_assignment_style') 23 " Possible values: "variable", "hanging" 24 let g:ruby_indent_assignment_style = 'hanging' 25 endif 26 27 if !exists('g:ruby_indent_block_style') 28 " Possible values: "expression", "do" 29 let g:ruby_indent_block_style = 'do' 30 endif 31 32 if !exists('g:ruby_indent_hanging_elements') 33 " Non-zero means hanging indents are enabled, zero means disabled 34 let g:ruby_indent_hanging_elements = 1 35 endif 36 37 setlocal nosmartindent 38 39 " Now, set up our indentation expression and keys that trigger it. 40 setlocal indentexpr=GetRubyIndent(v:lnum) 41 setlocal indentkeys=0{,0},0),0],!^F,o,O,e,:,. 42 setlocal indentkeys+==end,=else,=elsif,=when,=in\ ,=ensure,=rescue,==begin,==end 43 setlocal indentkeys+==private,=protected,=public 44 45 let b:undo_indent = "setlocal indentexpr< indentkeys< smartindent<" 46 47 " Only define the function once. 48 if exists("*GetRubyIndent") 49 finish 50 endif 51 52 let s:cpo_save = &cpo 53 set cpo&vim 54 55 " 1. Variables {{{1 56 " ============ 57 58 " Syntax group names that are strings. 59 let s:syng_string = 60 \ ['String', 'Interpolation', 'InterpolationDelimiter', 'StringEscape'] 61 62 " Syntax group names that are strings or documentation. 63 let s:syng_stringdoc = s:syng_string + ['Documentation'] 64 65 " Syntax group names that are or delimit strings/symbols/regexes or are comments. 66 let s:syng_strcom = s:syng_stringdoc + [ 67 \ 'Character', 68 \ 'Comment', 69 \ 'HeredocDelimiter', 70 \ 'PercentRegexpDelimiter', 71 \ 'PercentStringDelimiter', 72 \ 'PercentSymbolDelimiter', 73 \ 'Regexp', 74 \ 'RegexpCharClass', 75 \ 'RegexpDelimiter', 76 \ 'RegexpEscape', 77 \ 'StringDelimiter', 78 \ 'Symbol', 79 \ 'SymbolDelimiter', 80 \ ] 81 82 " Expression used to check whether we should skip a match with searchpair(). 83 let s:skip_expr = 84 \ 'index(map('.string(s:syng_strcom).',"hlID(''ruby''.v:val)"), synID(line("."),col("."),1)) >= 0' 85 86 " Regex used for words that, at the start of a line, add a level of indent. 87 let s:ruby_indent_keywords = 88 \ '^\s*\zs\<\%(module\|class\|if\|for' . 89 \ '\|while\|until\|else\|elsif\|case\|when\|in\|unless\|begin\|ensure\|rescue' . 90 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' . 91 \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' . 92 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>' 93 94 " Def without an end clause: def method_call(...) = <expression> 95 let s:ruby_endless_def = 96 \ '\<def\s\+\%(\k\+\.\)\=\%(\k\+[=!?]\=\|' . 97 \ '[-+*/%&^<>~!]\|' . 98 \ '\*\*\|>>\|<<\|' . 99 \ '===\?\|\!=\|=\~\|\!\~\|' . 100 \ '<=>\|<=\|>=\|' . 101 \ '[-+!\~]@\|\[\]' . 102 \ '\)\%((.*)\|\s\)\s*=' 103 104 " Regex used for words that, at the start of a line, remove a level of indent. 105 let s:ruby_deindent_keywords = 106 \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|in\|end\):\@!\>' 107 108 " Regex that defines the start-match for the 'end' keyword. 109 "let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>' 110 " TODO: the do here should be restricted somewhat (only at end of line)? 111 let s:end_start_regex = 112 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' . 113 \ '\<\%(module\|class\|if\|for\|while\|until\|case\|unless\|begin' . 114 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' . 115 \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>' 116 117 " Regex that defines the middle-match for the 'end' keyword. 118 let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|\%(\%(^\|;\)\s*\)\@<=\<in\|elsif\):\@!\>' 119 120 " Regex that defines the end-match for the 'end' keyword. 121 let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>' 122 123 " Expression used for searchpair() call for finding a match for an 'end' keyword. 124 function! s:EndSkipExpr() 125 if eval(s:skip_expr) 126 return 1 127 elseif expand('<cword>') == 'do' 128 \ && getline(".") =~ '^\s*\<\(while\|until\|for\):\@!\>' 129 return 1 130 elseif getline('.') =~ s:ruby_endless_def 131 return 1 132 elseif getline('.') =~ '\<def\s\+\k\+[!?]\=([^)]*$' 133 " Then it's a `def method(` with a possible `) =` later 134 call search('\<def\s\+\k\+\zs(', 'W', line('.')) 135 normal! % 136 return getline('.') =~ ')\s*=' 137 else 138 return 0 139 endif 140 endfunction 141 142 let s:end_skip_expr = function('s:EndSkipExpr') 143 144 " Regex that defines continuation lines, not including (, {, or [. 145 let s:non_bracket_continuation_regex = 146 \ '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$' 147 148 " Regex that defines continuation lines. 149 let s:continuation_regex = 150 \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$' 151 152 " Regex that defines continuable keywords 153 let s:continuable_regex = 154 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' . 155 \ '\<\%(if\|for\|while\|until\|unless\):\@!\>' 156 157 " Regex that defines bracket continuations 158 let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$' 159 160 " Regex that defines dot continuations 161 let s:dot_continuation_regex = '%\@<!\.\s*\%(#.*\)\=$' 162 163 " Regex that defines backslash continuations 164 let s:backslash_continuation_regex = '%\@<!\\\s*$' 165 166 " Regex that defines end of bracket continuation followed by another continuation 167 let s:bracket_switch_continuation_regex = '^\([^(]\+\zs).\+\)\+'.s:continuation_regex 168 169 " Regex that defines the first part of a splat pattern 170 let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$' 171 172 " Regex that describes all indent access modifiers 173 let s:access_modifier_regex = '\C^\s*\%(public\|protected\|private\)\s*\%(#.*\)\=$' 174 175 " Regex that describes the indent access modifiers (excludes public) 176 let s:indent_access_modifier_regex = '\C^\s*\%(protected\|private\)\s*\%(#.*\)\=$' 177 178 " Regex that defines blocks. 179 " 180 " Note that there's a slight problem with this regex and s:continuation_regex. 181 " Code like this will be matched by both: 182 " 183 " method_call do |(a, b)| 184 " 185 " The reason is that the pipe matches a hanging "|" operator. 186 " 187 let s:block_regex = 188 \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|[^|]*|\)\=\s*\%(#.*\)\=$' 189 190 let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex 191 192 " Regex that describes a leading operator (only a method call's dot for now) 193 let s:leading_operator_regex = '^\s*\%(&\=\.\)' 194 195 " 2. GetRubyIndent Function {{{1 196 " ========================= 197 198 function! GetRubyIndent(...) abort 199 " 2.1. Setup {{{2 200 " ---------- 201 202 let indent_info = {} 203 204 " The value of a single shift-width 205 if exists('*shiftwidth') 206 let indent_info.sw = shiftwidth() 207 else 208 let indent_info.sw = &sw 209 endif 210 211 " For the current line, use the first argument if given, else v:lnum 212 let indent_info.clnum = a:0 ? a:1 : v:lnum 213 let indent_info.cline = getline(indent_info.clnum) 214 215 " Set up variables for restoring position in file. Could use clnum here. 216 let indent_info.col = col('.') 217 218 " 2.2. Work on the current line {{{2 219 " ----------------------------- 220 let indent_callback_names = [ 221 \ 's:AccessModifier', 222 \ 's:ClosingBracketOnEmptyLine', 223 \ 's:BlockComment', 224 \ 's:DeindentingKeyword', 225 \ 's:MultilineStringOrLineComment', 226 \ 's:ClosingHeredocDelimiter', 227 \ 's:LeadingOperator', 228 \ ] 229 230 for callback_name in indent_callback_names 231 " Decho "Running: ".callback_name 232 let indent = call(function(callback_name), [indent_info]) 233 234 if indent >= 0 235 " Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info) 236 return indent 237 endif 238 endfor 239 240 " 2.3. Work on the previous line. {{{2 241 " ------------------------------- 242 243 " Special case: we don't need the real s:PrevNonBlankNonString for an empty 244 " line inside a string. And that call can be quite expensive in that 245 " particular situation. 246 let indent_callback_names = [ 247 \ 's:EmptyInsideString', 248 \ ] 249 250 for callback_name in indent_callback_names 251 " Decho "Running: ".callback_name 252 let indent = call(function(callback_name), [indent_info]) 253 254 if indent >= 0 255 " Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info) 256 return indent 257 endif 258 endfor 259 260 " Previous line number 261 let indent_info.plnum = s:PrevNonBlankNonString(indent_info.clnum - 1) 262 let indent_info.pline = getline(indent_info.plnum) 263 264 let indent_callback_names = [ 265 \ 's:StartOfFile', 266 \ 's:AfterAccessModifier', 267 \ 's:ContinuedLine', 268 \ 's:AfterBlockOpening', 269 \ 's:AfterHangingSplat', 270 \ 's:AfterUnbalancedBracket', 271 \ 's:AfterLeadingOperator', 272 \ 's:AfterEndKeyword', 273 \ 's:AfterIndentKeyword', 274 \ ] 275 276 for callback_name in indent_callback_names 277 " Decho "Running: ".callback_name 278 let indent = call(function(callback_name), [indent_info]) 279 280 if indent >= 0 281 " Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info) 282 return indent 283 endif 284 endfor 285 286 " 2.4. Work on the MSL line. {{{2 287 " -------------------------- 288 let indent_callback_names = [ 289 \ 's:PreviousNotMSL', 290 \ 's:IndentingKeywordInMSL', 291 \ 's:ContinuedHangingOperator', 292 \ ] 293 294 " Most Significant line based on the previous one -- in case it's a 295 " continuation of something above 296 let indent_info.plnum_msl = s:GetMSL(indent_info.plnum) 297 298 for callback_name in indent_callback_names 299 " Decho "Running: ".callback_name 300 let indent = call(function(callback_name), [indent_info]) 301 302 if indent >= 0 303 " Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info) 304 return indent 305 endif 306 endfor 307 308 " }}}2 309 310 " By default, just return the previous line's indent 311 " Decho "Default case matched" 312 return indent(indent_info.plnum) 313 endfunction 314 315 " 3. Indenting Logic Callbacks {{{1 316 " ============================ 317 318 function! s:AccessModifier(cline_info) abort 319 let info = a:cline_info 320 321 " If this line is an access modifier keyword, align according to the closest 322 " class declaration. 323 if g:ruby_indent_access_modifier_style == 'indent' 324 if s:Match(info.clnum, s:access_modifier_regex) 325 let class_lnum = s:FindContainingClass() 326 if class_lnum > 0 327 return indent(class_lnum) + info.sw 328 endif 329 endif 330 elseif g:ruby_indent_access_modifier_style == 'outdent' 331 if s:Match(info.clnum, s:access_modifier_regex) 332 let class_lnum = s:FindContainingClass() 333 if class_lnum > 0 334 return indent(class_lnum) 335 endif 336 endif 337 endif 338 339 return -1 340 endfunction 341 342 function! s:ClosingBracketOnEmptyLine(cline_info) abort 343 let info = a:cline_info 344 345 " If we got a closing bracket on an empty line, find its match and indent 346 " according to it. For parentheses we indent to its column - 1, for the 347 " others we indent to the containing line's MSL's level. Return -1 if fail. 348 let col = matchend(info.cline, '^\s*[]})]') 349 350 if col > 0 && !s:IsInStringOrComment(info.clnum, col) 351 call cursor(info.clnum, col) 352 let closing_bracket = info.cline[col - 1] 353 let bracket_pair = strpart('(){}[]', stridx(')}]', closing_bracket) * 2, 2) 354 355 if searchpair(escape(bracket_pair[0], '\['), '', bracket_pair[1], 'bW', s:skip_expr) > 0 356 if closing_bracket == ')' && col('.') != col('$') - 1 357 if g:ruby_indent_hanging_elements 358 let ind = virtcol('.') - 1 359 else 360 let ind = indent(line('.')) 361 end 362 elseif g:ruby_indent_block_style == 'do' 363 let ind = indent(line('.')) 364 else " g:ruby_indent_block_style == 'expression' 365 let ind = indent(s:GetMSL(line('.'))) 366 endif 367 endif 368 369 return ind 370 endif 371 372 return -1 373 endfunction 374 375 function! s:BlockComment(cline_info) abort 376 " If we have a =begin or =end set indent to first column. 377 if match(a:cline_info.cline, '^\s*\%(=begin\|=end\)$') != -1 378 return 0 379 endif 380 return -1 381 endfunction 382 383 function! s:DeindentingKeyword(cline_info) abort 384 let info = a:cline_info 385 386 " If we have a deindenting keyword, find its match and indent to its level. 387 " TODO: this is messy 388 if s:Match(info.clnum, s:ruby_deindent_keywords) 389 call cursor(info.clnum, 1) 390 391 if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW', 392 \ s:end_skip_expr) > 0 393 let msl = s:GetMSL(line('.')) 394 let line = getline(line('.')) 395 396 if s:IsAssignment(line, col('.')) && 397 \ strpart(line, col('.') - 1, 2) !~ 'do' 398 " assignment to case/begin/etc, on the same line 399 if g:ruby_indent_assignment_style == 'hanging' 400 " hanging indent 401 let ind = virtcol('.') - 1 402 else 403 " align with variable 404 let ind = indent(line('.')) 405 endif 406 elseif g:ruby_indent_block_style == 'do' 407 " align to line of the "do", not to the MSL 408 let ind = indent(line('.')) 409 elseif getline(msl) =~ '=\s*\(#.*\)\=$' 410 " in the case of assignment to the MSL, align to the starting line, 411 " not to the MSL 412 let ind = indent(line('.')) 413 else 414 " align to the MSL 415 let ind = indent(msl) 416 endif 417 endif 418 return ind 419 endif 420 421 return -1 422 endfunction 423 424 function! s:MultilineStringOrLineComment(cline_info) abort 425 let info = a:cline_info 426 427 " If we are in a multi-line string or line-comment, don't do anything to it. 428 if s:IsInStringOrDocumentation(info.clnum, matchend(info.cline, '^\s*') + 1) 429 return indent(info.clnum) 430 endif 431 return -1 432 endfunction 433 434 function! s:ClosingHeredocDelimiter(cline_info) abort 435 let info = a:cline_info 436 437 " If we are at the closing delimiter of a "<<" heredoc-style string, set the 438 " indent to 0. 439 if info.cline =~ '^\k\+\s*$' 440 \ && s:IsInStringDelimiter(info.clnum, 1) 441 \ && search('\V<<'.info.cline, 'nbW') > 0 442 return 0 443 endif 444 445 return -1 446 endfunction 447 448 function! s:LeadingOperator(cline_info) abort 449 " If the current line starts with a leading operator, add a level of indent. 450 if s:Match(a:cline_info.clnum, s:leading_operator_regex) 451 return indent(s:GetMSL(a:cline_info.clnum)) + a:cline_info.sw 452 endif 453 return -1 454 endfunction 455 456 function! s:EmptyInsideString(pline_info) abort 457 " If the line is empty and inside a string (the previous line is a string, 458 " too), use the previous line's indent 459 let info = a:pline_info 460 461 let plnum = prevnonblank(info.clnum - 1) 462 let pline = getline(plnum) 463 464 if info.cline =~ '^\s*$' 465 \ && s:IsInStringOrComment(plnum, 1) 466 \ && s:IsInStringOrComment(plnum, strlen(pline)) 467 return indent(plnum) 468 endif 469 return -1 470 endfunction 471 472 function! s:StartOfFile(pline_info) abort 473 " At the start of the file use zero indent. 474 if a:pline_info.plnum == 0 475 return 0 476 endif 477 return -1 478 endfunction 479 480 function! s:AfterAccessModifier(pline_info) abort 481 let info = a:pline_info 482 483 if g:ruby_indent_access_modifier_style == 'indent' 484 " If the previous line was a private/protected keyword, add a 485 " level of indent. 486 if s:Match(info.plnum, s:indent_access_modifier_regex) 487 return indent(info.plnum) + info.sw 488 endif 489 elseif g:ruby_indent_access_modifier_style == 'outdent' 490 " If the previous line was a private/protected/public keyword, add 491 " a level of indent, since the keyword has been out-dented. 492 if s:Match(info.plnum, s:access_modifier_regex) 493 return indent(info.plnum) + info.sw 494 endif 495 endif 496 return -1 497 endfunction 498 499 " Example: 500 " 501 " if foo || bar || 502 " baz || bing 503 " puts "foo" 504 " end 505 " 506 function! s:ContinuedLine(pline_info) abort 507 let info = a:pline_info 508 509 let col = s:Match(info.plnum, s:ruby_indent_keywords) 510 if s:Match(info.plnum, s:continuable_regex) && 511 \ s:Match(info.plnum, s:continuation_regex) 512 if col > 0 && s:IsAssignment(info.pline, col) 513 if g:ruby_indent_assignment_style == 'hanging' 514 " hanging indent 515 let ind = col - 1 516 else 517 " align with variable 518 let ind = indent(info.plnum) 519 endif 520 else 521 let ind = indent(s:GetMSL(info.plnum)) 522 endif 523 return ind + info.sw + info.sw 524 endif 525 return -1 526 endfunction 527 528 function! s:AfterBlockOpening(pline_info) abort 529 let info = a:pline_info 530 531 " If the previous line ended with a block opening, add a level of indent. 532 if s:Match(info.plnum, s:block_regex) 533 if g:ruby_indent_block_style == 'do' 534 " don't align to the msl, align to the "do" 535 let ind = indent(info.plnum) + info.sw 536 else 537 let plnum_msl = s:GetMSL(info.plnum) 538 539 if getline(plnum_msl) =~ '=\s*\(#.*\)\=$' 540 " in the case of assignment to the msl, align to the starting line, 541 " not to the msl 542 let ind = indent(info.plnum) + info.sw 543 else 544 let ind = indent(plnum_msl) + info.sw 545 endif 546 endif 547 548 return ind 549 endif 550 551 return -1 552 endfunction 553 554 function! s:AfterLeadingOperator(pline_info) abort 555 " If the previous line started with a leading operator, use its MSL's level 556 " of indent 557 if s:Match(a:pline_info.plnum, s:leading_operator_regex) 558 return indent(s:GetMSL(a:pline_info.plnum)) 559 endif 560 return -1 561 endfunction 562 563 function! s:AfterHangingSplat(pline_info) abort 564 let info = a:pline_info 565 566 " If the previous line ended with the "*" of a splat, add a level of indent 567 if info.pline =~ s:splat_regex 568 return indent(info.plnum) + info.sw 569 endif 570 return -1 571 endfunction 572 573 function! s:AfterUnbalancedBracket(pline_info) abort 574 let info = a:pline_info 575 576 " If the previous line contained unclosed opening brackets and we are still 577 " in them, find the rightmost one and add indent depending on the bracket 578 " type. 579 " 580 " If it contained hanging closing brackets, find the rightmost one, find its 581 " match and indent according to that. 582 if info.pline =~ '[[({]' || info.pline =~ '[])}]\s*\%(#.*\)\=$' 583 let [opening, closing] = s:ExtraBrackets(info.plnum) 584 585 if opening.pos != -1 586 if !g:ruby_indent_hanging_elements 587 return indent(info.plnum) + info.sw 588 elseif opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0 589 if col('.') + 1 == col('$') 590 return indent(info.plnum) + info.sw 591 else 592 return virtcol('.') 593 endif 594 else 595 let nonspace = matchend(info.pline, '\S', opening.pos + 1) - 1 596 return nonspace > 0 ? nonspace : indent(info.plnum) + info.sw 597 endif 598 elseif closing.pos != -1 599 call cursor(info.plnum, closing.pos + 1) 600 normal! % 601 602 if strpart(info.pline, closing.pos) =~ '^)\s*=' 603 " special case: the closing `) =` of an endless def 604 return indent(s:GetMSL(line('.'))) 605 endif 606 607 if s:Match(line('.'), s:ruby_indent_keywords) 608 return indent('.') + info.sw 609 else 610 return indent(s:GetMSL(line('.'))) 611 endif 612 else 613 call cursor(info.clnum, info.col) 614 end 615 endif 616 617 return -1 618 endfunction 619 620 function! s:AfterEndKeyword(pline_info) abort 621 let info = a:pline_info 622 " If the previous line ended with an "end", match that "end"s beginning's 623 " indent. 624 let col = s:Match(info.plnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$') 625 if col > 0 626 call cursor(info.plnum, col) 627 if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW', 628 \ s:end_skip_expr) > 0 629 let n = line('.') 630 let ind = indent('.') 631 let msl = s:GetMSL(n) 632 if msl != n 633 let ind = indent(msl) 634 end 635 return ind 636 endif 637 end 638 return -1 639 endfunction 640 641 function! s:AfterIndentKeyword(pline_info) abort 642 let info = a:pline_info 643 let col = s:Match(info.plnum, s:ruby_indent_keywords) 644 645 if col > 0 && s:Match(info.plnum, s:ruby_endless_def) <= 0 646 call cursor(info.plnum, col) 647 let ind = virtcol('.') - 1 + info.sw 648 " TODO: make this better (we need to count them) (or, if a searchpair 649 " fails, we know that something is lacking an end and thus we indent a 650 " level 651 if s:Match(info.plnum, s:end_end_regex) 652 let ind = indent('.') 653 elseif s:IsAssignment(info.pline, col) 654 if g:ruby_indent_assignment_style == 'hanging' 655 " hanging indent 656 let ind = col + info.sw - 1 657 else 658 " align with variable 659 let ind = indent(info.plnum) + info.sw 660 endif 661 endif 662 return ind 663 endif 664 665 return -1 666 endfunction 667 668 function! s:PreviousNotMSL(msl_info) abort 669 let info = a:msl_info 670 671 " If the previous line wasn't a MSL 672 if info.plnum != info.plnum_msl 673 " If previous line ends bracket and begins non-bracket continuation decrease indent by 1. 674 if s:Match(info.plnum, s:bracket_switch_continuation_regex) 675 " TODO (2016-10-07) Wrong/unused? How could it be "1"? 676 return indent(info.plnum) - 1 677 " If previous line is a continuation return its indent. 678 elseif s:Match(info.plnum, s:non_bracket_continuation_regex) 679 return indent(info.plnum) 680 endif 681 endif 682 683 return -1 684 endfunction 685 686 function! s:IndentingKeywordInMSL(msl_info) abort 687 let info = a:msl_info 688 " If the MSL line had an indenting keyword in it, add a level of indent. 689 " TODO: this does not take into account contrived things such as 690 " module Foo; class Bar; end 691 let col = s:Match(info.plnum_msl, s:ruby_indent_keywords) 692 if col > 0 && s:Match(info.plnum_msl, s:ruby_endless_def) <= 0 693 let ind = indent(info.plnum_msl) + info.sw 694 if s:Match(info.plnum_msl, s:end_end_regex) 695 let ind = ind - info.sw 696 elseif s:IsAssignment(getline(info.plnum_msl), col) 697 if g:ruby_indent_assignment_style == 'hanging' 698 " hanging indent 699 let ind = col + info.sw - 1 700 else 701 " align with variable 702 let ind = indent(info.plnum_msl) + info.sw 703 endif 704 endif 705 return ind 706 endif 707 return -1 708 endfunction 709 710 function! s:ContinuedHangingOperator(msl_info) abort 711 let info = a:msl_info 712 713 " If the previous line ended with [*+/.,-=], but wasn't a block ending or a 714 " closing bracket, indent one extra level. 715 if s:Match(info.plnum_msl, s:non_bracket_continuation_regex) && !s:Match(info.plnum_msl, '^\s*\([\])}]\|end\)') 716 if info.plnum_msl == info.plnum 717 let ind = indent(info.plnum_msl) + info.sw 718 else 719 let ind = indent(info.plnum_msl) 720 endif 721 return ind 722 endif 723 724 return -1 725 endfunction 726 727 " 4. Auxiliary Functions {{{1 728 " ====================== 729 730 function! s:IsInRubyGroup(groups, lnum, col) abort 731 let ids = map(copy(a:groups), 'hlID("ruby".v:val)') 732 return index(ids, synID(a:lnum, a:col, 1)) >= 0 733 endfunction 734 735 " Check if the character at lnum:col is inside a string, comment, or is ascii. 736 function! s:IsInStringOrComment(lnum, col) abort 737 return s:IsInRubyGroup(s:syng_strcom, a:lnum, a:col) 738 endfunction 739 740 " Check if the character at lnum:col is inside a string. 741 function! s:IsInString(lnum, col) abort 742 return s:IsInRubyGroup(s:syng_string, a:lnum, a:col) 743 endfunction 744 745 " Check if the character at lnum:col is inside a string or documentation. 746 function! s:IsInStringOrDocumentation(lnum, col) abort 747 return s:IsInRubyGroup(s:syng_stringdoc, a:lnum, a:col) 748 endfunction 749 750 " Check if the character at lnum:col is inside a string delimiter 751 function! s:IsInStringDelimiter(lnum, col) abort 752 return s:IsInRubyGroup( 753 \ ['HeredocDelimiter', 'PercentStringDelimiter', 'StringDelimiter'], 754 \ a:lnum, a:col 755 \ ) 756 endfunction 757 758 function! s:IsAssignment(str, pos) abort 759 return strpart(a:str, 0, a:pos - 1) =~ '=\s*$' 760 endfunction 761 762 " Find line above 'lnum' that isn't empty, in a comment, or in a string. 763 function! s:PrevNonBlankNonString(lnum) abort 764 let in_block = 0 765 let lnum = prevnonblank(a:lnum) 766 while lnum > 0 767 " Go in and out of blocks comments as necessary. 768 " If the line isn't empty (with opt. comment) or in a string, end search. 769 let line = getline(lnum) 770 if line =~ '^=begin' 771 if in_block 772 let in_block = 0 773 else 774 break 775 endif 776 elseif !in_block && line =~ '^=end' 777 let in_block = 1 778 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1) 779 \ && s:IsInStringOrComment(lnum, strlen(line))) 780 break 781 endif 782 let lnum = prevnonblank(lnum - 1) 783 endwhile 784 return lnum 785 endfunction 786 787 " Find line above 'lnum' that started the continuation 'lnum' may be part of. 788 function! s:GetMSL(lnum) abort 789 " Start on the line we're at and use its indent. 790 let msl = a:lnum 791 let lnum = s:PrevNonBlankNonString(a:lnum - 1) 792 while lnum > 0 793 " If we have a continuation line, or we're in a string, use line as MSL. 794 " Otherwise, terminate search as we have found our MSL already. 795 let line = getline(lnum) 796 797 if !s:Match(msl, s:backslash_continuation_regex) && 798 \ s:Match(lnum, s:backslash_continuation_regex) 799 " If the current line doesn't end in a backslash, but the previous one 800 " does, look for that line's msl 801 " 802 " Example: 803 " foo = "bar" \ 804 " "baz" 805 " 806 let msl = lnum 807 elseif s:Match(msl, s:leading_operator_regex) 808 " If the current line starts with a leading operator, keep its indent 809 " and keep looking for an MSL. 810 let msl = lnum 811 elseif s:Match(lnum, s:splat_regex) 812 " If the above line looks like the "*" of a splat, use the current one's 813 " indentation. 814 " 815 " Example: 816 " Hash[* 817 " method_call do 818 " something 819 " 820 return msl 821 elseif s:Match(lnum, s:non_bracket_continuation_regex) && 822 \ s:Match(msl, s:non_bracket_continuation_regex) 823 " If the current line is a non-bracket continuation and so is the 824 " previous one, keep its indent and continue looking for an MSL. 825 " 826 " Example: 827 " method_call one, 828 " two, 829 " three 830 " 831 let msl = lnum 832 elseif s:Match(lnum, s:dot_continuation_regex) && 833 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex)) 834 " If the current line is a bracket continuation or a block-starter, but 835 " the previous is a dot, keep going to see if the previous line is the 836 " start of another continuation. 837 " 838 " Example: 839 " parent. 840 " method_call { 841 " three 842 " 843 let msl = lnum 844 elseif s:Match(lnum, s:non_bracket_continuation_regex) && 845 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex)) 846 " If the current line is a bracket continuation or a block-starter, but 847 " the previous is a non-bracket one, respect the previous' indentation, 848 " and stop here. 849 " 850 " Example: 851 " method_call one, 852 " two { 853 " three 854 " 855 return lnum 856 elseif s:Match(lnum, s:bracket_continuation_regex) && 857 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex)) 858 " If both lines are bracket continuations (the current may also be a 859 " block-starter), use the current one's and stop here 860 " 861 " Example: 862 " method_call( 863 " other_method_call( 864 " foo 865 return msl 866 elseif s:Match(lnum, s:block_regex) && 867 \ !s:Match(msl, s:continuation_regex) && 868 \ !s:Match(msl, s:block_continuation_regex) 869 " If the previous line is a block-starter and the current one is 870 " mostly ordinary, use the current one as the MSL. 871 " 872 " Example: 873 " method_call do 874 " something 875 " something_else 876 return msl 877 else 878 let col = match(line, s:continuation_regex) + 1 879 if (col > 0 && !s:IsInStringOrComment(lnum, col)) 880 \ || s:IsInString(lnum, strlen(line)) 881 let msl = lnum 882 else 883 break 884 endif 885 endif 886 887 let lnum = s:PrevNonBlankNonString(lnum - 1) 888 endwhile 889 return msl 890 endfunction 891 892 " Check if line 'lnum' has more opening brackets than closing ones. 893 function! s:ExtraBrackets(lnum) abort 894 let opening = {'parentheses': [], 'braces': [], 'brackets': []} 895 let closing = {'parentheses': [], 'braces': [], 'brackets': []} 896 897 let line = getline(a:lnum) 898 let pos = match(line, '[][(){}]', 0) 899 900 " Save any encountered opening brackets, and remove them once a matching 901 " closing one has been found. If a closing bracket shows up that doesn't 902 " close anything, save it for later. 903 while pos != -1 904 if !s:IsInStringOrComment(a:lnum, pos + 1) 905 if line[pos] == '(' 906 call add(opening.parentheses, {'type': '(', 'pos': pos}) 907 elseif line[pos] == ')' 908 if empty(opening.parentheses) 909 call add(closing.parentheses, {'type': ')', 'pos': pos}) 910 else 911 let opening.parentheses = opening.parentheses[0:-2] 912 endif 913 elseif line[pos] == '{' 914 call add(opening.braces, {'type': '{', 'pos': pos}) 915 elseif line[pos] == '}' 916 if empty(opening.braces) 917 call add(closing.braces, {'type': '}', 'pos': pos}) 918 else 919 let opening.braces = opening.braces[0:-2] 920 endif 921 elseif line[pos] == '[' 922 call add(opening.brackets, {'type': '[', 'pos': pos}) 923 elseif line[pos] == ']' 924 if empty(opening.brackets) 925 call add(closing.brackets, {'type': ']', 'pos': pos}) 926 else 927 let opening.brackets = opening.brackets[0:-2] 928 endif 929 endif 930 endif 931 932 let pos = match(line, '[][(){}]', pos + 1) 933 endwhile 934 935 " Find the rightmost brackets, since they're the ones that are important in 936 " both opening and closing cases 937 let rightmost_opening = {'type': '(', 'pos': -1} 938 let rightmost_closing = {'type': ')', 'pos': -1} 939 940 for opening in opening.parentheses + opening.braces + opening.brackets 941 if opening.pos > rightmost_opening.pos 942 let rightmost_opening = opening 943 endif 944 endfor 945 946 for closing in closing.parentheses + closing.braces + closing.brackets 947 if closing.pos > rightmost_closing.pos 948 let rightmost_closing = closing 949 endif 950 endfor 951 952 return [rightmost_opening, rightmost_closing] 953 endfunction 954 955 function! s:Match(lnum, regex) abort 956 let line = getline(a:lnum) 957 let offset = match(line, '\C'.a:regex) 958 let col = offset + 1 959 960 while offset > -1 && s:IsInStringOrComment(a:lnum, col) 961 let offset = match(line, '\C'.a:regex, offset + 1) 962 let col = offset + 1 963 endwhile 964 965 if offset > -1 966 return col 967 else 968 return 0 969 endif 970 endfunction 971 972 " Locates the containing class/module's definition line, ignoring nested classes 973 " along the way. 974 " 975 function! s:FindContainingClass() abort 976 let saved_position = getpos('.') 977 978 while searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW', 979 \ s:end_skip_expr) > 0 980 if expand('<cword>') =~# '\<class\|module\>' 981 let found_lnum = line('.') 982 call setpos('.', saved_position) 983 return found_lnum 984 endif 985 endwhile 986 987 call setpos('.', saved_position) 988 return 0 989 endfunction 990 991 " }}}1 992 993 let &cpo = s:cpo_save 994 unlet s:cpo_save 995 996 " vim:set sw=2 sts=2 ts=8 et: