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