Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 169e66dd3e
Fetching contributors…

Cannot retrieve contributors at this time

494 lines (388 sloc) 15.005 kb
;;; coffee-mode.el --- Major mode to edit CoffeeScript files in Emacs
;; Copyright (C) 2010 Chris Wanstrath
;; Version 0.1.0
;; Keywords: CoffeeScript major mode
;; Author: Chris Wanstrath <>
;; URL:
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, write to the Free Software
;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
;;; Commentary
;; For commentary please see the or
;;; Installation
;; In your shell:
;; $ cd ~/.emacs.d/vendor
;; $ git clone git://
;; In your emacs config:
;; (add-to-list 'load-path "~/.emacs.d/vendor/coffee-mode")
;; (require 'coffee-mode)
;;; Thanks
;; Major thanks to
;; the instructions.
;; Also thanks to Jason Blevins's markdown-mode.el for guidance.
;; TODO:
;; - Fix indentation toggling on blank (pure whitespace) lines
;; - imenu support
;; - Make prototype accessor assignments like `String::length: -> 10` pretty.
;; - Automatically `delete-trailing-whitespace' on save, configurable.
;; - mirror-mode - close brackets and parens automatically
;;; Code:
(require 'easymenu)
(require 'font-lock)
(require 'cl)
;; Customizable Variables
(defconst coffee-mode-version "0.1.0"
"The version of this `coffee-mode'.")
(defvar coffee-debug-mode nil
"Whether to run in debug mode or not. Logs to `*Messages*'.")
(defvar coffee-mode-hook nil
"A hook for you to run your own code when the mode is loaded.")
(defvar coffee-command "coffee"
"The CoffeeScript command used for evaluating code. Must be in your
(defvar coffee-repl-args '("-i")
"The command line arguments to pass to `coffee-command' to start a REPL.")
(defvar coffee-command-args '("-s" "-p" "--no-wrap")
"The command line arguments to pass to `coffee-command' to get it to
print the compiled JavaScript.")
(defvar coffee-js-mode 'js2-mode
"The mode to use when viewing compiled JavaScript.")
(defvar coffee-compiled-buffer-name "*coffee-compiled*"
"The name of the scratch buffer used when compiling CoffeeScript.")
(defvar coffee-mode-map (make-keymap)
"Keymap for CoffeeScript major mode.")
;; Macros
(defmacro setd (var val)
"Like setq but optionally logs the variable's value using `coffee-debug'."
(if coffee-debug-mode
(coffee-debug "%s: %s" ',var ,val)
(setq ,var ,val))
`(setq ,var ,val)))
;; Commands
(defun coffee-repl ()
"Launch a CoffeeScript REPL using `coffee-command' as an inferior mode."
(unless (comint-check-proc "*CoffeeREPL*")
(apply 'make-comint "CoffeeREPL"
coffee-command nil coffee-repl-args)))
(pop-to-buffer "*CoffeeScript*"))
(defun coffee-compile-buffer ()
"Compiles the current buffer and displays the JS in another buffer."
(coffee-compile-region (point-min) (point-max))))
(defun coffee-compile-region (start end)
"Compiles a region and displays the JS in another buffer."
(interactive "r")
(let ((buffer (get-buffer coffee-compiled-buffer-name)))
(when buffer
(kill-buffer buffer)))
(call-process-region start end coffee-command nil
(get-buffer-create coffee-compiled-buffer-name)
"-s" "-p" "--no-wrap")
(switch-to-buffer-other-frame (get-buffer coffee-compiled-buffer-name))
(funcall coffee-js-mode)
(defun coffee-show-version ()
"Prints the `coffee-mode' version."
(message (concat "coffee-mode v" coffee-mode-version)))
(defun coffee-open-reference ()
"Open browser to CoffeeScript reference."
(browse-url ""))
(defun coffee-open-github ()
"Open browser to `coffee-mode' project on GithHub."
(browse-url ""))
;; Menubar
(easy-menu-define coffee-mode-menu coffee-mode-map
"Menu for CoffeeScript mode"
["Compile Buffer" coffee-compile-buffer]
["Compile Region" coffee-compile-region]
["REPL" coffee-repl]
["CoffeeScript reference" coffee-open-reference]
["coffee-mode on GitHub" coffee-open-github]
["Version" coffee-show-version]
;; Define Language Syntax
;; Instance variables (implicit this)
(defvar coffee-this-regexp "@\\w*\\|this")
;; Assignment
(defvar coffee-assign-regexp "\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\):")
;; Lambda
(defvar coffee-lambda-regexp "\\((.+)\\)?\s *\\(->\\|=>\\)")
;; Booleans
(defvar coffee-boolean-regexp "\\b\\(true\\|false\\|yes\\|no\\|on\\|off\\)\\b")
;; Regular Expressions
(defvar coffee-regexp-regexp "\\/.+?\\/")
;; JavaScript Keywords
(defvar coffee-js-keywords
'("if" "else" "new" "return" "try" "catch"
"finally" "throw" "break" "continue" "for" "in" "while"
"delete" "instanceof" "typeof" "switch" "super" "extends"
;; Reserved keywords either by JS or CS.
(defvar coffee-js-reserved
'("case" "default" "do" "function" "var" "void" "with"
"const" "let" "debugger" "enum" "export" "import" "native"
"__extends" "__hasProp"))
;; CoffeeScript keywords.
(defvar coffee-cs-keywords
'("then" "unless" "and" "or" "is"
"isnt" "not" "of" "by" "where" "when"))
;; Regular expression combining the above three lists.
(defvar coffee-keywords-regexp (regexp-opt
coffee-cs-keywords) 'words))
;; Create the list for font-lock. Each class of keyword is given a
;; particular face.
(defvar coffee-font-lock-keywords
;; *Note*: order below matters. `coffee-keywords-regexp' goes last
;; because otherwise the keyword "state" in the function
;; "state_entry" would be highlighted.
`((,coffee-this-regexp . font-lock-variable-name-face)
(,coffee-assign-regexp . font-lock-type-face)
(,coffee-regexp-regexp . font-lock-constant-face)
(,coffee-boolean-regexp . font-lock-constant-face)
(,coffee-keywords-regexp . font-lock-keyword-face)))
;; Helper Functions
;; The command to comment/uncomment text
(defun coffee-comment-dwim (arg)
"Comment or uncomment current line or region in a smart way.
For detail, see `comment-dwim'."
(interactive "*P")
(require 'newcomment)
(let ((deactivate-mark nil) (comment-start "#") (comment-end ""))
(comment-dwim arg)))
(defun coffee-command-full ()
"The full `coffee-command' complete with args."
(mapconcat 'identity (append (list coffee-command) coffee-command-args) " "))
(defun coffee-debug (string &rest args)
"Print a message when in debug mode."
(when coffee-debug-mode
(apply 'message (append (list string) args))))
;; imenu support
;; This is a pretty naive but workable way of doing it. First we look
;; for any lines that starting with `coffee-assign-regexp' that include
;; `coffee-lambda-regexp' then add those tokens to the list.
;; Should cover cases like these:
;; minus: (x, y) -> x - y
;; String::length: -> 10
;; block: ->
;; print('potion')
;; (TODO: This next part is incomplete and therefor a lie.)
;; Next we look for any line that starts with `class' or
;; `coffee-assign-regexp' followed by `{` and drop into a
;; namespace. This means we search one indentation level deeper for
;; more assignments and add them to the alist prefixed with the
;; namespace name.
;; Should cover cases like these:
;; class Person
;; print: ->
;; print 'My name is ' + + '.'
;; class Policeman extends Person
;; constructor: (rank) ->
;; @rank: rank
;; print: ->
;; print 'My name is ' + + " and I'm a " + this.rank + '.'
;; app = {
;; window: {width: 200, height: 200}
;; para: -> 'Welcome.'
;; button: -> 'OK'
;; }
(defun coffee-imenu-create-index ()
"Create an imenu index of all methods in the buffer."
;; This function is called within a `save-excursion' so we're safe.
(let ((index-alist '()) assign pos func)
;; Go through every assignment that includes -> or => on the same
;; line.
(while (re-search-forward
(concat "^\\s *"
;; The token being assigned. `Please.print:` will be
;; `Please.print`, `block:` will be `block`, etc.
(setd assign (match-string 1))
;; The position of the match in the buffer.
(setd pos (match-beginning 0))
;; Add this to the alist. Done.
(push (cons assign pos) index-alist))
;; Return the alist.
;; Indentation
;;; The theory is explained in the README.
(defun coffee-indent-line ()
"Indent current line as CoffeeScript."
;; Bail early by indenting if point as the front of the line.
(if (= (point) (point-at-bol))
(let ((prev-indent 0) (cur-indent 0))
;; Figure out the indentation of the previous line
(forward-line -1)
(setd prev-indent (current-indentation))
;; Figure out the current line's indentation
(forward-line 1)
(setd cur-indent (current-indentation))
;; Shift one column to the left
(backward-to-indentation 0)
;; We're too far, remove all indentation.
(when (> (- (current-indentation) prev-indent) tab-width)
(backward-to-indentation 0)
(delete-region (point-at-bol) (point)))))))
(defun coffee-newline-and-indent ()
"Inserts a newline and indents it to the same level as the previous line."
;; Remember the current line indentation level,
;; insert a newline, and indent the newline to the same
;; level as the previous line.
(let ((prev-indent (current-indentation)) (indent-next nil))
(insert-tab (/ prev-indent tab-width))
;; We need to insert an additional tab because the last line was special.
(when (coffee-line-wants-indent)
;; Last line was a comment so this one should probably be,
;; too. Makes it easy to write multi-line comments (like the one I'm
;; writing right now).
(when (coffee-previous-line-is-comment)
(insert "# ")))
;; Indenters help determine whether the current line should be
;; indented further based on the content of the previous line. If a
;; line starts with `class', for instance, you're probably going to
;; want to indent the next line.
(defvar coffee-indenters-bol '("class" "for" "if" "try")
"Keywords or syntax whose presence at the start of a line means the
next line should probably be indented.")
(defun coffee-indenters-bol-regexp ()
"Builds a regexp out of `coffee-indenters-bol' words."
(concat "^" (regexp-opt coffee-indenters-bol 'words)))
(defvar coffee-indenters-eol '(?> ?{ ?\[)
"Single characters at the end of a line that mean the next line
should probably be indented.")
(defun coffee-line-wants-indent ()
"Does the current line want to be indented deeper than the previous
line? Returns `t' or `nil'. See the README for more details."
(let ((indenter-at-bol) (indenter-at-eol))
;; Go back a line and to the first character.
(forward-line -1)
(backward-to-indentation 0)
;; If the next few characters match one of our magic indenter
;; keywords, we want to indent the line we were on originally.
(when (looking-at (coffee-indenters-bol-regexp))
(setd indenter-at-bol t))
;; If that didn't match, go to the back of the line and check to
;; see if the last character matches one of our indenter
;; characters.
(when (not indenter-at-bol)
;; Optimized for speed - checks only the last character.
(when (some (lambda (char)
(= (char-before) char))
(setd indenter-at-eol t)))
;; If we found an indenter, return `t'.
(or indenter-at-bol indenter-at-eol))))
(defun coffee-previous-line-is-comment ()
"Returns `t' if the previous line is a CoffeeScript comment."
(forward-line -1)
(defun coffee-line-is-comment ()
"Returns `t' if the current line is a CoffeeScript comment."
(backward-to-indentation 0)
(= (char-after) (string-to-char "#"))))
;; Define Major Mode
(define-derived-mode coffee-mode fundamental-mode
"Major mode for editing CoffeeScript..."
(define-key coffee-mode-map (kbd "A-r") 'coffee-compile-buffer)
(define-key coffee-mode-map (kbd "A-R") 'coffee-execute-line)
(define-key coffee-mode-map (kbd "A-M-r") 'coffee-repl)
(define-key coffee-mode-map [remap comment-dwim] 'coffee-comment-dwim)
(define-key coffee-mode-map "\C-m" 'coffee-newline-and-indent)
;; code for syntax highlighting
(setq font-lock-defaults '((coffee-font-lock-keywords)))
;; perl style comment: "# ..."
(modify-syntax-entry ?# "< b" coffee-mode-syntax-table)
(modify-syntax-entry ?\n "> b" coffee-mode-syntax-table)
(setq comment-start "#")
;; single quote strings
(modify-syntax-entry ?' "\"" coffee-mode-syntax-table)
(modify-syntax-entry ?' "\"" coffee-mode-syntax-table)
;; indentation
(make-local-variable 'indent-line-function)
(setq indent-line-function 'coffee-indent-line)
;; imenu
(make-local-variable 'imenu-create-index-function)
(setq imenu-create-index-function 'coffee-imenu-create-index)
;; no tabs
(setq indent-tabs-mode nil)
;; clear memory
;; TODO: make these accurate
(setq coffee-keywords-regexp nil)
(setq coffee-types-regexp nil)
(setq coffee-constants-regexp nil)
(setq coffee-events-regexp nil)
(setq coffee-functions-regexp nil))
(provide 'coffee-mode)
;; On Load
;; Run coffee-mode for files ending in .coffee.
(add-to-list 'auto-mode-alist '("\\.coffee$" . coffee-mode))
(add-to-list 'auto-mode-alist '("Cakefile" . coffee-mode))
Jump to Line
Something went wrong with that request. Please try again.