if polyglot#init#is_disabled(expand('<sfile>:p'), 'crystal', 'autoload/crystal/indent.vim')
  finish
endif

" Variables {{{1
" =========

" Syntax group names that are strings.
let g:crystal#indent#syng_string =
      \ '\<crystal\%(String\|Interpolation\|NoInterpolation\|StringEscape\)\>'
lockvar g:crystal#indent#syng_string

" Syntax group names that are strings/symbols/regexes or comments.
let g:crystal#indent#syng_strcom =
      \ g:crystal#indent#syng_string .
      \ '\|' .
      \ '\<crystal\%(CharLiteral\|Comment\|Regexp\|RegexpCharClass\|RegexpEscape\|Symbol\|ASCIICode\)\>'
lockvar g:crystal#indent#syng_strcom

" Syntax group names that are string/regex/symbol delimiters.
let g:crystal#indent#syng_delim =
      \ '\<crystal\%(StringDelimiter\|RegexpDelimiter\|SymbolDelimiter\|InterpolationDelim\)\>'
lockvar g:crystal#indent#syng_delim

" Syntax group that represents all of the above combined.
let g:crystal#indent#syng_strcomdelim =
      \ g:crystal#indent#syng_strcom .
      \ '\|' .
      \ g:crystal#indent#syng_delim
lockvar g:crystal#indent#syng_strcomdelim

" Regex for the start of a line
let g:crystal#indent#sol = '\%(\_^\|;\)\s*\zs'
lockvar g:crystal#indent#sol

" Regex for the end of a line
let g:crystal#indent#eol = '\ze\s*\%(#.*\)\=\%(\_$\|;\)'
lockvar g:crystal#indent#eol

" Expression used to check whether we should skip a match with searchpair().
let g:crystal#indent#skip_expr =
      \ 'crystal#indent#IsInStringOrComment(line("."), col("."))'
lockvar g:crystal#indent#skip_expr

" Regex that defines a link attribute
let g:crystal#indent#link_attribute_regex =
      \ g:crystal#indent#sol.'@\[.*]'
lockvar g:crystal#indent#link_attribute_regex

" Regex that defines a type declaration
let g:crystal#indent#type_declaration_regex =
      \ g:crystal#indent#sol .
      \ '\%(\<\%(private\|protected\)\s\+\)\=' .
      \ '\%(\<\%(getter\|setter\|property\)?\=\s\+\)\=' .
      \ '@\=\h\k*\s\+:\s\+\S.*'
lockvar g:crystal#indent#type_declaration_regex

" Regex for operator symbols:
" , : / + * - = ~ < & ^ \
" | that is not part of a block opening
" % that is not part of a macro delimiter
" ! that is not part of a method name
" ? that is not part of a method name
" > that is not part of a ->
"
" Additionally, all symbols must not be part of a global variable name,
" like $~.
let g:crystal#indent#operator_regex =
      \ '\$\@1<!' .
      \ '\%(' .
      \ '[.,:/+*\-=~<&^\\]' .
      \ '\|' .
      \ '\%(\%(\<do\>\|%\@1<!{\)\s*|[^|]*\)\@<!|' .
      \ '\|' .
      \ '{\@1<!%' .
      \ '\|' .
      \ '\%(\k\|]\)\@1<!\!' .
      \ '\|' .
      \ '\%(\k\|]\)\@1<!?' .
      \ '\|' .
      \ '-\@1<!>' .
      \ '\)'
lockvar g:crystal#indent#operator_regex

" Regex that defines blocks.
let g:crystal#indent#block_regex =
      \ '\%(' .
      \ '\%('.g:crystal#indent#operator_regex.'\s*\)\@<!{\@1<!{' .
      \ '\|' .
      \ '\<do\>' .
      \ '\)' .
      \ '\s*\%(|[^|]*|\)\=' .
      \ g:crystal#indent#eol
lockvar g:crystal#indent#block_regex

" Regex that defines the beginning of a hanging expression.
let g:crystal#indent#hanging_assignment_regex =
      \ '\%('.g:crystal#indent#operator_regex.'\s*\)\@<=' .
      \ '\.\@1<!\<\%(if\||unless\|case\|begin\|select\)\>'
lockvar g:crystal#indent#hanging_assignment_regex

" Regex that defines the start-match for the 'end' keyword.
let g:crystal#indent#end_start_regex =
      \ '\%(' .
      \ g:crystal#indent#sol .
      \ '\%(' .
      \ '\%(\<\%(private\|protected\)\s\+\)\=' .
      \ '\%(\<\%(abstract\s\+\)\=\%(class\|struct\)\>\|\<\%(def\|module\|macro\|lib\|enum\|annotation\)\>\)' .
      \ '\|' .
      \ '\<\%(if\|unless\|while\|until\|case\|begin\|union\|select\)\>' .
      \ '\)' .
      \ '\|' .
      \ g:crystal#indent#hanging_assignment_regex .
      \ '\|' .
      \ g:crystal#indent#block_regex .
      \ '\)'
lockvar g:crystal#indent#end_start_regex

" Regex that defines the middle-match for the 'end' keyword.
let g:crystal#indent#end_middle_regex =
      \ g:crystal#indent#sol .
      \ '\<\%(else\|elsif\|when\|in\|rescue\|ensure\)\>'
lockvar g:crystal#indent#end_middle_regex

" Regex that defines the end-match for the 'end' keyword.
let g:crystal#indent#end_end_regex =
      \ g:crystal#indent#sol.'\%(\<end\>\|%\@1<!}\@1<!}}\@!\)'
lockvar g:crystal#indent#end_end_regex

" Regex used for words that, at the start of a line, add a level of indent.
let g:crystal#indent#crystal_indent_keywords =
      \ g:crystal#indent#end_start_regex .
      \ '\|' .
      \ g:crystal#indent#end_middle_regex
lockvar g:crystal#indent#crystal_indent_keywords

" Regex used for words that, at the start of a line, remove a level of indent.
let g:crystal#indent#crystal_deindent_keywords =
      \ g:crystal#indent#end_middle_regex .
      \ '\|' .
      \ g:crystal#indent#end_end_regex
lockvar g:crystal#indent#crystal_deindent_keywords

" Regex that defines hanging expressions for macro control tags.
let g:crystal#indent#macro_hanging_assignment_regex =
      \ '\%('.g:crystal#indent#operator_regex.'\s*\)\@<=' .
      \ '\\\=\zs{%\s*\%(if\|unless\|begin\)\>.*%}'
lockvar g:crystal#indent#macro_hanging_assignment_regex

" Regex that defines the start-match for the 'end' keyword in macro
" control tags.
let g:crystal#indent#macro_end_start_regex =
      \ '\%(' .
      \ g:crystal#indent#sol .
      \ '\%(' .
      \ '\\\=\zs{%\s*\%(if\|unless\|for\|begin\)\>.*%}' .
      \ '\|' .
      \ '\\\=\zs{%.*\<do\s*%}' .
      \ '\)' .
      \ '\|' .
      \ g:crystal#indent#macro_hanging_assignment_regex .
      \ '\)'
lockvar g:crystal#indent#macro_end_start_regex

" Regex that defines the middle-match for the 'end' keyword in macro
" control tags.
let g:crystal#indent#macro_end_middle_regex =
      \ g:crystal#indent#sol.'\\\=\zs{%\s*\%(else\|elsif\)\>.*%}'
lockvar g:crystal#indent#macro_end_middle_regex

" Regex that defines the end-match for the 'end' keyword in macro
" control tags.
let g:crystal#indent#macro_end_end_regex =
      \ g:crystal#indent#sol.'\\\=\zs{%\s*end\s*%}'
lockvar g:crystal#indent#macro_end_end_regex

" Regex used for words that, at the start of a line, add a level of
" indent after macro control tags.
let g:crystal#indent#crystal_macro_indent_keywords =
      \ g:crystal#indent#macro_end_start_regex .
      \ '\|' .
      \ g:crystal#indent#macro_end_middle_regex
lockvar g:crystal#indent#crystal_macro_indent_keywords

" Regex used for words that, at the start of a line, remove a level of
" indent after macro control tags.
let g:crystal#indent#crystal_macro_deindent_keywords =
      \ g:crystal#indent#macro_end_middle_regex .
      \ '\|' .
      \ g:crystal#indent#macro_end_end_regex
lockvar g:crystal#indent#crystal_macro_deindent_keywords

" Regex that defines bracket continuations
let g:crystal#indent#bracket_continuation_regex =
      \ '%\@1<![({[]'.g:crystal#indent#eol
lockvar g:crystal#indent#bracket_continuation_regex

" Regex that defines continuation lines, not including (, {, or [.
let g:crystal#indent#non_bracket_continuation_regex =
      \ '\%(' .
      \ g:crystal#indent#operator_regex .
      \ '\|' .
      \ '\<\%(if\|unless\|then\)\>' .
      \ '\)' .
      \ g:crystal#indent#eol
lockvar g:crystal#indent#non_bracket_continuation_regex

" Regex that defines continuation lines.
let g:crystal#indent#continuation_regex =
      \ g:crystal#indent#bracket_continuation_regex .
      \ '\|' .
      \ g:crystal#indent#non_bracket_continuation_regex
lockvar g:crystal#indent#continuation_regex

" Regex that defines dot continuations
let g:crystal#indent#dot_continuation_regex = '\.'.g:crystal#indent#eol
lockvar g:crystal#indent#dot_continuation_regex

" Regex that defines end of bracket continuation followed by another continuation
let g:crystal#indent#bracket_switch_continuation_regex =
      \ '^\%([^(]\+\zs).\+\)\+\%('.g:crystal#indent#continuation_regex.'\)'
lockvar g:crystal#indent#bracket_switch_continuation_regex

let g:crystal#indent#block_continuation_regex =
      \ '^\s*[^])}\t ].*'.g:crystal#indent#block_regex
lockvar g:crystal#indent#block_continuation_regex

" Regex that describes a leading operator (only a method call's dot for now)
let g:crystal#indent#leading_operator_regex = g:crystal#indent#sol.'\.'
lockvar g:crystal#indent#leading_operator_regex

" Indent callbacks for the current line
let g:crystal#indent#curr_line_callbacks = [
      \ 'crystal#indent#MultilineString',
      \ 'crystal#indent#ClosingBracketOnEmptyLine',
      \ 'crystal#indent#DeindentingMacroTag',
      \ 'crystal#indent#DeindentingKeyword',
      \ 'crystal#indent#LeadingOperator'
      \ ]
lockvar g:crystal#indent#curr_line_callbacks

" Indent callbacks for the previous line
let g:crystal#indent#prev_line_callbacks = [
      \ 'crystal#indent#StartOfFile',
      \ 'crystal#indent#AfterTypeDeclaration',
      \ 'crystal#indent#AfterLinkAttribute',
      \ 'crystal#indent#ContinuedLine',
      \ 'crystal#indent#AfterBlockOpening',
      \ 'crystal#indent#AfterUnbalancedBracket',
      \ 'crystal#indent#AfterLeadingOperator',
      \ 'crystal#indent#AfterEndMacroTag',
      \ 'crystal#indent#AfterEndKeyword',
      \ 'crystal#indent#AfterIndentMacroTag',
      \ 'crystal#indent#AfterIndentKeyword'
      \ ]
lockvar g:crystal#indent#prev_line_callbacks

" Indent callbacks for the MSL
let g:crystal#indent#msl_callbacks = [
      \ 'crystal#indent#PreviousNotMSL',
      \ 'crystal#indent#IndentingKeywordInMSL',
      \ 'crystal#indent#ContinuedHangingOperator'
      \ ]
lockvar g:crystal#indent#msl_callbacks

" Indenting Logic Callbacks {{{1
" =========================

function! crystal#indent#ClosingBracketOnEmptyLine(cline_info) abort
  let info = a:cline_info

  " If we got a closing bracket on an empty line, find its match and indent
  " according to it.  For parentheses we indent to its column - 1, for the
  " others we indent to the containing line's MSL's level.  Return -1 if fail.

  let idx = match(info.cline, g:crystal#indent#sol.'[]})]')

  if idx >= 0
    let closing_bracket = info.cline[idx]

    if closing_bracket ==# ')'
      let opening_bracket = '('
    elseif closing_bracket ==# ']'
      let opening_bracket = '\['
    elseif closing_bracket ==# '}'
      let opening_bracket = '{'
    endif

    call searchpair(
          \ opening_bracket,
          \ '',
          \ closing_bracket,
          \ 'bW',
          \ g:crystal#indent#skip_expr)

    if line('.') == info.clnum
      return indent('.')
    endif

    if g:crystal_indent_block_style ==# 'do' &&
          \ getline('.') =~# g:crystal#indent#block_regex
        return col('.') - 1
      else
        return indent(crystal#indent#GetMSL(line('.')))
    endif
  endif

  return -1
endfunction

function! crystal#indent#DeindentingKeyword(cline_info) abort
  let info = a:cline_info

  " If we have a deindenting keyword, find its match and indent to its level.
  let idx = match(info.cline, g:crystal#indent#crystal_deindent_keywords)

  if idx >= 0
    call cursor(0, idx + 1)

    call searchpair(
          \ g:crystal#indent#end_start_regex,
          \ g:crystal#indent#end_middle_regex,
          \ g:crystal#indent#end_end_regex,
          \ 'bW',
          \ g:crystal#indent#skip_expr)

    let lnum = line('.')

    " If the search did not change the current line, then either 1) the
    " code is malformed or 2) the indenting keyword is on the same line
    " as this one: in either case, do nothing and exit the indent
    " expression.
    if lnum == info.clnum
      return indent('.')
    endif

    " Count the number of both opening and closing macro control tags
    " between this line and the starting line: if the number of
    " opening tags is greater than the number of closing tags, then we
    " must be inside of a macro block, so indent accordingly.
    let diff = crystal#indent#RelativeMacroDepth(lnum, info.clnum)

    if diff > 0
      return indent(lnum) + info.sw * (diff + 1)
    elseif diff < 0
      return indent(lnum) + info.sw * (diff - 1)
    endif

    " If none of the above special cases apply, proceed normally.
    let line = getline(lnum)

    if g:crystal_indent_block_style ==# 'do' &&
          \ line =~# g:crystal#indent#block_regex
      return col('.') - 1
    elseif g:crystal_indent_assignment_style ==# 'hanging' &&
          \ line =~# g:crystal#indent#hanging_assignment_regex
      return col('.') - 1
    else
      return indent(crystal#indent#GetMSL(lnum))
    endif
  endif

  return -1
endfunction

function! crystal#indent#DeindentingMacroTag(cline_info) abort
  let info = a:cline_info

  " If we have a deindenting tag, find its match and indent to its level.
  let idx = match(info.cline, g:crystal#indent#crystal_macro_deindent_keywords)

  if idx >= 0
    call cursor(0, idx + 1)

    call searchpair(
          \ g:crystal#indent#macro_end_start_regex,
          \ g:crystal#indent#macro_end_middle_regex,
          \ g:crystal#indent#macro_end_end_regex,
          \ 'bW',
          \ g:crystal#indent#skip_expr)

    " If this tag was preceded by a \, we need to keep searching until
    " we find a tag that also has a \.
    if info.cline[idx - 1] ==# '\'
      while getline('.')[col('.') - 2] !=# '\'
        call searchpair(
              \ g:crystal#indent#macro_end_start_regex,
              \ g:crystal#indent#macro_end_middle_regex,
              \ g:crystal#indent#macro_end_end_regex,
              \ 'bW',
              \ g:crystal#indent#skip_expr)
      endwhile

      " Position the cursor on the \ for later
      call cursor(0, col('.') - 1)
    endif

    " If the search did not change the current line, then either 1) the
    " code is malformed or 2) the indenting tag is on the same line as
    " this one: in either case, do nothing and exit the indent
    " expression.
    if line('.') == info.clnum
      return indent('.')
    endif

    if g:crystal_indent_assignment_style ==# 'hanging' &&
          \ getline('.') =~# g:crystal#indent#macro_hanging_assignment_regex
      return col('.') - 1
    else
      return indent('.')
    endif
  endif

  return -1
endfunction

function! crystal#indent#MultilineString(cline_info) abort
  let info = a:cline_info

  " If we are in a multi-line string, don't do anything to it.
  if crystal#indent#IsInString(info.clnum, 1)
    return indent('.')
  endif

  return -1
endfunction

function! crystal#indent#LeadingOperator(cline_info) abort
  let info = a:cline_info

  " If the current line starts with a leading operator, add a level of indent.
  if info.cline =~# g:crystal#indent#leading_operator_regex
    return indent(crystal#indent#GetMSL(info.clnum)) + info.sw
  endif

  return -1
endfunction

function! crystal#indent#EmptyInsideString(pline_info) abort
  let info = a:pline_info

  " If the line is empty and inside a string (the previous line is a string,
  " too), use the previous line's indent

  let plnum = prevnonblank(info.clnum - 1)
  let pline = getline(plnum)

  if info.cline =~# '^\s*$'
        \ && crystal#indent#IsInString(plnum, 1)
        \ && crystal#indent#IsInString(plnum, strlen(pline))
    return indent(plnum)
  endif

  return -1
endfunction

function! crystal#indent#StartOfFile(pline_info) abort
  let info = a:pline_info

  " At the start of the file use zero indent.
  if info.plnum == 0
    return 0
  endif

  return -1
endfunction

function! crystal#indent#AfterTypeDeclaration(pline_info) abort
  let info = a:pline_info

  " Short circuit if the previous line was a type declaration; this
  " allows us to skip checking for type declarations before * and
  " ? later on, which will save a lot of time.

  if info.pline =~# g:crystal#indent#type_declaration_regex
    if info.pline =~# ','.g:crystal#indent#eol
      return indent(info.plnum)
    else
      let idx = match(info.pline, g:crystal#indent#block_regex)

      if idx >= 0
        if g:crystal_indent_block_style ==# 'do'
          return idx + info.sw
        else
          return indent(info.plnum)
        endif
      endif

      return indent(crystal#indent#GetMSL(info.plnum))
    endif
  endif

  return -1
endfunction

function! crystal#indent#AfterLinkAttribute(pline_info) abort
  let info = a:pline_info

  " Short circuit if the previous line was a link attribute.

  if info.pline =~# g:crystal#indent#link_attribute_regex
    return indent(info.plnum)
  endif

  return -1
endfunction

" Example:
"
"   if foo || bar ||
"       baz || bing
"     puts "foo"
"   end
"
function! crystal#indent#ContinuedLine(pline_info) abort
  let info = a:pline_info

  let idx = match(info.pline, g:crystal#indent#end_start_regex)

  if idx >= 0 && info.pline =~# g:crystal#indent#non_bracket_continuation_regex
    if info.pline =~# g:crystal#indent#hanging_assignment_regex
      if g:crystal_indent_assignment_style ==# 'hanging'
        " hanging indent
        let ind = idx
      else
        " align with variable
        let ind = indent(info.plnum)
      endif
    else
      let ind = indent(crystal#indent#GetMSL(info.plnum))
    endif

    return ind + info.sw * 2
  endif

  return -1
endfunction

function! crystal#indent#AfterBlockOpening(pline_info) abort
  let info = a:pline_info

  " If the previous line ended with a block opening, add a level of indent.
  let idx = match(info.pline, g:crystal#indent#block_regex)

  if idx >= 0
    if g:crystal_indent_block_style ==# 'do'
      " don't align to the msl, align to the "do"
      let ind = idx + info.sw
    else
      let plnum_msl = crystal#indent#GetMSL(info.plnum)

      if getline(plnum_msl) =~# '='.g:crystal#indent#eol
        " in the case of assignment to the msl, align to the starting line,
        " not to the msl
        let ind = indent(info.plnum) + info.sw
      else
        let ind = indent(plnum_msl) + info.sw
      endif
    endif

    return ind
  endif

  return -1
endfunction

function! crystal#indent#AfterLeadingOperator(pline_info) abort
  let info = a:pline_info

  " If the previous line started with a leading operator, use its MSL's level
  " of indent
  if info.pline =~# g:crystal#indent#leading_operator_regex
    return indent(crystal#indent#GetMSL(info.plnum))
  endif

  return -1
endfunction

function! crystal#indent#AfterUnbalancedBracket(pline_info) abort
  let info = a:pline_info

  " If the previous line contained unclosed opening brackets and we are still
  " in them, find the rightmost one and add indent depending on the bracket
  " type.
  "
  " If it contained hanging closing brackets, find the rightmost one, find its
  " match and indent according to that.

  if info.pline =~# '[[({]\|[])}]'.g:crystal#indent#eol
    let [opening, closing] = crystal#indent#ExtraBrackets(info.plnum)

    if opening.pos != -1
      if strpart(info.pline, opening.pos + 1) =~# '^'.g:crystal#indent#eol
        return indent(crystal#indent#GetMSL(info.plnum)) + info.sw
      else
        return opening.pos + 1
      endif
    elseif closing.pos != -1
      call cursor(info.plnum, closing.pos + 1)

      if closing.type ==# ')'
        let target = '('
      elseif closing.type ==# ']'
        let target = '\['
      elseif closing.type ==# '}'
        let target = '{'
      endif

      call searchpair(target, '', closing.type, 'bW', g:crystal#indent#skip_expr)

      return indent(crystal#indent#GetMSL(line('.')))
    end
  endif

  return -1
endfunction

function! crystal#indent#AfterEndKeyword(pline_info) abort
  let info = a:pline_info

  let idx = match(info.pline, g:crystal#indent#end_end_regex)

  if idx >= 0
    if g:crystal_indent_assignment_style ==# 'variable' &&
          \ g:crystal_indent_block_style ==# 'expression'
      " Simply align with the "end"
      return idx
    endif

    " Return the indent of the nearest indenting line
    call cursor(info.plnum, idx + 1)

    let lnum = searchpair(
          \ g:crystal#indent#end_start_regex,
          \ '',
          \ g:crystal#indent#end_end_regex,
          \ 'bW',
          \ g:crystal#indent#skip_expr)

    return indent(crystal#indent#GetMSL(lnum))
  endif

  return -1
endfunction

function! crystal#indent#AfterEndMacroTag(pline_info) abort
  let info = a:pline_info

  " If the previous line ended with an "end" macro tag, match the indent
  " of that tag's corresponding opening tag.
  let idx = match(info.pline, g:crystal#indent#macro_end_end_regex)

  if idx >= 0
    call cursor(info.plnum, idx + 1)

    if g:crystal_indent_assignment_style ==# 'hanging'
      let lnum = searchpair(
            \ g:crystal#indent#macro_end_start_regex,
            \ '',
            \ g:crystal#indent#macro_end_end_regex,
            \ 'bW',
            \ g:crystal#indent#skip_expr)
    else
      let lnum = searchpair(
            \ g:crystal#indent#macro_end_start_regex,
            \ g:crystal#indent#macro_end_middle_regex,
            \ g:crystal#indent#macro_end_end_regex,
            \ 'bW',
            \ g:crystal#indent#skip_expr)
    endif

    return indent(lnum)
  end

  return -1
endfunction

function! crystal#indent#AfterIndentKeyword(pline_info) abort
  let info = a:pline_info

  let idx = match(info.pline, g:crystal#indent#crystal_indent_keywords)

  if idx >= 0
    " If there is an "end" after the indenting keyword on the same line,
    " do nothing.
    let idx2 = match(info.pline, g:crystal#indent#end_end_regex)

    if idx2 > idx
      return indent('.')
    endif

    if g:crystal_indent_assignment_style ==# 'hanging' &&
          \ info.pline =~# g:crystal#indent#hanging_assignment_regex
      return idx + info.sw
    else
      return indent(info.plnum) + info.sw
    endif
  endif

  return -1
endfunction

function! crystal#indent#AfterIndentMacroTag(pline_info) abort
  let info = a:pline_info

  let idx = match(info.pline, g:crystal#indent#crystal_macro_indent_keywords)

  if idx >= 0
    if g:crystal_indent_assignment_style ==# 'hanging' &&
          \ info.pline =~# g:crystal#indent#macro_hanging_assignment_regex
      " If the indenting tag was preceded by a \, we must shift over an
      " additional space.
      let shift = info.pline[idx - 1] ==# '\'
      return idx + info.sw - shift
    else
      return indent(info.plnum) + info.sw
    endif
  endif

  return -1
endfunction

function! crystal#indent#PreviousNotMSL(msl_info) abort
  let info = a:msl_info

  if info.plnum != info.plnum_msl
    if info.pline =~# g:crystal#indent#bracket_switch_continuation_regex
      return indent(info.plnum) - 1
    elseif info.pline =~# g:crystal#indent#non_bracket_continuation_regex
      return indent(info.plnum)
    endif
  endif

  return -1
endfunction

function! crystal#indent#IndentingKeywordInMSL(msl_info) abort
  let info = a:msl_info

  " If the MSL line had an indenting keyword in it, add a level of indent.
  let idx = match(info.pline_msl, g:crystal#indent#crystal_indent_keywords)

  if idx >= 0
    let ind = indent(info.plnum_msl) + info.sw

    if info.pline_msl =~# g:crystal#indent#end_end_regex
      let ind = ind - info.sw
    elseif info.pline =~# g:crystal#indent#hanging_assignment_regex
      if g:crystal_indent_assignment_style ==# 'hanging'
        " hanging indent
        let ind = idx + info.sw
      else
        " align with variable
        let ind = indent(info.plnum_msl) + info.sw
      endif
    endif

    return ind
  endif

  return -1
endfunction

function! crystal#indent#ContinuedHangingOperator(msl_info) abort
  let info = a:msl_info

  " If the previous line ended with an operator but wasn't a block
  " ending or a closing bracket, indent one extra level.
  if crystal#indent#Match(info.plnum_msl, g:crystal#indent#non_bracket_continuation_regex) &&
        \ info.pline_msl !~# g:crystal#indent#sol.'\%([\])}]\|\<end\>\)'
    if info.plnum_msl == info.plnum
      let ind = indent(info.plnum_msl) + info.sw
    else
      let ind = indent(info.plnum_msl)
    endif

    return ind
  endif

  return -1
endfunction

" Auxiliary Functions {{{1
" ===================

" Check if the character at lnum:col is inside a string.
function! crystal#indent#IsInString(lnum, col) abort
  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~# g:crystal#indent#syng_string
endfunction

" Check if the character at lnum:col is inside a string delimiter.
function! crystal#indent#IsInStringDelimiter(lnum, col) abort
  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~# g:crystal#indent#syng_delim
endfunction

" Check if the character at lnum:col is inside a string, comment, regexp, etc.
function! crystal#indent#IsInStringOrComment(lnum, col) abort
  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~# g:crystal#indent#syng_strcom
endfunction

" Check if the character lnum:col is inside a string, comment, regexp,
" delimiter, etc.
function! crystal#indent#IsInStringOrCommentOrDelimiter(lnum, col) abort
  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~# g:crystal#indent#syng_strcomdelim
endfunction

function! crystal#indent#IsAssignment(str, pos) abort
  return strpart(a:str, 0, a:pos - 1) =~# '=\s*$'
endfunction

function! crystal#indent#IsLineComment(lnum) abort
  return getline(a:lnum) =~# g:crystal#indent#sol.'#'
endfunction

" Determine the relative macro block depth of one line versus another.
" 'a' and 'b' are the line numbers for said lines.
"
" For example:
"
" A return value of 2 would indicate that line A is inside two macro
" blocks relative to line B.
"
" A return value of -2 would indicate that line B is inside two macro
" blocks relative to line A.
"
" A return value of 0 would indicate that line A and line B are either
" in the same macro block or at the same relative macro block depth.
"
" NOTE: It is assumed that a < b - 2; otherwise, the return value will
" always be 0.
function! crystal#indent#RelativeMacroDepth(a, b) abort
  let diff = 0

  for i in range(a:a + 1, a:b - 1)
    if crystal#indent#Match(i, g:crystal#indent#macro_end_start_regex)
      let diff += 1
    elseif crystal#indent#Match(i, g:crystal#indent#macro_end_end_regex)
      let diff -= 1
    endif
  endfor

  return diff
endfunction

" Wrapper for prevnonblank() that skips lines that are line comments or
" inside of multiline strings.
function! crystal#indent#PrevNonBlank(lnum) abort
  let lnum = prevnonblank(a:lnum)

  while lnum > 0
    let line = getline(lnum)
    let start = match(line, '\S') + 1

    if !crystal#indent#IsInStringOrComment(lnum, start)
      break
    endif

    let lnum = prevnonblank(lnum - 1)
  endwhile

  return lnum
endfunction

" Find line above 'lnum' that started the continuation 'lnum' may be part of.
function! crystal#indent#GetMSL(lnum) abort
  " Start on the line we're at and use its indent.
  let mslnum = a:lnum
  let lnum = crystal#indent#PrevNonBlank(a:lnum - 1)

  while lnum > 0
    " If we have a continuation line, or we're in a string, use line as MSL.
    " Otherwise, terminate search as we have found our MSL already.
    let msl = getline(mslnum)
    let line = getline(lnum)

    if msl =~# g:crystal#indent#leading_operator_regex
      " If the current line starts with a leading operator, keep its indent
      " and keep looking for an MSL.
      let mslnum = lnum
    elseif line =~# g:crystal#indent#type_declaration_regex &&
          \ line !~# ','.g:crystal#indent#eol &&
          \ line !~# g:crystal#indent#block_regex
      " If the previous line is a type declaration that doesn't end with
      " a comman or a block opening, it is the MSL.
      "
      " Example:
      "   record ColorRGB,
      "     red : UInt8,
      "     green : UInt8,
      "     blue : UInt8 do
      "     def fore(io : IO) : Nil
      "       io << "38;2;"
      "       io << red << ";"
      "       io << green << ";"
      "       io << blue
      "     end
      "   end
      "
      return mslnum
    elseif line =~# g:crystal#indent#non_bracket_continuation_regex &&
          \ msl =~# g:crystal#indent#non_bracket_continuation_regex
      " If the current line is a non-bracket continuation and so is the
      " previous one, keep its indent and continue looking for an mslnum.
      "
      " Example:
      "   method_call one,
      "     two,
      "     three
      "
      let mslnum = lnum
    elseif line =~# g:crystal#indent#non_bracket_continuation_regex &&
          \ (
          \ msl =~# g:crystal#indent#bracket_continuation_regex ||
          \ msl =~# g:crystal#indent#block_continuation_regex
          \ )
      " If the current line is a bracket continuation or a block-starter, but
      " the previous is a non-bracket one, keep looking for an mslnum.
      "
      " Example:
      "   method_call one,
      "     two {
      "     three
      "
      "   method_call one,
      "     two,
      "     three {
      "     four
      "
      let mslnum = lnum
    elseif line =~# g:crystal#indent#bracket_continuation_regex &&
          \ (
          \ msl =~# g:crystal#indent#bracket_continuation_regex ||
          \ msl =~# g:crystal#indent#block_continuation_regex
          \ )
      " If both lines are bracket continuations (the current may also be a
      " block-starter), use the current one's and stop here.
      "
      " Example:
      "   method_call(
      "     other_method_call(
      "       foo
      "
      return mslnum
    elseif line =~# g:crystal#indent#block_regex &&
          \ msl !~# g:crystal#indent#continuation_regex
      " If the previous line is a block-starter and the current one is
      " mostly ordinary, use the current one as the mslnum.
      "
      " Example:
      "   method_call do
      "     something
      "     something_else
      "
      return mslnum
    elseif line =~# g:crystal#indent#continuation_regex
      let mslnum = lnum
    else
      break
    endif

    let lnum = crystal#indent#PrevNonBlank(lnum - 1)
  endwhile

  return mslnum
endfunction

" Check if line 'lnum' has more opening brackets than closing ones.
function! crystal#indent#ExtraBrackets(lnum) abort
  let opening = {'parentheses': [], 'braces': [], 'brackets': []}
  let closing = {'parentheses': [], 'braces': [], 'brackets': []}

  let line = getline(a:lnum)
  let pos  = match(line, '[][(){}]')

  " Save any encountered opening brackets, and remove them once a matching
  " closing one has been found. If a closing bracket shows up that doesn't
  " close anything, save it for later.
  while pos != -1
    if !crystal#indent#IsInStringOrComment(a:lnum, pos + 1)
      if line[pos] ==# '('
        call add(opening.parentheses, {'type': '(', 'pos': pos})
      elseif line[pos] ==# ')'
        if empty(opening.parentheses)
          call add(closing.parentheses, {'type': ')', 'pos': pos})
        else
          let opening.parentheses = opening.parentheses[0:-2]
        endif
      elseif line[pos] ==# '{'
        call add(opening.braces, {'type': '{', 'pos': pos})
      elseif line[pos] ==# '}'
        if empty(opening.braces)
          call add(closing.braces, {'type': '}', 'pos': pos})
        else
          let opening.braces = opening.braces[0:-2]
        endif
      elseif line[pos] ==# '['
        call add(opening.brackets, {'type': '[', 'pos': pos})
      elseif line[pos] ==# ']'
        if empty(opening.brackets)
          call add(closing.brackets, {'type': ']', 'pos': pos})
        else
          let opening.brackets = opening.brackets[0:-2]
        endif
      endif
    endif

    let pos = match(line, '[][(){}]', pos + 1)
  endwhile

  " Find the rightmost brackets, since they're the ones that are important in
  " both opening and closing cases
  let rightmost_opening = {'type': '(', 'pos': -1}
  let rightmost_closing = {'type': ')', 'pos': -1}

  for opening in opening.parentheses + opening.braces + opening.brackets
    if opening.pos > rightmost_opening.pos
      let rightmost_opening = opening
    endif
  endfor

  for closing in closing.parentheses + closing.braces + closing.brackets
    if closing.pos > rightmost_closing.pos
      let rightmost_closing = closing
    endif
  endfor

  return [rightmost_opening, rightmost_closing]
endfunction

function! crystal#indent#Match(lnum, regex) abort
  let line   = getline(a:lnum)
  let offset = match(line, '\C'.a:regex)
  let col    = offset + 1

  while col && crystal#indent#IsInStringOrCommentOrDelimiter(a:lnum, col)
    let offset = match(line, '\C'.a:regex, offset + 1)
    let col = offset + 1
  endwhile

  return col ? col : 0
endfunction

" }}}1

" vim:sw=2 sts=2 ts=8 fdm=marker et:
