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