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