summaryrefslogtreecommitdiffstats
path: root/indent/coffee.vim
diff options
context:
space:
mode:
Diffstat (limited to 'indent/coffee.vim')
-rw-r--r--indent/coffee.vim418
1 files changed, 418 insertions, 0 deletions
diff --git a/indent/coffee.vim b/indent/coffee.vim
new file mode 100644
index 00000000..49953151
--- /dev/null
+++ b/indent/coffee.vim
@@ -0,0 +1,418 @@
+" Language: CoffeeScript
+" Maintainer: Mick Koch <kchmck@gmail.com>
+" URL: http://github.com/kchmck/vim-coffee-script
+" License: WTFPL
+
+if exists('b:did_indent')
+ finish
+endif
+
+let b:did_indent = 1
+
+setlocal autoindent
+setlocal indentexpr=GetCoffeeIndent(v:lnum)
+" Make sure GetCoffeeIndent is run when these are typed so they can be
+" indented or outdented.
+setlocal indentkeys+=0],0),0.,=else,=when,=catch,=finally
+
+" If no indenting or outdenting is needed, either keep the indent of the cursor
+" (use autoindent) or match the indent of the previous line.
+if exists('g:coffee_indent_keep_current')
+ let s:DEFAULT_LEVEL = '-1'
+else
+ let s:DEFAULT_LEVEL = 'indent(prevnlnum)'
+endif
+
+" Only define the function once.
+if exists('*GetCoffeeIndent')
+ finish
+endif
+
+" Keywords that begin a block
+let s:BEGIN_BLOCK_KEYWORD = '\C^\%(if\|unless\|else\|for\|while\|until\|'
+\ . 'loop\|switch\|when\|try\|catch\|finally\|'
+\ . 'class\)\>\%(\s*:\)\@!'
+
+" An expression that uses the result of a statement
+let s:COMPOUND_EXPRESSION = '\C\%([^-]-\|[^+]+\|[^/]/\|[:=*%&|^<>]\)\s*'
+\ . '\%(if\|unless\|for\|while\|until\|loop\|switch\|'
+\ . 'try\|class\)\>'
+
+" Combine the two above
+let s:BEGIN_BLOCK = s:BEGIN_BLOCK_KEYWORD . '\|' . s:COMPOUND_EXPRESSION
+
+" Operators that begin a block but also count as a continuation
+let s:BEGIN_BLOCK_OP = '[([{:=]$'
+
+" Begins a function block
+let s:FUNCTION = '[-=]>$'
+
+" Operators that continue a line onto the next line
+let s:CONTINUATION_OP = '\C\%(\<\%(is\|isnt\|and\|or\)\>\|'
+\ . '[^-]-\|[^+]+\|[^-=]>\|[^.]\.\|[<*/%&|^,]\)$'
+
+" Ancestor operators that prevent continuation indenting
+let s:CONTINUATION = s:CONTINUATION_OP . '\|' . s:BEGIN_BLOCK_OP
+
+" A closing bracket by itself on a line followed by a continuation
+let s:BRACKET_CONTINUATION = '^\s*[}\])]\s*' . s:CONTINUATION_OP
+
+" A continuation dot access
+let s:DOT_ACCESS = '^\.'
+
+" Keywords that break out of a block
+let s:BREAK_BLOCK_OP = '\C^\%(return\|break\|continue\|throw\)\>'
+
+" A condition attached to the end of a statement
+let s:POSTFIX_CONDITION = '\C\S\s\+\zs\<\%(if\|unless\|when\|while\|until\)\>'
+
+" A then contained in brackets
+let s:CONTAINED_THEN = '\C[(\[].\{-}\<then\>.\{-\}[)\]]'
+
+" An else with a condition attached
+let s:ELSE_COND = '\C^\s*else\s\+\<\%(if\|unless\)\>'
+
+" A single-line else statement (without a condition attached)
+let s:SINGLE_LINE_ELSE = '\C^else\s\+\%(\<\%(if\|unless\)\>\)\@!'
+
+" Pairs of starting and ending keywords, with an initial pattern to match
+let s:KEYWORD_PAIRS = [
+\ ['\C^else\>', '\C\<\%(if\|unless\|when\|else\s\+\%(if\|unless\)\)\>',
+\ '\C\<else\>'],
+\ ['\C^catch\>', '\C\<try\>', '\C\<catch\>'],
+\ ['\C^finally\>', '\C\<try\>', '\C\<finally\>']
+\]
+
+" Pairs of starting and ending brackets
+let s:BRACKET_PAIRS = {']': '\[', '}': '{', ')': '('}
+
+" Max lines to look back for a match
+let s:MAX_LOOKBACK = 50
+
+" Syntax names for strings
+let s:SYNTAX_STRING = 'coffee\%(String\|AssignString\|Embed\|Regex\|Heregex\|'
+\ . 'Heredoc\)'
+
+" Syntax names for comments
+let s:SYNTAX_COMMENT = 'coffee\%(Comment\|BlockComment\|HeregexComment\)'
+
+" Syntax names for strings and comments
+let s:SYNTAX_STRING_COMMENT = s:SYNTAX_STRING . '\|' . s:SYNTAX_COMMENT
+
+" Get the linked syntax name of a character.
+function! s:SyntaxName(lnum, col)
+ return synIDattr(synID(a:lnum, a:col, 1), 'name')
+endfunction
+
+" Check if a character is in a comment.
+function! s:IsComment(lnum, col)
+ return s:SyntaxName(a:lnum, a:col) =~ s:SYNTAX_COMMENT
+endfunction
+
+" Check if a character is in a string.
+function! s:IsString(lnum, col)
+ return s:SyntaxName(a:lnum, a:col) =~ s:SYNTAX_STRING
+endfunction
+
+" Check if a character is in a comment or string.
+function! s:IsCommentOrString(lnum, col)
+ return s:SyntaxName(a:lnum, a:col) =~ s:SYNTAX_STRING_COMMENT
+endfunction
+
+" Search a line for a regex until one is found outside a string or comment.
+function! s:SearchCode(lnum, regex)
+ " Start at the first column and look for an initial match (including at the
+ " cursor.)
+ call cursor(a:lnum, 1)
+ let pos = search(a:regex, 'c', a:lnum)
+
+ while pos
+ if !s:IsCommentOrString(a:lnum, col('.'))
+ return 1
+ endif
+
+ " Move to the match and continue searching (don't accept matches at the
+ " cursor.)
+ let pos = search(a:regex, '', a:lnum)
+ endwhile
+
+ return 0
+endfunction
+
+" Search for the nearest previous line that isn't a comment.
+function! s:GetPrevNormalLine(startlnum)
+ let curlnum = a:startlnum
+
+ while curlnum
+ let curlnum = prevnonblank(curlnum - 1)
+
+ " Return the line if the first non-whitespace character isn't a comment.
+ if !s:IsComment(curlnum, indent(curlnum) + 1)
+ return curlnum
+ endif
+ endwhile
+
+ return 0
+endfunction
+
+function! s:SearchPair(startlnum, lookback, skip, open, close)
+ " Go to the first column so a:close will be matched even if it's at the
+ " beginning of the line.
+ call cursor(a:startlnum, 1)
+ return searchpair(a:open, '', a:close, 'bnW', a:skip, max([1, a:lookback]))
+endfunction
+
+" Skip if a match
+" - is in a string or comment
+" - is a single-line statement that isn't immediately
+" adjacent
+" - has a postfix condition and isn't an else statement or compound
+" expression
+function! s:ShouldSkip(startlnum, lnum, col)
+ return s:IsCommentOrString(a:lnum, a:col) ||
+ \ s:SearchCode(a:lnum, '\C\<then\>') && a:startlnum - a:lnum > 1 ||
+ \ s:SearchCode(a:lnum, s:POSTFIX_CONDITION) &&
+ \ getline(a:lnum) !~ s:ELSE_COND &&
+ \ !s:SearchCode(a:lnum, s:COMPOUND_EXPRESSION)
+endfunction
+
+" Search for the nearest and farthest match for a keyword pair.
+function! s:SearchMatchingKeyword(startlnum, open, close)
+ let skip = 's:ShouldSkip(' . a:startlnum . ", line('.'), line('.'))"
+
+ " Search for the nearest match.
+ let nearestlnum = s:SearchPair(a:startlnum, a:startlnum - s:MAX_LOOKBACK,
+ \ skip, a:open, a:close)
+
+ if !nearestlnum
+ return []
+ endif
+
+ " Find the nearest previous line with indent less than or equal to startlnum.
+ let ind = indent(a:startlnum)
+ let lookback = s:GetPrevNormalLine(a:startlnum)
+
+ while lookback && indent(lookback) > ind
+ let lookback = s:GetPrevNormalLine(lookback)
+ endwhile
+
+ " Search for the farthest match. If there are no other matches, then the
+ " nearest match is also the farthest one.
+ let matchlnum = nearestlnum
+
+ while matchlnum
+ let lnum = matchlnum
+ let matchlnum = s:SearchPair(matchlnum, lookback, skip, a:open, a:close)
+ endwhile
+
+ return [nearestlnum, lnum]
+endfunction
+
+" Strip a line of a trailing comment and surrounding whitespace.
+function! s:GetTrimmedLine(lnum)
+ " Try to find a comment starting at the first column.
+ call cursor(a:lnum, 1)
+ let pos = search('#', 'c', a:lnum)
+
+ " Keep searching until a comment is found or search returns 0.
+ while pos
+ if s:IsComment(a:lnum, col('.'))
+ break
+ endif
+
+ let pos = search('#', '', a:lnum)
+ endwhile
+
+ if !pos
+ " No comment was found so use the whole line.
+ let line = getline(a:lnum)
+ else
+ " Subtract 1 to get to the column before the comment and another 1 for
+ " column indexing -> zero-based indexing.
+ let line = getline(a:lnum)[:col('.') - 2]
+ endif
+
+ return substitute(substitute(line, '^\s\+', '', ''),
+ \ '\s\+$', '', '')
+endfunction
+
+" Get the indent policy when no special rules are used.
+function! s:GetDefaultPolicy(curlnum)
+ " Check whether equalprg is being ran on existing lines.
+ if strlen(getline(a:curlnum)) == indent(a:curlnum)
+ " If not indenting an existing line, use the default policy.
+ return s:DEFAULT_LEVEL
+ else
+ " Otherwise let autoindent determine what to do with an existing line.
+ return '-1'
+ endif
+endfunction
+
+function! GetCoffeeIndent(curlnum)
+ " Get the previous non-blank line (may be a comment.)
+ let prevlnum = prevnonblank(a:curlnum - 1)
+
+ " Bail if there's no code before.
+ if !prevlnum
+ return -1
+ endif
+
+ " Bail if inside a multiline string.
+ if s:IsString(a:curlnum, 1)
+ let prevnlnum = prevlnum
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+
+ " Get the code part of the current line.
+ let curline = s:GetTrimmedLine(a:curlnum)
+ " Get the previous non-comment line.
+ let prevnlnum = s:GetPrevNormalLine(a:curlnum)
+
+ " Check if the current line is the closing bracket in a bracket pair.
+ if has_key(s:BRACKET_PAIRS, curline[0])
+ " Search for a matching opening bracket.
+ let matchlnum = s:SearchPair(a:curlnum, a:curlnum - s:MAX_LOOKBACK,
+ \ "s:IsCommentOrString(line('.'), col('.'))",
+ \ s:BRACKET_PAIRS[curline[0]], curline[0])
+
+ if matchlnum
+ " Match the indent of the opening bracket.
+ return indent(matchlnum)
+ else
+ " No opening bracket found (bad syntax), so bail.
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+ endif
+
+ " Check if the current line is the closing keyword in a keyword pair.
+ for pair in s:KEYWORD_PAIRS
+ if curline =~ pair[0]
+ " Find the nearest and farthest matches within the same indent level.
+ let matches = s:SearchMatchingKeyword(a:curlnum, pair[1], pair[2])
+
+ if len(matches)
+ " Don't force indenting/outdenting as long as line is already lined up
+ " with a valid match
+ return max([min([indent(a:curlnum), indent(matches[0])]),
+ \ indent(matches[1])])
+ else
+ " No starting keyword found (bad syntax), so bail.
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+ endif
+ endfor
+
+ " Check if the current line is a `when` and not the first in a switch block.
+ if curline =~ '\C^when\>' && !s:SearchCode(prevnlnum, '\C\<switch\>')
+ " Look back for a `when`.
+ while prevnlnum
+ if getline(prevnlnum) =~ '\C^\s*when\>'
+ " Indent to match the found `when`, but don't force indenting (for when
+ " indenting nested switch blocks.)
+ return min([indent(a:curlnum), indent(prevnlnum)])
+ endif
+
+ let prevnlnum = s:GetPrevNormalLine(prevnlnum)
+ endwhile
+
+ " No matching `when` found (bad syntax), so bail.
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+
+ " If the previous line is a comment, use its indentation, but don't force
+ " indenting.
+ if prevlnum != prevnlnum
+ return min([indent(a:curlnum), indent(prevlnum)])
+ endif
+
+ let prevline = s:GetTrimmedLine(prevnlnum)
+
+ " Always indent after these operators.
+ if prevline =~ s:BEGIN_BLOCK_OP
+ return indent(prevnlnum) + shiftwidth()
+ endif
+
+ " Indent if the previous line starts a function block, but don't force
+ " indenting if the line is non-blank (for empty function bodies.)
+ if prevline =~ s:FUNCTION
+ if strlen(getline(a:curlnum)) > indent(a:curlnum)
+ return min([indent(prevnlnum) + shiftwidth(), indent(a:curlnum)])
+ else
+ return indent(prevnlnum) + shiftwidth()
+ endif
+ endif
+
+ " Check if continuation indenting is needed. If the line ends in a slash, make
+ " sure it isn't a regex.
+ if prevline =~ s:CONTINUATION_OP &&
+ \ !(prevline =~ '/$' && s:IsString(prevnlnum, col([prevnlnum, '$']) - 1))
+ " Don't indent if the continuation follows a closing bracket.
+ if prevline =~ s:BRACKET_CONTINUATION
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+
+ let prevprevnlnum = s:GetPrevNormalLine(prevnlnum)
+
+ " Don't indent if not the first continuation.
+ if prevprevnlnum && s:GetTrimmedLine(prevprevnlnum) =~ s:CONTINUATION
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+
+ " Continuation indenting seems to vary between programmers, so if the line
+ " is non-blank, don't override the indentation
+ if strlen(getline(a:curlnum)) > indent(a:curlnum)
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+
+ " Otherwise indent a level.
+ return indent(prevnlnum) + shiftwidth()
+ endif
+
+ " Check if the previous line starts with a keyword that begins a block.
+ if prevline =~ s:BEGIN_BLOCK
+ " Indent if the current line doesn't start with `then` and the previous line
+ " isn't a single-line statement.
+ if curline !~ '\C^\<then\>' && !s:SearchCode(prevnlnum, '\C\<then\>') &&
+ \ prevline !~ s:SINGLE_LINE_ELSE
+ return indent(prevnlnum) + shiftwidth()
+ else
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+ endif
+
+ " Indent a dot access if it's the first.
+ if curline =~ s:DOT_ACCESS
+ if prevline !~ s:DOT_ACCESS
+ return indent(prevnlnum) + shiftwidth()
+ else
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+ endif
+
+ " Outdent if a keyword breaks out of a block as long as it doesn't have a
+ " postfix condition (and the postfix condition isn't a single-line statement.)
+ if prevline =~ s:BREAK_BLOCK_OP
+ if !s:SearchCode(prevnlnum, s:POSTFIX_CONDITION) ||
+ \ s:SearchCode(prevnlnum, '\C\<then\>') &&
+ \ !s:SearchCode(prevnlnum, s:CONTAINED_THEN)
+ " Don't force indenting.
+ return min([indent(a:curlnum), indent(prevnlnum) - shiftwidth()])
+ else
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+ endif
+ endif
+
+ " Check if inside brackets.
+ let matchlnum = s:SearchPair(a:curlnum, a:curlnum - s:MAX_LOOKBACK,
+ \ "s:IsCommentOrString(line('.'), col('.'))",
+ \ '\[\|(\|{', '\]\|)\|}')
+
+ " If inside brackets, indent relative to the brackets, but don't outdent an
+ " already indented line.
+ if matchlnum
+ return max([indent(a:curlnum), indent(matchlnum) + shiftwidth()])
+ endif
+
+ " No special rules applied, so use the default policy.
+ exec 'return' s:GetDefaultPolicy(a:curlnum)
+endfunction