diff options
Diffstat (limited to 'indent/ecrystal.vim')
-rw-r--r-- | indent/ecrystal.vim | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/indent/ecrystal.vim b/indent/ecrystal.vim new file mode 100644 index 00000000..adf64e89 --- /dev/null +++ b/indent/ecrystal.vim @@ -0,0 +1,489 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1 + +" Setup {{{1 +" ===== + +if exists('b:did_indent') + finish +endif + +call ecrystal#SetSubtype() + +if b:ecrystal_subtype !=# '' + exec 'runtime! indent/'.b:ecrystal_subtype.'.vim' + unlet! b:did_indent +endif + +if &l:indentexpr ==# '' + if &l:cindent + let &l:indentexpr = 'cindent(v:lnum)' + else + let &l:indentexpr = 'indent(prevnonblank(v:lnum - 1))' + endif +endif + +let b:ecrystal_subtype_indentexpr = &l:indentexpr + +" Should we use folding? +if has('folding') && get(g:, 'ecrystal_fold', 0) + setlocal foldmethod=expr + setlocal foldexpr=GetEcrystalFold() +endif + +" Should closing control tags be aligned with their corresponding +" opening tags? +if !exists('b:ecrystal_align_end') + if exists('g:ecrystal_align_end') + let b:ecrystal_align_end = g:ecrystal_align_end + else + let b:ecrystal_align_end = b:ecrystal_subtype !=# 'html' && b:ecrystal_subtype !=# 'xml' + endif +endif + +" Should multiline tags be indented? +if !exists('b:ecrystal_indent_multiline') + let b:ecrystal_indent_multiline = get(g:, 'ecrystal_indent_multiline', 1) +endif + +if b:ecrystal_indent_multiline + runtime! indent/crystal.vim + unlet! b:did_indent + setlocal indentexpr< +endif + +setlocal indentexpr=GetEcrystalIndent() +setlocal indentkeys+=<>>,=end,=else,=elsif,=rescue,=ensure,=when + +let b:did_indent = 1 + +" Only define the function once. +if exists('*GetEcrystalIndent') + finish +endif + +" Helper variables and functions {{{1 +" ============================== + +let s:ecr_open = '<%%\@!' +let s:ecr_close = '%>' + +let s:ecr_control_open = '<%%\@!-\=[=#]\@!' +let s:ecr_comment_open = '<%%\@!-\=#' + +let s:ecr_indent_regex = + \ '\<\%(if\|unless\|else\|elsif\|case\|for\|when\|while\|until\|begin\|do\|rescue\|ensure\|' . + \ 'class\|module\|struct\|lib\|enum\|union\)\>' + +let s:ecr_dedent_regex = + \ '\<\%(end\|else\|elsif\|when\|rescue\|ensure\)\>' + +" Return the value of a single shift-width +if exists('*shiftwidth') + let s:sw = function('shiftwidth') +else + function s:sw() + return &shiftwidth + endfunction +endif + +" Does the given pattern match at the cursor's position? +function s:MatchCursor(pattern) + return searchpos(a:pattern, 'cnz', line('.')) == [line('.'), col('.')] +endfunction + +" Does the given pattern match at the given position? +function s:MatchAt(lnum, col, pattern) + let pos = getcurpos() + + try + call cursor(a:lnum, a:col) + let result = s:MatchCursor(a:pattern) + finally + call setpos('.', pos) + endtry + + return result +endfunction + +" Is the cell at the given position part of a tag? If so, return the +" position of the opening delimiter. +function s:MatchECR(...) + if a:0 + let lnum = a:1 + let col = a:2 + + call cursor(lnum, col) + endif + + let pos = getcurpos() + + try + let flags = s:MatchCursor(s:ecr_open) ? 'bcWz' : 'bWz' + + let [open_lnum, open_col] = searchpairpos( + \ s:ecr_open, '', s:ecr_close, + \ flags, g:crystal#indent#skip_expr) + finally + call setpos('.', pos) + endtry + + return [open_lnum, open_col] +endfunction + +" If the cell at the given position is part of a control tag, return the +" respective positions of the opening and closing delimiters for that +" tag. +function s:MatchECRControl(...) + let pos = getcurpos() + + if a:0 + let lnum = a:1 + let col = a:2 + + call cursor(lnum, col) + else + let [lnum, col] = [line('.'), col('.')] + endif + + let open = { 'lnum': 0, 'col': 0 } + let close = { 'lnum': 0, 'col': 0 } + + let [open.lnum, open.col] = s:MatchECR(lnum, col) + + if !open.lnum + call setpos('.', pos) + return [open, close] + endif + + call cursor(open.lnum, open.col) + + if !s:MatchCursor(s:ecr_control_open) + let open.lnum = 0 + let open.col = 0 + + call setpos('.', pos) + return [open, close] + endif + + let [close.lnum, close.col] = searchpairpos( + \ s:ecr_control_open, '', s:ecr_close, + \ 'Wz', g:crystal#indent#skip_expr) + + call setpos('.', pos) + return [open, close] +endfunction + +" Determine whether or not the control tag at the given position starts +" an indent. +function s:ECRIndent(...) + if a:0 + if type(a:1) == 0 + let [open, close] = s:MatchECRControl(a:1, a:2) + elseif type(a:1) == 4 + let [open, close] = [a:1, a:2] + endif + else + let [open, close] = s:MatchECRControl() + endif + + let result = 0 + + if !open.lnum + return result + endif + + let pos = getcurpos() + + call cursor(open.lnum, open.col) + + " Find each Crystal keyword that starts an indent; if any of them do + " not have a corresponding ending keyword, then this tag starts an + " indent. + while search(s:ecr_indent_regex, 'z', close.lnum) + let [lnum, col] = [line('.'), col('.')] + + if lnum == close.lnum && col > close.col + break + endif + + if crystal#indent#IsInStringOrComment(lnum, col) + continue + endif + + let [end_lnum, end_col] = searchpairpos( + \ g:crystal#indent#end_start_regex, + \ g:crystal#indent#end_middle_regex, + \ g:crystal#indent#end_end_regex, + \ 'nz', + \ g:crystal#indent#skip_expr, + \ close.lnum) + + if end_lnum + if end_lnum == close.lnum && end_col > close.col + let result = 1 + endif + else + let result = 1 + endif + + if result + break + endif + endwhile + + call setpos('.', pos) + return result +endfunction + +" Determine if the control tag at the given position ends an indent or +" not. +function s:ECRDedent(...) + if a:0 + if type(a:1) == 0 + let [open, close] = s:MatchECRControl(a:1, a:2) + elseif type(a:1) == 4 + let [open, close] = [a:1, a:2] + endif + else + let [open, close] = s:MatchECRControl() + endif + + let result = 0 + + if !open.lnum + return result + endif + + let pos = getcurpos() + + call cursor(open.lnum, open.col) + + " Find each Crystal keyword that ends an indent; if any of them do not + " have a corresponding starting keyword, then this tag ends an indent + while search(s:ecr_dedent_regex, 'z', close.lnum) + let [lnum, col] = [line('.'), col('.')] + + if lnum == close.lnum && col > close.col + break + endif + + if crystal#indent#IsInStringOrComment(lnum, col) + continue + endif + + let [begin_lnum, begin_col] = searchpairpos( + \ g:crystal#indent#end_start_regex, + \ g:crystal#indent#end_middle_regex, + \ g:crystal#indent#end_end_regex, + \ 'bnz', + \ g:crystal#indent#skip_expr, + \ open.lnum) + + if begin_lnum + if begin_lnum == open.lnum && begin_col < open.col + let result = 1 + endif + else + let result = 1 + endif + + if result + break + endif + endwhile + + call setpos('.', pos) + return result +endfunction + +" Find and match a control tag in the given line, if one exists. +function s:FindECRControl(...) + let lnum = a:0 ? a:1 : line('.') + + let open = { 'lnum': 0, 'col': 0 } + let close = { 'lnum': 0, 'col': 0 } + + let pos = getcurpos() + + call cursor(lnum, 1) + + while search(s:ecr_control_open, 'cz', lnum) + let [open, close] = s:MatchECRControl() + + if open.lnum + break + endif + endwhile + + call setpos('.', pos) + return [open, close] +endfunction + +" Find and match the previous control tag. +" +" This takes two arguments: the first is the line to start searching +" from (exclusive); the second is the line to stop searching at +" (inclusive). +function s:FindPrevECRControl(...) + if a:0 == 0 + let start = line('.') + let stop = 1 + elseif a:0 == 1 + let start = a:1 + let stop = 1 + elseif a:0 == 2 + let start = a:1 + let stop = a:2 + endif + + let open = { 'lnum': 0, 'col': 0 } + let close = { 'lnum': 0, 'col': 0 } + + let pos = getcurpos() + + call cursor(start, 1) + + let [lnum, col] = searchpos(s:ecr_close, 'bWz', stop) + + if !lnum + call setpos('.', pos) + return [open, close] + endif + + let [open, close] = s:MatchECRControl() + + while !open.lnum + let [lnum, col] = searchpos(s:ecr_close, 'bWz', stop) + + if !lnum + break + endif + + let [open, close] = s:MatchECRControl() + endwhile + + call setpos('.', pos) + return [open, close] +endfunction + +" GetEcrystalIndent {{{1 +" ================= + +function GetEcrystalIndent() abort + let prev_lnum = prevnonblank(v:lnum - 1) + + if b:ecrystal_indent_multiline + let [open_tag_lnum, open_tag_col] = s:MatchECR() + else + let open_tag_lnum = 0 + endif + + if open_tag_lnum && open_tag_lnum < v:lnum + " If we are inside a multiline eCrystal tag... + + let ind = indent(open_tag_lnum) + + " If this line has a closing delimiter that isn't inside a string or + " comment, then we are done with this tag + if crystal#indent#Match(v:lnum, s:ecr_close) + return ind + endif + + " All tag contents will have at least one indent + let ind += s:sw() + + if open_tag_lnum == prev_lnum + " If this is the first line after the opening delimiter, then one + " indent is enough + return ind + elseif s:MatchAt(open_tag_lnum, open_tag_col, s:ecr_comment_open)[0] + " eCrystal comments shouldn't be indented any further + return ind + else + " Else, fall back to Crystal indentation... + let crystal_ind = GetCrystalIndent() + + " But only if it isn't less than the default indentation for this + " tag + return crystal_ind < ind ? ind : crystal_ind + endif + else + let [prev_ecr_open, prev_ecr_close] = s:FindPrevECRControl(v:lnum, prev_lnum) + let [curr_ecr_open, curr_ecr_close] = s:FindECRControl() + + let prev_is_ecr = prev_ecr_open.lnum != 0 + let curr_is_ecr = curr_ecr_open.lnum != 0 + + let shift = 0 + + if prev_is_ecr + if s:ECRIndent(prev_ecr_open, prev_ecr_close) + let shift = 1 + endif + endif + + if curr_is_ecr + if s:ECRDedent(curr_ecr_open, curr_ecr_close) + if !b:ecrystal_align_end + let shift = shift ? 0 : -1 + else + " Find the nearest previous control tag that starts an indent + " and align this one with that one + let end_tags = 0 + + let [open, close] = s:FindPrevECRControl() + + while open.lnum + if s:ECRIndent(open, close) + if end_tags + let end_tags -= 1 + else + return indent(open.lnum) + endif + elseif s:ECRDedent(open, close) + let end_tags += 1 + endif + + let [open, close] = s:FindPrevECRControl(open.lnum) + endwhile + endif + endif + endif + + if shift + return indent(prev_lnum) + s:sw() * shift + else + exec 'return ' . b:ecrystal_subtype_indentexpr + endif + endif +endfunction + +" GetEcrystalFold {{{1 +" =============== + +function GetEcrystalFold() abort + let fold = '=' + + let col = crystal#indent#Match(v:lnum, s:ecr_control_open) + + if col + let [open, close] = s:MatchECRControl(v:lnum, col) + + let starts_indent = s:ECRIndent(open, close) + let ends_indent = s:ECRDedent(open, close) + + if starts_indent && !ends_indent + let fold = 'a1' + elseif ends_indent && !starts_indent + let fold = 's1' + endif + endif + + return fold +endfunction + +" }}} + +" vim:fdm=marker + +endif |