coffee.vim 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. " Language: CoffeeScript
  2. " Maintainer: Mick Koch <kchmck@gmail.com>
  3. " URL: http://github.com/kchmck/vim-coffee-script
  4. " License: WTFPL
  5. if exists("b:did_indent")
  6. finish
  7. endif
  8. let b:did_indent = 1
  9. setlocal autoindent
  10. setlocal indentexpr=GetCoffeeIndent(v:lnum)
  11. " Make sure GetCoffeeIndent is run when these are typed so they can be
  12. " indented or outdented.
  13. setlocal indentkeys+=0],0),0.,=else,=when,=catch,=finally
  14. " Only define the function once.
  15. if exists("*GetCoffeeIndent")
  16. finish
  17. endif
  18. " Keywords to indent after
  19. let s:INDENT_AFTER_KEYWORD = '^\%(if\|unless\|else\|for\|while\|until\|'
  20. \ . 'loop\|switch\|when\|try\|catch\|finally\|'
  21. \ . 'class\)\>'
  22. " Operators to indent after
  23. let s:INDENT_AFTER_OPERATOR = '\%([([{:=]\|[-=]>\)$'
  24. " Keywords and operators that continue a line
  25. let s:CONTINUATION = '\<\%(is\|isnt\|and\|or\)\>$'
  26. \ . '\|'
  27. \ . '\%(-\@<!-\|+\@<!+\|<\|[-=]\@<!>\|\*\|/\@<!/\|%\||\|'
  28. \ . '&\|,\|\.\@<!\.\)$'
  29. " Operators that block continuation indenting
  30. let s:CONTINUATION_BLOCK = '[([{:=]$'
  31. " A continuation dot access
  32. let s:DOT_ACCESS = '^\.'
  33. " Keywords to outdent after
  34. let s:OUTDENT_AFTER = '^\%(return\|break\|continue\|throw\)\>'
  35. " A compound assignment like `... = if ...`
  36. let s:COMPOUND_ASSIGNMENT = '[:=]\s*\%(if\|unless\|for\|while\|until\|'
  37. \ . 'switch\|try\|class\)\>'
  38. " A postfix condition like `return ... if ...`.
  39. let s:POSTFIX_CONDITION = '\S\s\+\zs\<\%(if\|unless\)\>'
  40. " A single-line else statement like `else ...` but not `else if ...
  41. let s:SINGLE_LINE_ELSE = '^else\s\+\%(\<\%(if\|unless\)\>\)\@!'
  42. " Max lines to look back for a match
  43. let s:MAX_LOOKBACK = 50
  44. " Syntax names for strings
  45. let s:SYNTAX_STRING = 'coffee\%(String\|AssignString\|Embed\|Regex\|Heregex\|'
  46. \ . 'Heredoc\)'
  47. " Syntax names for comments
  48. let s:SYNTAX_COMMENT = 'coffee\%(Comment\|BlockComment\|HeregexComment\)'
  49. " Syntax names for strings and comments
  50. let s:SYNTAX_STRING_COMMENT = s:SYNTAX_STRING . '\|' . s:SYNTAX_COMMENT
  51. " Get the linked syntax name of a character.
  52. function! s:SyntaxName(linenum, col)
  53. return synIDattr(synID(a:linenum, a:col, 1), 'name')
  54. endfunction
  55. " Check if a character is in a comment.
  56. function! s:IsComment(linenum, col)
  57. return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_COMMENT
  58. endfunction
  59. " Check if a character is in a string.
  60. function! s:IsString(linenum, col)
  61. return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_STRING
  62. endfunction
  63. " Check if a character is in a comment or string.
  64. function! s:IsCommentOrString(linenum, col)
  65. return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_STRING_COMMENT
  66. endfunction
  67. " Check if a whole line is a comment.
  68. function! s:IsCommentLine(linenum)
  69. " Check the first non-whitespace character.
  70. return s:IsComment(a:linenum, indent(a:linenum) + 1)
  71. endfunction
  72. " Repeatedly search a line for a regex until one is found outside a string or
  73. " comment.
  74. function! s:SmartSearch(linenum, regex)
  75. " Start at the first column.
  76. let col = 0
  77. " Search until there are no more matches, unless a good match is found.
  78. while 1
  79. call cursor(a:linenum, col + 1)
  80. let [_, col] = searchpos(a:regex, 'cn', a:linenum)
  81. " No more matches.
  82. if !col
  83. break
  84. endif
  85. if !s:IsCommentOrString(a:linenum, col)
  86. return 1
  87. endif
  88. endwhile
  89. " No good match found.
  90. return 0
  91. endfunction
  92. " Skip a match if it's in a comment or string, is a single-line statement that
  93. " isn't adjacent, or is a postfix condition.
  94. function! s:ShouldSkip(startlinenum, linenum, col)
  95. if s:IsCommentOrString(a:linenum, a:col)
  96. return 1
  97. endif
  98. " Check for a single-line statement that isn't adjacent.
  99. if s:SmartSearch(a:linenum, '\<then\>') && a:startlinenum - a:linenum > 1
  100. return 1
  101. endif
  102. if s:SmartSearch(a:linenum, s:POSTFIX_CONDITION) &&
  103. \ !s:SmartSearch(a:linenum, s:COMPOUND_ASSIGNMENT)
  104. return 1
  105. endif
  106. return 0
  107. endfunction
  108. " Find the farthest line to look back to, capped to line 1 (zero and negative
  109. " numbers cause bad things).
  110. function! s:MaxLookback(startlinenum)
  111. return max([1, a:startlinenum - s:MAX_LOOKBACK])
  112. endfunction
  113. " Get the skip expression for searchpair().
  114. function! s:SkipExpr(startlinenum)
  115. return "s:ShouldSkip(" . a:startlinenum . ", line('.'), col('.'))"
  116. endfunction
  117. " Search for pairs of text.
  118. function! s:SearchPair(start, end)
  119. " The cursor must be in the first column for regexes to match.
  120. call cursor(0, 1)
  121. let startlinenum = line('.')
  122. " Don't need the W flag since MaxLookback caps the search to line 1.
  123. return searchpair(a:start, '', a:end, 'bcn',
  124. \ s:SkipExpr(startlinenum),
  125. \ s:MaxLookback(startlinenum))
  126. endfunction
  127. " Try to find a previous matching line.
  128. function! s:GetMatch(curline)
  129. let firstchar = a:curline[0]
  130. if firstchar == '}'
  131. return s:SearchPair('{', '}')
  132. elseif firstchar == ')'
  133. return s:SearchPair('(', ')')
  134. elseif firstchar == ']'
  135. return s:SearchPair('\[', '\]')
  136. elseif a:curline =~ '^else\>'
  137. return s:SearchPair('\<\%(if\|unless\|when\)\>', '\<else\>')
  138. elseif a:curline =~ '^catch\>'
  139. return s:SearchPair('\<try\>', '\<catch\>')
  140. elseif a:curline =~ '^finally\>'
  141. return s:SearchPair('\<try\>', '\<finally\>')
  142. endif
  143. return 0
  144. endfunction
  145. " Get the nearest previous line that isn't a comment.
  146. function! s:GetPrevNormalLine(startlinenum)
  147. let curlinenum = a:startlinenum
  148. while curlinenum > 0
  149. let curlinenum = prevnonblank(curlinenum - 1)
  150. if !s:IsCommentLine(curlinenum)
  151. return curlinenum
  152. endif
  153. endwhile
  154. return 0
  155. endfunction
  156. " Try to find a comment in a line.
  157. function! s:FindComment(linenum)
  158. let col = 0
  159. while 1
  160. call cursor(a:linenum, col + 1)
  161. let [_, col] = searchpos('#', 'cn', a:linenum)
  162. if !col
  163. break
  164. endif
  165. if s:IsComment(a:linenum, col)
  166. return col
  167. endif
  168. endwhile
  169. return 0
  170. endfunction
  171. " Get a line without comments or surrounding whitespace.
  172. function! s:GetTrimmedLine(linenum)
  173. let comment = s:FindComment(a:linenum)
  174. let line = getline(a:linenum)
  175. if comment
  176. " Subtract 1 to get to the column before the comment and another 1 for
  177. " zero-based indexing.
  178. let line = line[:comment - 2]
  179. endif
  180. return substitute(substitute(line, '^\s\+', '', ''),
  181. \ '\s\+$', '', '')
  182. endfunction
  183. function! s:GetCoffeeIndent(curlinenum)
  184. let prevlinenum = s:GetPrevNormalLine(a:curlinenum)
  185. " Don't do anything if there's no previous line.
  186. if !prevlinenum
  187. return -1
  188. endif
  189. let curline = s:GetTrimmedLine(a:curlinenum)
  190. " Try to find a previous matching statement. This handles outdenting.
  191. let matchlinenum = s:GetMatch(curline)
  192. if matchlinenum
  193. return indent(matchlinenum)
  194. endif
  195. " Try to find a matching `when`.
  196. if curline =~ '^when\>' && !s:SmartSearch(prevlinenum, '\<switch\>')
  197. let linenum = a:curlinenum
  198. while linenum > 0
  199. let linenum = s:GetPrevNormalLine(linenum)
  200. if getline(linenum) =~ '^\s*when\>'
  201. return indent(linenum)
  202. endif
  203. endwhile
  204. return -1
  205. endif
  206. let prevline = s:GetTrimmedLine(prevlinenum)
  207. let previndent = indent(prevlinenum)
  208. " Always indent after these operators.
  209. if prevline =~ s:INDENT_AFTER_OPERATOR
  210. return previndent + &shiftwidth
  211. endif
  212. " Indent after a continuation if it's the first.
  213. if prevline =~ s:CONTINUATION
  214. " If the line ends in a slash, make sure it isn't a regex.
  215. if prevline =~ '/$'
  216. " Move to the line so we can get the last column.
  217. call cursor(prevlinenum)
  218. if s:IsString(prevlinenum, col('$') - 1)
  219. return -1
  220. endif
  221. endif
  222. let prevprevlinenum = s:GetPrevNormalLine(prevlinenum)
  223. " If the continuation is the first in the file, don't run the other checks.
  224. if !prevprevlinenum
  225. return previndent + &shiftwidth
  226. endif
  227. let prevprevline = s:GetTrimmedLine(prevprevlinenum)
  228. if prevprevline !~ s:CONTINUATION && prevprevline !~ s:CONTINUATION_BLOCK
  229. return previndent + &shiftwidth
  230. endif
  231. return -1
  232. endif
  233. " Indent after these keywords and compound assignments if they aren't a
  234. " single-line statement.
  235. if prevline =~ s:INDENT_AFTER_KEYWORD || prevline =~ s:COMPOUND_ASSIGNMENT
  236. if !s:SmartSearch(prevlinenum, '\<then\>') && prevline !~ s:SINGLE_LINE_ELSE
  237. return previndent + &shiftwidth
  238. endif
  239. return -1
  240. endif
  241. " Indent a dot access if it's the first.
  242. if curline =~ s:DOT_ACCESS && prevline !~ s:DOT_ACCESS
  243. return previndent + &shiftwidth
  244. endif
  245. " Outdent after these keywords if they don't have a postfix condition or are
  246. " a single-line statement.
  247. if prevline =~ s:OUTDENT_AFTER
  248. if !s:SmartSearch(prevlinenum, s:POSTFIX_CONDITION) ||
  249. \ s:SmartSearch(prevlinenum, '\<then\>')
  250. return previndent - &shiftwidth
  251. endif
  252. endif
  253. " No indenting or outdenting is needed.
  254. return -1
  255. endfunction
  256. " Wrap s:GetCoffeeIndent to keep the cursor position.
  257. function! GetCoffeeIndent(curlinenum)
  258. let oldcursor = getpos('.')
  259. let indent = s:GetCoffeeIndent(a:curlinenum)
  260. call setpos('.', oldcursor)
  261. return indent
  262. endfunction