raylu 11 лет назад
Родитель
Сommit
dc71fe27b6
3 измененных файлов с 666 добавлено и 0 удалено
  1. 17 0
      vim/ftdetect/coffee.vim
  2. 428 0
      vim/indent/coffee.vim
  3. 221 0
      vim/syntax/coffee.vim

+ 17 - 0
vim/ftdetect/coffee.vim

@@ -0,0 +1,17 @@
+" Language:    CoffeeScript
+" Maintainer:  Mick Koch <mick@kochm.co>
+" URL:         http://github.com/kchmck/vim-coffee-script
+" License:     WTFPL
+
+autocmd BufNewFile,BufRead *.coffee set filetype=coffee
+autocmd BufNewFile,BufRead *Cakefile set filetype=coffee
+autocmd BufNewFile,BufRead *.coffeekup,*.ck set filetype=coffee
+autocmd BufNewFile,BufRead *._coffee set filetype=coffee
+
+function! s:DetectCoffee()
+    if getline(1) =~ '^#!.*\<coffee\>'
+        set filetype=coffee
+    endif
+endfunction
+
+autocmd BufNewFile,BufRead * call s:DetectCoffee()

+ 428 - 0
vim/indent/coffee.vim

@@ -0,0 +1,428 @@
+" Language:    CoffeeScript
+" Maintainer:  Mick Koch <mick@kochm.co>
+" 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
+
+" Compatibility code for shiftwidth() as recommended by the docs, but modified
+" so there isn't as much of a penalty if shiftwidth() exists.
+if exists('*shiftwidth')
+  let s:ShiftWidth = function('shiftwidth')
+else
+  function! s:ShiftWidth()
+    return &shiftwidth
+  endfunction
+endif
+
+" 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) + s: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) + s:ShiftWidth(), indent(a:curlnum)])
+    else
+      return indent(prevnlnum) + s: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) + s: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) + s: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) + s: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) - s: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) + s:ShiftWidth()])
+  endif
+
+  " No special rules applied, so use the default policy.
+  exec 'return' s:GetDefaultPolicy(a:curlnum)
+endfunction

+ 221 - 0
vim/syntax/coffee.vim

@@ -0,0 +1,221 @@
+" Language:    CoffeeScript
+" Maintainer:  Mick Koch <mick@kochm.co>
+" URL:         http://github.com/kchmck/vim-coffee-script
+" License:     WTFPL
+
+" Bail if our syntax is already loaded.
+if exists('b:current_syntax') && b:current_syntax == 'coffee'
+  finish
+endif
+
+" Include JavaScript for coffeeEmbed.
+syn include @coffeeJS syntax/javascript.vim
+silent! unlet b:current_syntax
+
+" Highlight long strings.
+syntax sync fromstart
+
+" These are `matches` instead of `keywords` because vim's highlighting
+" priority for keywords is higher than matches. This causes keywords to be
+" highlighted inside matches, even if a match says it shouldn't contain them --
+" like with coffeeAssign and coffeeDot.
+syn match coffeeStatement /\<\%(return\|break\|continue\|throw\)\>/ display
+hi def link coffeeStatement Statement
+
+syn match coffeeRepeat /\<\%(for\|while\|until\|loop\)\>/ display
+hi def link coffeeRepeat Repeat
+
+syn match coffeeConditional /\<\%(if\|else\|unless\|switch\|when\|then\)\>/
+\                           display
+hi def link coffeeConditional Conditional
+
+syn match coffeeException /\<\%(try\|catch\|finally\)\>/ display
+hi def link coffeeException Exception
+
+syn match coffeeKeyword /\<\%(new\|in\|of\|by\|and\|or\|not\|is\|isnt\|class\|extends\|super\|do\)\>/
+\                       display
+" The `own` keyword is only a keyword after `for`.
+syn match coffeeKeyword /\<for\s\+own\>/ contained containedin=coffeeRepeat
+\                       display
+hi def link coffeeKeyword Keyword
+
+syn match coffeeOperator /\<\%(instanceof\|typeof\|delete\)\>/ display
+hi def link coffeeOperator Operator
+
+" The first case matches symbol operators only if they have an operand before.
+syn match coffeeExtendedOp /\%(\S\s*\)\@<=[+\-*/%&|\^=!<>?.]\{-1,}\|[-=]>\|--\|++\|:/
+\                          display
+syn match coffeeExtendedOp /\<\%(and\|or\)=/ display
+hi def link coffeeExtendedOp coffeeOperator
+
+" This is separate from `coffeeExtendedOp` to help differentiate commas from
+" dots.
+syn match coffeeSpecialOp /[,;]/ display
+hi def link coffeeSpecialOp SpecialChar
+
+syn match coffeeBoolean /\<\%(true\|on\|yes\|false\|off\|no\)\>/ display
+hi def link coffeeBoolean Boolean
+
+syn match coffeeGlobal /\<\%(null\|undefined\)\>/ display
+hi def link coffeeGlobal Type
+
+" A special variable
+syn match coffeeSpecialVar /\<\%(this\|prototype\|arguments\)\>/ display
+hi def link coffeeSpecialVar Special
+
+" An @-variable
+syn match coffeeSpecialIdent /@\%(\%(\I\|\$\)\%(\i\|\$\)*\)\?/ display
+hi def link coffeeSpecialIdent Identifier
+
+" A class-like name that starts with a capital letter
+syn match coffeeObject /\<\u\w*\>/ display
+hi def link coffeeObject Structure
+
+" A constant-like name in SCREAMING_CAPS
+syn match coffeeConstant /\<\u[A-Z0-9_]\+\>/ display
+hi def link coffeeConstant Constant
+
+" A variable name
+syn cluster coffeeIdentifier contains=coffeeSpecialVar,coffeeSpecialIdent,
+\                                     coffeeObject,coffeeConstant
+
+" A non-interpolated string
+syn cluster coffeeBasicString contains=@Spell,coffeeEscape
+" An interpolated string
+syn cluster coffeeInterpString contains=@coffeeBasicString,coffeeInterp
+
+" Regular strings
+syn region coffeeString start=/"/ skip=/\\\\\|\\"/ end=/"/
+\                       contains=@coffeeInterpString
+syn region coffeeString start=/'/ skip=/\\\\\|\\'/ end=/'/
+\                       contains=@coffeeBasicString
+hi def link coffeeString String
+
+" A integer, including a leading plus or minus
+syn match coffeeNumber /\%(\i\|\$\)\@<![-+]\?\d\+\%([eE][+-]\?\d\+\)\?/ display
+" A hex, binary, or octal number
+syn match coffeeNumber /\<0[xX]\x\+\>/ display
+syn match coffeeNumber /\<0[bB][01]\+\>/ display
+syn match coffeeNumber /\<0[oO][0-7]\+\>/ display
+syn match coffeeNumber /\<\%(Infinity\|NaN\)\>/ display
+hi def link coffeeNumber Number
+
+" A floating-point number, including a leading plus or minus
+syn match coffeeFloat /\%(\i\|\$\)\@<![-+]\?\d*\.\@<!\.\d\+\%([eE][+-]\?\d\+\)\?/
+\                     display
+hi def link coffeeFloat Float
+
+" An error for reserved keywords, taken from the RESERVED array:
+" http://coffeescript.org/documentation/docs/lexer.html#section-67
+syn match coffeeReservedError /\<\%(case\|default\|function\|var\|void\|with\|const\|let\|enum\|export\|import\|native\|__hasProp\|__extends\|__slice\|__bind\|__indexOf\|implements\|interface\|package\|private\|protected\|public\|static\|yield\)\>/
+\                             display
+hi def link coffeeReservedError Error
+
+" A normal object assignment
+syn match coffeeObjAssign /@\?\%(\I\|\$\)\%(\i\|\$\)*\s*\ze::\@!/ contains=@coffeeIdentifier display
+hi def link coffeeObjAssign Identifier
+
+syn keyword coffeeTodo TODO FIXME XXX contained
+hi def link coffeeTodo Todo
+
+syn match coffeeComment /#.*/ contains=@Spell,coffeeTodo
+hi def link coffeeComment Comment
+
+syn region coffeeBlockComment start=/####\@!/ end=/###/
+\                             contains=@Spell,coffeeTodo
+hi def link coffeeBlockComment coffeeComment
+
+" A comment in a heregex
+syn region coffeeHeregexComment start=/#/ end=/\ze\/\/\/\|$/ contained
+\                               contains=@Spell,coffeeTodo
+hi def link coffeeHeregexComment coffeeComment
+
+" Embedded JavaScript
+syn region coffeeEmbed matchgroup=coffeeEmbedDelim
+\                      start=/`/ skip=/\\\\\|\\`/ end=/`/ keepend
+\                      contains=@coffeeJS
+hi def link coffeeEmbedDelim Delimiter
+
+syn region coffeeInterp matchgroup=coffeeInterpDelim start=/#{/ end=/}/ contained
+\                       contains=@coffeeAll
+hi def link coffeeInterpDelim PreProc
+
+" A string escape sequence
+syn match coffeeEscape /\\\d\d\d\|\\x\x\{2\}\|\\u\x\{4\}\|\\./ contained display
+hi def link coffeeEscape SpecialChar
+
+" A regex -- must not follow a parenthesis, number, or identifier, and must not
+" be followed by a number
+syn region coffeeRegex start=#\%(\%()\|\%(\i\|\$\)\@<!\d\)\s*\|\i\)\@<!/=\@!\s\@!#
+\                      end=#/[gimy]\{,4}\d\@!#
+\                      oneline contains=@coffeeBasicString,coffeeRegexCharSet
+syn region coffeeRegexCharSet start=/\[/ end=/]/ contained
+\                             contains=@coffeeBasicString
+hi def link coffeeRegex String
+hi def link coffeeRegexCharSet coffeeRegex
+
+" A heregex
+syn region coffeeHeregex start=#///# end=#///[gimy]\{,4}#
+\                        contains=@coffeeInterpString,coffeeHeregexComment,
+\                                  coffeeHeregexCharSet
+\                        fold
+syn region coffeeHeregexCharSet start=/\[/ end=/]/ contained
+\                               contains=@coffeeInterpString
+hi def link coffeeHeregex coffeeRegex
+hi def link coffeeHeregexCharSet coffeeHeregex
+
+" Heredoc strings
+syn region coffeeHeredoc start=/"""/ end=/"""/ contains=@coffeeInterpString
+\                        fold
+syn region coffeeHeredoc start=/'''/ end=/'''/ contains=@coffeeBasicString
+\                        fold
+hi def link coffeeHeredoc String
+
+" An error for trailing whitespace, as long as the line isn't just whitespace
+syn match coffeeSpaceError /\S\@<=\s\+$/ display
+hi def link coffeeSpaceError Error
+
+" An error for trailing semicolons, for help transitioning from JavaScript
+syn match coffeeSemicolonError /;$/ display
+hi def link coffeeSemicolonError Error
+
+" Ignore reserved words in dot accesses.
+syn match coffeeDotAccess /\.\@<!\.\s*\%(\I\|\$\)\%(\i\|\$\)*/he=s+1 contains=@coffeeIdentifier
+hi def link coffeeDotAccess coffeeExtendedOp
+
+" Ignore reserved words in prototype accesses.
+syn match coffeeProtoAccess /::\s*\%(\I\|\$\)\%(\i\|\$\)*/he=s+2 contains=@coffeeIdentifier
+hi def link coffeeProtoAccess coffeeExtendedOp
+
+" This is required for interpolations to work.
+syn region coffeeCurlies matchgroup=coffeeCurly start=/{/ end=/}/
+\                        contains=@coffeeAll
+syn region coffeeBrackets matchgroup=coffeeBracket start=/\[/ end=/\]/
+\                         contains=@coffeeAll
+syn region coffeeParens matchgroup=coffeeParen start=/(/ end=/)/
+\                       contains=@coffeeAll
+
+" These are highlighted the same as commas since they tend to go together.
+hi def link coffeeBlock coffeeSpecialOp
+hi def link coffeeBracket coffeeBlock
+hi def link coffeeCurly coffeeBlock
+hi def link coffeeParen coffeeBlock
+
+" This is used instead of TOP to keep things coffee-specific for good
+" embedding. `contained` groups aren't included.
+syn cluster coffeeAll contains=coffeeStatement,coffeeRepeat,coffeeConditional,
+\                              coffeeException,coffeeKeyword,coffeeOperator,
+\                              coffeeExtendedOp,coffeeSpecialOp,coffeeBoolean,
+\                              coffeeGlobal,coffeeSpecialVar,coffeeSpecialIdent,
+\                              coffeeObject,coffeeConstant,coffeeString,
+\                              coffeeNumber,coffeeFloat,coffeeReservedError,
+\                              coffeeObjAssign,coffeeComment,coffeeBlockComment,
+\                              coffeeEmbed,coffeeRegex,coffeeHeregex,
+\                              coffeeHeredoc,coffeeSpaceError,
+\                              coffeeSemicolonError,coffeeDotAccess,
+\                              coffeeProtoAccess,coffeeCurlies,coffeeBrackets,
+\                              coffeeParens
+
+if !exists('b:current_syntax')
+  let b:current_syntax = 'coffee'
+endif