summaryrefslogtreecommitdiffstats
path: root/indent/ecrystal.vim
diff options
context:
space:
mode:
Diffstat (limited to 'indent/ecrystal.vim')
-rw-r--r--indent/ecrystal.vim489
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