Simple image host.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

coffee.vim 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. " Language: CoffeeScript
  2. " Maintainer: Mick Koch <mick@kochm.co>
  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. " If no indenting or outdenting is needed, either keep the indent of the cursor
  15. " (use autoindent) or match the indent of the previous line.
  16. if exists('g:coffee_indent_keep_current')
  17. let s:DEFAULT_LEVEL = '-1'
  18. else
  19. let s:DEFAULT_LEVEL = 'indent(prevnlnum)'
  20. endif
  21. " Only define the function once.
  22. if exists('*GetCoffeeIndent')
  23. finish
  24. endif
  25. " Keywords that begin a block
  26. let s:BEGIN_BLOCK_KEYWORD = '\C^\%(if\|unless\|else\|for\|while\|until\|'
  27. \ . 'loop\|switch\|when\|try\|catch\|finally\|'
  28. \ . 'class\)\>\%(\s*:\)\@!'
  29. " An expression that uses the result of a statement
  30. let s:COMPOUND_EXPRESSION = '\C\%([^-]-\|[^+]+\|[^/]/\|[:=*%&|^<>]\)\s*'
  31. \ . '\%(if\|unless\|for\|while\|until\|loop\|switch\|'
  32. \ . 'try\|class\)\>'
  33. " Combine the two above
  34. let s:BEGIN_BLOCK = s:BEGIN_BLOCK_KEYWORD . '\|' . s:COMPOUND_EXPRESSION
  35. " Operators that begin a block but also count as a continuation
  36. let s:BEGIN_BLOCK_OP = '[([{:=]$'
  37. " Begins a function block
  38. let s:FUNCTION = '[-=]>$'
  39. " Operators that continue a line onto the next line
  40. let s:CONTINUATION_OP = '\C\%(\<\%(is\|isnt\|and\|or\)\>\|'
  41. \ . '[^-]-\|[^+]+\|[^-=]>\|[^.]\.\|[<*/%&|^,]\)$'
  42. " Ancestor operators that prevent continuation indenting
  43. let s:CONTINUATION = s:CONTINUATION_OP . '\|' . s:BEGIN_BLOCK_OP
  44. " A closing bracket by itself on a line followed by a continuation
  45. let s:BRACKET_CONTINUATION = '^\s*[}\])]\s*' . s:CONTINUATION_OP
  46. " A continuation dot access
  47. let s:DOT_ACCESS = '^\.'
  48. " Keywords that break out of a block
  49. let s:BREAK_BLOCK_OP = '\C^\%(return\|break\|continue\|throw\)\>'
  50. " A condition attached to the end of a statement
  51. let s:POSTFIX_CONDITION = '\C\S\s\+\zs\<\%(if\|unless\|when\|while\|until\)\>'
  52. " A then contained in brackets
  53. let s:CONTAINED_THEN = '\C[(\[].\{-}\<then\>.\{-\}[)\]]'
  54. " An else with a condition attached
  55. let s:ELSE_COND = '\C^\s*else\s\+\<\%(if\|unless\)\>'
  56. " A single-line else statement (without a condition attached)
  57. let s:SINGLE_LINE_ELSE = '\C^else\s\+\%(\<\%(if\|unless\)\>\)\@!'
  58. " Pairs of starting and ending keywords, with an initial pattern to match
  59. let s:KEYWORD_PAIRS = [
  60. \ ['\C^else\>', '\C\<\%(if\|unless\|when\|else\s\+\%(if\|unless\)\)\>',
  61. \ '\C\<else\>'],
  62. \ ['\C^catch\>', '\C\<try\>', '\C\<catch\>'],
  63. \ ['\C^finally\>', '\C\<try\>', '\C\<finally\>']
  64. \]
  65. " Pairs of starting and ending brackets
  66. let s:BRACKET_PAIRS = {']': '\[', '}': '{', ')': '('}
  67. " Max lines to look back for a match
  68. let s:MAX_LOOKBACK = 50
  69. " Syntax names for strings
  70. let s:SYNTAX_STRING = 'coffee\%(String\|AssignString\|Embed\|Regex\|Heregex\|'
  71. \ . 'Heredoc\)'
  72. " Syntax names for comments
  73. let s:SYNTAX_COMMENT = 'coffee\%(Comment\|BlockComment\|HeregexComment\)'
  74. " Syntax names for strings and comments
  75. let s:SYNTAX_STRING_COMMENT = s:SYNTAX_STRING . '\|' . s:SYNTAX_COMMENT
  76. " Compatibility code for shiftwidth() as recommended by the docs, but modified
  77. " so there isn't as much of a penalty if shiftwidth() exists.
  78. if exists('*shiftwidth')
  79. let s:ShiftWidth = function('shiftwidth')
  80. else
  81. function! s:ShiftWidth()
  82. return &shiftwidth
  83. endfunction
  84. endif
  85. " Get the linked syntax name of a character.
  86. function! s:SyntaxName(lnum, col)
  87. return synIDattr(synID(a:lnum, a:col, 1), 'name')
  88. endfunction
  89. " Check if a character is in a comment.
  90. function! s:IsComment(lnum, col)
  91. return s:SyntaxName(a:lnum, a:col) =~ s:SYNTAX_COMMENT
  92. endfunction
  93. " Check if a character is in a string.
  94. function! s:IsString(lnum, col)
  95. return s:SyntaxName(a:lnum, a:col) =~ s:SYNTAX_STRING
  96. endfunction
  97. " Check if a character is in a comment or string.
  98. function! s:IsCommentOrString(lnum, col)
  99. return s:SyntaxName(a:lnum, a:col) =~ s:SYNTAX_STRING_COMMENT
  100. endfunction
  101. " Search a line for a regex until one is found outside a string or comment.
  102. function! s:SearchCode(lnum, regex)
  103. " Start at the first column and look for an initial match (including at the
  104. " cursor.)
  105. call cursor(a:lnum, 1)
  106. let pos = search(a:regex, 'c', a:lnum)
  107. while pos
  108. if !s:IsCommentOrString(a:lnum, col('.'))
  109. return 1
  110. endif
  111. " Move to the match and continue searching (don't accept matches at the
  112. " cursor.)
  113. let pos = search(a:regex, '', a:lnum)
  114. endwhile
  115. return 0
  116. endfunction
  117. " Search for the nearest previous line that isn't a comment.
  118. function! s:GetPrevNormalLine(startlnum)
  119. let curlnum = a:startlnum
  120. while curlnum
  121. let curlnum = prevnonblank(curlnum - 1)
  122. " Return the line if the first non-whitespace character isn't a comment.
  123. if !s:IsComment(curlnum, indent(curlnum) + 1)
  124. return curlnum
  125. endif
  126. endwhile
  127. return 0
  128. endfunction
  129. function! s:SearchPair(startlnum, lookback, skip, open, close)
  130. " Go to the first column so a:close will be matched even if it's at the
  131. " beginning of the line.
  132. call cursor(a:startlnum, 1)
  133. return searchpair(a:open, '', a:close, 'bnW', a:skip, max([1, a:lookback]))
  134. endfunction
  135. " Skip if a match
  136. " - is in a string or comment
  137. " - is a single-line statement that isn't immediately
  138. " adjacent
  139. " - has a postfix condition and isn't an else statement or compound
  140. " expression
  141. function! s:ShouldSkip(startlnum, lnum, col)
  142. return s:IsCommentOrString(a:lnum, a:col) ||
  143. \ s:SearchCode(a:lnum, '\C\<then\>') && a:startlnum - a:lnum > 1 ||
  144. \ s:SearchCode(a:lnum, s:POSTFIX_CONDITION) &&
  145. \ getline(a:lnum) !~ s:ELSE_COND &&
  146. \ !s:SearchCode(a:lnum, s:COMPOUND_EXPRESSION)
  147. endfunction
  148. " Search for the nearest and farthest match for a keyword pair.
  149. function! s:SearchMatchingKeyword(startlnum, open, close)
  150. let skip = 's:ShouldSkip(' . a:startlnum . ", line('.'), line('.'))"
  151. " Search for the nearest match.
  152. let nearestlnum = s:SearchPair(a:startlnum, a:startlnum - s:MAX_LOOKBACK,
  153. \ skip, a:open, a:close)
  154. if !nearestlnum
  155. return []
  156. endif
  157. " Find the nearest previous line with indent less than or equal to startlnum.
  158. let ind = indent(a:startlnum)
  159. let lookback = s:GetPrevNormalLine(a:startlnum)
  160. while lookback && indent(lookback) > ind
  161. let lookback = s:GetPrevNormalLine(lookback)
  162. endwhile
  163. " Search for the farthest match. If there are no other matches, then the
  164. " nearest match is also the farthest one.
  165. let matchlnum = nearestlnum
  166. while matchlnum
  167. let lnum = matchlnum
  168. let matchlnum = s:SearchPair(matchlnum, lookback, skip, a:open, a:close)
  169. endwhile
  170. return [nearestlnum, lnum]
  171. endfunction
  172. " Strip a line of a trailing comment and surrounding whitespace.
  173. function! s:GetTrimmedLine(lnum)
  174. " Try to find a comment starting at the first column.
  175. call cursor(a:lnum, 1)
  176. let pos = search('#', 'c', a:lnum)
  177. " Keep searching until a comment is found or search returns 0.
  178. while pos
  179. if s:IsComment(a:lnum, col('.'))
  180. break
  181. endif
  182. let pos = search('#', '', a:lnum)
  183. endwhile
  184. if !pos
  185. " No comment was found so use the whole line.
  186. let line = getline(a:lnum)
  187. else
  188. " Subtract 1 to get to the column before the comment and another 1 for
  189. " column indexing -> zero-based indexing.
  190. let line = getline(a:lnum)[:col('.') - 2]
  191. endif
  192. return substitute(substitute(line, '^\s\+', '', ''),
  193. \ '\s\+$', '', '')
  194. endfunction
  195. " Get the indent policy when no special rules are used.
  196. function! s:GetDefaultPolicy(curlnum)
  197. " Check whether equalprg is being ran on existing lines.
  198. if strlen(getline(a:curlnum)) == indent(a:curlnum)
  199. " If not indenting an existing line, use the default policy.
  200. return s:DEFAULT_LEVEL
  201. else
  202. " Otherwise let autoindent determine what to do with an existing line.
  203. return '-1'
  204. endif
  205. endfunction
  206. function! GetCoffeeIndent(curlnum)
  207. " Get the previous non-blank line (may be a comment.)
  208. let prevlnum = prevnonblank(a:curlnum - 1)
  209. " Bail if there's no code before.
  210. if !prevlnum
  211. return -1
  212. endif
  213. " Bail if inside a multiline string.
  214. if s:IsString(a:curlnum, 1)
  215. let prevnlnum = prevlnum
  216. exec 'return' s:GetDefaultPolicy(a:curlnum)
  217. endif
  218. " Get the code part of the current line.
  219. let curline = s:GetTrimmedLine(a:curlnum)
  220. " Get the previous non-comment line.
  221. let prevnlnum = s:GetPrevNormalLine(a:curlnum)
  222. " Check if the current line is the closing bracket in a bracket pair.
  223. if has_key(s:BRACKET_PAIRS, curline[0])
  224. " Search for a matching opening bracket.
  225. let matchlnum = s:SearchPair(a:curlnum, a:curlnum - s:MAX_LOOKBACK,
  226. \ "s:IsCommentOrString(line('.'), col('.'))",
  227. \ s:BRACKET_PAIRS[curline[0]], curline[0])
  228. if matchlnum
  229. " Match the indent of the opening bracket.
  230. return indent(matchlnum)
  231. else
  232. " No opening bracket found (bad syntax), so bail.
  233. exec 'return' s:GetDefaultPolicy(a:curlnum)
  234. endif
  235. endif
  236. " Check if the current line is the closing keyword in a keyword pair.
  237. for pair in s:KEYWORD_PAIRS
  238. if curline =~ pair[0]
  239. " Find the nearest and farthest matches within the same indent level.
  240. let matches = s:SearchMatchingKeyword(a:curlnum, pair[1], pair[2])
  241. if len(matches)
  242. " Don't force indenting/outdenting as long as line is already lined up
  243. " with a valid match
  244. return max([min([indent(a:curlnum), indent(matches[0])]),
  245. \ indent(matches[1])])
  246. else
  247. " No starting keyword found (bad syntax), so bail.
  248. exec 'return' s:GetDefaultPolicy(a:curlnum)
  249. endif
  250. endif
  251. endfor
  252. " Check if the current line is a `when` and not the first in a switch block.
  253. if curline =~ '\C^when\>' && !s:SearchCode(prevnlnum, '\C\<switch\>')
  254. " Look back for a `when`.
  255. while prevnlnum
  256. if getline(prevnlnum) =~ '\C^\s*when\>'
  257. " Indent to match the found `when`, but don't force indenting (for when
  258. " indenting nested switch blocks.)
  259. return min([indent(a:curlnum), indent(prevnlnum)])
  260. endif
  261. let prevnlnum = s:GetPrevNormalLine(prevnlnum)
  262. endwhile
  263. " No matching `when` found (bad syntax), so bail.
  264. exec 'return' s:GetDefaultPolicy(a:curlnum)
  265. endif
  266. " If the previous line is a comment, use its indentation, but don't force
  267. " indenting.
  268. if prevlnum != prevnlnum
  269. return min([indent(a:curlnum), indent(prevlnum)])
  270. endif
  271. let prevline = s:GetTrimmedLine(prevnlnum)
  272. " Always indent after these operators.
  273. if prevline =~ s:BEGIN_BLOCK_OP
  274. return indent(prevnlnum) + s:ShiftWidth()
  275. endif
  276. " Indent if the previous line starts a function block, but don't force
  277. " indenting if the line is non-blank (for empty function bodies.)
  278. if prevline =~ s:FUNCTION
  279. if strlen(getline(a:curlnum)) > indent(a:curlnum)
  280. return min([indent(prevnlnum) + s:ShiftWidth(), indent(a:curlnum)])
  281. else
  282. return indent(prevnlnum) + s:ShiftWidth()
  283. endif
  284. endif
  285. " Check if continuation indenting is needed. If the line ends in a slash, make
  286. " sure it isn't a regex.
  287. if prevline =~ s:CONTINUATION_OP &&
  288. \ !(prevline =~ '/$' && s:IsString(prevnlnum, col([prevnlnum, '$']) - 1))
  289. " Don't indent if the continuation follows a closing bracket.
  290. if prevline =~ s:BRACKET_CONTINUATION
  291. exec 'return' s:GetDefaultPolicy(a:curlnum)
  292. endif
  293. let prevprevnlnum = s:GetPrevNormalLine(prevnlnum)
  294. " Don't indent if not the first continuation.
  295. if prevprevnlnum && s:GetTrimmedLine(prevprevnlnum) =~ s:CONTINUATION
  296. exec 'return' s:GetDefaultPolicy(a:curlnum)
  297. endif
  298. " Continuation indenting seems to vary between programmers, so if the line
  299. " is non-blank, don't override the indentation
  300. if strlen(getline(a:curlnum)) > indent(a:curlnum)
  301. exec 'return' s:GetDefaultPolicy(a:curlnum)
  302. endif
  303. " Otherwise indent a level.
  304. return indent(prevnlnum) + s:ShiftWidth()
  305. endif
  306. " Check if the previous line starts with a keyword that begins a block.
  307. if prevline =~ s:BEGIN_BLOCK
  308. " Indent if the current line doesn't start with `then` and the previous line
  309. " isn't a single-line statement.
  310. if curline !~ '\C^\<then\>' && !s:SearchCode(prevnlnum, '\C\<then\>') &&
  311. \ prevline !~ s:SINGLE_LINE_ELSE
  312. return indent(prevnlnum) + s:ShiftWidth()
  313. else
  314. exec 'return' s:GetDefaultPolicy(a:curlnum)
  315. endif
  316. endif
  317. " Indent a dot access if it's the first.
  318. if curline =~ s:DOT_ACCESS
  319. if prevline !~ s:DOT_ACCESS
  320. return indent(prevnlnum) + s:ShiftWidth()
  321. else
  322. exec 'return' s:GetDefaultPolicy(a:curlnum)
  323. endif
  324. endif
  325. " Outdent if a keyword breaks out of a block as long as it doesn't have a
  326. " postfix condition (and the postfix condition isn't a single-line statement.)
  327. if prevline =~ s:BREAK_BLOCK_OP
  328. if !s:SearchCode(prevnlnum, s:POSTFIX_CONDITION) ||
  329. \ s:SearchCode(prevnlnum, '\C\<then\>') &&
  330. \ !s:SearchCode(prevnlnum, s:CONTAINED_THEN)
  331. " Don't force indenting.
  332. return min([indent(a:curlnum), indent(prevnlnum) - s:ShiftWidth()])
  333. else
  334. exec 'return' s:GetDefaultPolicy(a:curlnum)
  335. endif
  336. endif
  337. " Check if inside brackets.
  338. let matchlnum = s:SearchPair(a:curlnum, a:curlnum - s:MAX_LOOKBACK,
  339. \ "s:IsCommentOrString(line('.'), col('.'))",
  340. \ '\[\|(\|{', '\]\|)\|}')
  341. " If inside brackets, indent relative to the brackets, but don't outdent an
  342. " already indented line.
  343. if matchlnum
  344. return max([indent(a:curlnum), indent(matchlnum) + s:ShiftWidth()])
  345. endif
  346. " No special rules applied, so use the default policy.
  347. exec 'return' s:GetDefaultPolicy(a:curlnum)
  348. endfunction