summaryrefslogtreecommitdiffstats
path: root/autoload/crystal/indent.vim
diff options
context:
space:
mode:
Diffstat (limited to 'autoload/crystal/indent.vim')
-rw-r--r--autoload/crystal/indent.vim371
1 files changed, 371 insertions, 0 deletions
diff --git a/autoload/crystal/indent.vim b/autoload/crystal/indent.vim
new file mode 100644
index 00000000..dbf4c141
--- /dev/null
+++ b/autoload/crystal/indent.vim
@@ -0,0 +1,371 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+" Variables {{{1
+" =========
+
+" Regex of syntax group names that are strings or characters.
+let g:crystal#indent#syng_string =
+ \ '\<crystal\%(String\|Interpolation\|NoInterpolation\|StringEscape\|CharLiteral\|ASCIICode\)\>'
+lockvar g:crystal#indent#syng_string
+
+" Regex of syntax group names that are strings, characters, symbols,
+" regexps, or comments.
+let g:crystal#indent#syng_strcom =
+ \ g:crystal#indent#syng_string.'\|' .
+ \ '\<crystal\%(Regexp\|RegexpEscape\|Symbol\|Comment\)\>'
+lockvar g:crystal#indent#syng_strcom
+
+" Expression used to check whether we should skip a match with searchpair().
+let g:crystal#indent#skip_expr =
+ \ 'synIDattr(synID(line("."), col("."), 1), "name") =~# "'.g:crystal#indent#syng_strcom.'"'
+lockvar g:crystal#indent#skip_expr
+
+" Regex for the start of a line:
+" start of line + whitespace + optional opening macro delimiter
+let g:crystal#indent#sol = '^\s*\zs\%(\\\={%\s*\)\='
+lockvar g:crystal#indent#sol
+
+" Regex for the end of a line:
+" whitespace + optional closing macro delimiter + whitespace +
+" optional comment + end of line
+let g:crystal#indent#eol = '\s*\%(%}\)\=\ze\s*\%(#.*\)\=$'
+lockvar g:crystal#indent#eol
+
+" Regex that defines the start-match for the 'end' keyword.
+" NOTE: This *should* properly match the 'do' only at the end of the
+" line
+let g:crystal#indent#end_start_regex =
+ \ g:crystal#indent#sol .
+ \ '\%(' .
+ \ '\%(\<\%(private\|protected\)\s\+\)\=' .
+ \ '\%(\<\%(abstract\s\+\)\=\%(class\|struct\)\>\|\<\%(def\|module\|macro\|lib\|enum\)\>\)' .
+ \ '\|' .
+ \ '\<\%(if\|unless\|while\|until\|case\|begin\|for\|union\)\>' .
+ \ '\)' .
+ \ '\|' .
+ \ '.\{-}\zs\<do\s*\%(|.*|\)\='.g:crystal#indent#eol
+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\|rescue\|ensure\|when\)\>'
+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\>'
+lockvar g:crystal#indent#end_end_regex
+
+" Regex used for words that 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 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 a type declaration
+let g:crystal#indent#crystal_type_declaration =
+ \ '@\=\h\k*\s\+:\s\+\S.*'
+lockvar g:crystal#indent#crystal_type_declaration
+
+" Regex that defines continuation lines, not including (, {, or [.
+let g:crystal#indent#non_bracket_continuation_regex =
+ \ '\%(' .
+ \ '[\\.,:/%+\-=~<>|&^]' .
+ \ '\|' .
+ \ '\W?' .
+ \ '\|' .
+ \ '\<\%(if\|unless\)\>' .
+ \ '\|' .
+ \ '\%('.g:crystal#indent#sol.g:crystal#indent#crystal_type_declaration.'\h\k*\)\@<!\*' .
+ \ '\)' .
+ \ g:crystal#indent#eol
+lockvar g:crystal#indent#non_bracket_continuation_regex
+
+" Regex that defines bracket continuations
+let g:crystal#indent#bracket_continuation_regex = '%\@1<!\%([({[]\)\s*\%(#.*\)\=$'
+lockvar g:crystal#indent#bracket_continuation_regex
+
+" Regex that defines continuation lines.
+let g:crystal#indent#continuation_regex =
+ \ g:crystal#indent#non_bracket_continuation_regex .
+ \ '\|' .
+ \ g:crystal#indent#bracket_continuation_regex
+lockvar g:crystal#indent#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
+
+" Regex that defines continuable keywords
+let g:crystal#indent#continuable_regex =
+ \ '\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
+ \ '\<\%(if\|for\|while\|until\|unless\):\@!\>'
+lockvar g:crystal#indent#continuable_regex
+
+" Regex that defines the first part of a splat pattern
+let g:crystal#indent#splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
+lockvar g:crystal#indent#splat_regex
+
+" Regex that defines blocks.
+"
+" Note that there's a slight problem with this regex and crystal#indent#continuation_regex.
+" Code like this will be matched by both:
+"
+" method_call do |(a, b)|
+"
+" The reason is that the pipe matches a hanging "|" operator.
+"
+let g:crystal#indent#block_regex =
+ \ '\%(\<do:\@!\>\|%\@1<!{\)\s*\%(|\s*(*\s*\%([*@&]\=\h\w*,\=\s*\)\%(,\s*(*\s*[*@&]\=\h\w*\s*)*\s*\)*|\)\=\s*\%(%}\)\=\s*\%(#.*\)\=$'
+lockvar g:crystal#indent#block_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 = '^\s*[.]'
+lockvar g:crystal#indent#leading_operator_regex
+
+" Auxiliary Functions {{{1
+" ===================
+
+" Check if the character at lnum:col is inside a string, comment, or is ascii.
+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 at lnum:col is inside a string or character.
+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 or regexp
+" delimiter
+function! crystal#indent#IsInStringDelimiter(lnum, col) abort
+ return synIDattr(synID(a:lnum, a:col, 1), 'name') =~# '\<crystal\%(StringDelimiter\|RegexpDelimiter\)\>'
+endfunction
+
+" Find line above 'lnum' that isn't empty, in a comment, or in a string.
+function! crystal#indent#PrevNonBlankNonString(lnum) abort
+ let lnum = prevnonblank(a:lnum)
+
+ while lnum > 0
+ let line = getline(lnum)
+ let start = match(line, '\S')
+
+ if !crystal#indent#IsInStringOrComment(lnum, start + 1)
+ 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 msl = a:lnum
+ let msl_body = getline(msl)
+ let lnum = crystal#indent#PrevNonBlankNonString(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 line = getline(lnum)
+
+ if crystal#indent#Match(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 msl = lnum
+ elseif crystal#indent#Match(lnum, g:crystal#indent#splat_regex)
+ " If the above line looks like the "*" of a splat, use the current one's
+ " indentation.
+ "
+ " Example:
+ " Hash[*
+ " method_call do
+ " something
+ "
+ return msl
+ elseif crystal#indent#Match(lnum, g:crystal#indent#non_bracket_continuation_regex) &&
+ \ crystal#indent#Match(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 MSL.
+ "
+ " Example:
+ " method_call one,
+ " two,
+ " three
+ "
+ let msl = lnum
+ elseif crystal#indent#Match(lnum, g:crystal#indent#non_bracket_continuation_regex) &&
+ \ (
+ \ crystal#indent#Match(msl, g:crystal#indent#bracket_continuation_regex) ||
+ \ crystal#indent#Match(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, respect the previous' indentation,
+ " and stop here.
+ "
+ " Example:
+ " method_call one,
+ " two {
+ " three
+ "
+ return lnum
+ elseif crystal#indent#Match(lnum, g:crystal#indent#bracket_continuation_regex) &&
+ \ (
+ \ crystal#indent#Match(msl, g:crystal#indent#bracket_continuation_regex) ||
+ \ crystal#indent#Match(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 msl
+ elseif crystal#indent#Match(lnum, g:crystal#indent#block_regex) &&
+ \ !crystal#indent#Match(msl, g:crystal#indent#continuation_regex) &&
+ \ !crystal#indent#Match(msl, g:crystal#indent#block_continuation_regex)
+ " If the previous line is a block-starter and the current one is
+ " mostly ordinary, use the current one as the MSL.
+ "
+ " Example:
+ " method_call do
+ " something
+ " something_else
+ return msl
+ else
+ let col = match(line, g:crystal#indent#continuation_regex) + 1
+
+ if (col > 0 && !crystal#indent#IsInStringOrComment(lnum, col))
+ \ || crystal#indent#IsInString(lnum, strlen(line))
+ let msl = lnum
+ else
+ break
+ endif
+ endif
+
+ let msl_body = getline(msl)
+ let lnum = crystal#indent#PrevNonBlankNonString(lnum - 1)
+ endwhile
+
+ return msl
+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, '[][(){}]', 0)
+
+ " 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 regex = '\C'.a:regex
+
+ let line = getline(a:lnum)
+ let col = match(line, regex) + 1
+
+ while col &&
+ \ crystal#indent#IsInStringOrComment(a:lnum, col) ||
+ \ crystal#indent#IsInStringDelimiter(a:lnum, col)
+ let col = match(line, regex, col) + 1
+ endwhile
+
+ return col
+endfunction
+
+" Locates the containing class/module/struct/enum/lib's definition line,
+" ignoring nested classes along the way.
+function! crystal#indent#FindContainingClass() abort
+ let saved_position = getcurpos()
+
+ while searchpair(
+ \ g:crystal#indent#end_start_regex,
+ \ g:crystal#indent#end_middle_regex,
+ \ g:crystal#indent#end_end_regex,
+ \ 'bWz',
+ \ g:crystal#indent#skip_expr) > 0
+ if expand('<cword>') =~# '\<\%(class\|module\|struct\|enum\|lib\)\>'
+ let found_lnum = line('.')
+ call setpos('.', saved_position)
+ return found_lnum
+ endif
+ endwhile
+
+ call setpos('.', saved_position)
+ return 0
+endfunction
+
+endif