Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
983 lines (803 sloc) 31.9 KB
;;; coffee-mode.el --- Major mode to edit CoffeeScript files in Emacs
;; Copyright (C) 2010 Chris Wanstrath
;; Version: 0.4.1
;; 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
;; Provides syntax highlighting, indentation support, imenu support,
;; a menu bar, and a few cute commands.
;; ## Indentation
;; ### TAB Theory
;; It goes like this: when you press `TAB`, we indent the line unless
;; doing so would make the current line more than two indentation levels
;; deepers than the previous line. If that's the case, remove all
;; indentation.
;; Consider this code, with point at the position indicated by the
;; caret:
;; line1()
;; line2()
;; line3()
;; ^
;; Pressing `TAB` will produce the following code:
;; line1()
;; line2()
;; line3()
;; ^
;; Pressing `TAB` again will produce this code:
;; line1()
;; line2()
;; line3()
;; ^
;; And so on. I think this is a pretty good way of getting decent
;; indentation with a whitespace-sensitive language.
;; ### Newline and Indent
;; We all love hitting `RET` and having the next line indented
;; properly. Given this code and cursor position:
;; line1()
;; line2()
;; line3()
;; ^
;; Pressing `RET` would insert a newline and place our cursor at the
;; following position:
;; line1()
;; line2()
;; line3()
;; ^
;; In other words, the level of indentation is maintained. This
;; applies to comments as well. Combined with the `TAB` you should be
;; able to get things where you want them pretty easily.
;; ### Indenters
;; `class`, `for`, `if`, and possibly other keywords cause the next line
;; to be indented a level deeper automatically.
;; For example, given this code and cursor position::
;; class Animal
;; ^
;; Pressing enter would produce the following:
;; class Animal
;; ^
;; That is, indented a column deeper.
;; This also applies to lines ending in `->`, `=>`, `{`, `[`, and
;; possibly more characters.
;; So this code and cursor position:
;; $('#demo').click ->
;; ^
;; On enter would produce this:
;; $('#demo').click ->
;; ^
;; Pretty slick.
;; Thanks to Jeremy Ashkenas for CoffeeScript, and to
;;, Jason
;; Blevins's markdown-mode.el and Steve Yegge's js2-mode for guidance.
;; TODO:
;; - Make prototype accessor assignments like `String::length: -> 10` pretty.
;; - mirror-mode - close brackets and parens automatically
;;; Code:
(require 'comint)
(require 'easymenu)
(require 'font-lock)
(require 'cl))
;; Customizable Variables
(defconst coffee-mode-version "0.4.1"
"The version of `coffee-mode'.")
(defgroup coffee nil
"A CoffeeScript major mode."
:group 'languages)
(defcustom coffee-tab-width tab-width
"The tab width to use when indenting."
:type 'integer
:group 'coffee)
(defcustom coffee-command "coffee"
"The CoffeeScript command used for evaluating code."
:type 'string
:group 'coffee)
(defcustom coffee-js-directory ""
"The directory for compiled JavaScript files output. This can
be an absolute path starting with a `/`, or it can be path
relative to the directory containing the coffeescript sources to
be compiled."
:type 'string
:group 'coffee)
(defcustom js2coffee-command "js2coffee"
"The js2coffee command used for evaluating code."
:type 'string
:group 'coffee)
(defcustom coffee-args-repl '("-i")
"The arguments to pass to `coffee-command' to start a REPL."
:type 'list
:group 'coffee)
(defcustom coffee-args-compile '("-c")
"The arguments to pass to `coffee-command' to compile a file."
:type 'list
:group 'coffee)
(defcustom coffee-compiled-buffer-name "*coffee-compiled*"
"The name of the scratch buffer used for compiled CoffeeScript."
:type 'string
:group 'coffee)
(defcustom coffee-repl-buffer "*CoffeeREPL*"
"The name of the CoffeeREPL buffer."
:type 'string
:group 'coffee)
(defcustom coffee-compile-jump-to-error t
"Whether to jump to the first error if compilation fails.
Since the coffee compiler does not always include a line number in
its error messages, this is not always possible."
:type 'boolean
:group 'coffee)
(defcustom coffee-watch-buffer-name "*coffee-watch*"
"The name of the scratch buffer used when using the --watch flag
with CoffeeScript."
:type 'string
:group 'coffee)
(defcustom coffee-mode-hook nil
"Hook called by `coffee-mode'. Examples:
;; CoffeeScript uses two spaces.
(make-local-variable 'tab-width)
(set 'tab-width 2)
;; If you don't want your compiled files to be wrapped
(setq coffee-args-compile '(\"-c\" \"--bare\"))
;; Emacs key binding
(define-key coffee-mode-map [(meta r)] 'coffee-compile-buffer)
;; Bleeding edge.
(setq coffee-command \"~/dev/coffee\")
;; Compile '.coffee' files on every save
(and (file-exists-p (buffer-file-name))
(file-exists-p (coffee-compiled-file-name))
(coffee-cos-mode t)))"
:type 'hook
:group 'coffee)
(defvar coffee-mode-map
(let ((map (make-sparse-keymap)))
;; key bindings
(define-key map (kbd "A-r") 'coffee-compile-buffer)
(define-key map (kbd "A-R") 'coffee-compile-region)
(define-key map (kbd "A-M-r") 'coffee-repl)
(define-key map [remap comment-dwim] 'coffee-comment-dwim)
(define-key map [remap newline-and-indent] 'coffee-newline-and-indent)
(define-key map "\C-m" 'coffee-newline-and-indent)
(define-key map "\C-c\C-o\C-s" 'coffee-cos-mode)
(define-key map "\177" 'coffee-dedent-line-backspace)
(define-key map (kbd "C-c C-<") 'coffee-indent-shift-left)
(define-key map (kbd "C-c C->") 'coffee-indent-shift-right)
(define-key map (kbd "C-c C-l") 'coffee-send-line)
(define-key map (kbd "C-c C-r") 'coffee-send-region)
(define-key map (kbd "C-c C-b") 'coffee-send-buffer)
(define-key map (kbd "<backtab>") 'coffee-indent-shift-left)
"Keymap for CoffeeScript major mode.")
;; Commands
(defun coffee-repl ()
"Launch a CoffeeScript REPL using `coffee-command' as an inferior mode."
(unless (comint-check-proc coffee-repl-buffer)
(apply 'make-comint "CoffeeREPL"
nil (append (list "NODE_NO_READLINE=1" coffee-command) coffee-args-repl)))
;; Workaround:
(set (make-local-variable 'comint-preoutput-filter-functions)
(cons (lambda (string)
(replace-regexp-in-string "\x1b\\[.[GJK]" "" string)) nil)))
(pop-to-buffer coffee-repl-buffer))
(defun coffee-compiled-file-name (&optional filename)
(let ((working-on-file (expand-file-name (or filename (buffer-file-name)))))
(unless (string= coffee-js-directory "")
(setq working-on-file
(concat (if (not (string-match "^/" coffee-js-directory))
(concat (file-name-directory working-on-file) "/"))
coffee-js-directory "/"
(file-name-nondirectory working-on-file)))))
;; Returns the name of the JavaScript file compiled from a CoffeeScript file.
;; If FILENAME is omitted, the current buffer's file name is used.
(concat (file-name-sans-extension working-on-file) ".js")))
(defun coffee-revert-buffer-compiled-file (file-name)
"Revert a buffer of compiled file when the buffer exist and is not modified."
(let ((buffer (find-buffer-visiting file-name)))
(when (and buffer (not (buffer-modified-p buffer)))
(with-current-buffer buffer
(revert-buffer nil t)))))
(defun coffee-compile-file ()
"Compiles and saves the current file to disk in a file of the same
base name, with extension `.js'. Subsequent runs will overwrite the
If there are compilation errors, point is moved to the first
(see `coffee-compile-jump-to-error')."
(let ((compiler-output (shell-command-to-string (coffee-command-compile (buffer-file-name)))))
(if (string= compiler-output "")
(let ((file-name (coffee-compiled-file-name)))
(message "Compiled and saved %s" file-name)
(coffee-revert-buffer-compiled-file file-name))
(let* ((msg (car (split-string compiler-output "[\n\r]+")))
(line (and (string-match "on line \\([0-9]+\\)" msg)
(string-to-number (match-string 1 msg)))))
(message msg)
(when (and coffee-compile-jump-to-error line (> line 0))
(goto-char (point-min))
(forward-line (1- line)))))))
(defun coffee-compile-buffer ()
"Compiles the current buffer and displays the JavaScript in a buffer
called `coffee-compiled-buffer-name'."
(coffee-compile-region (point-min) (point-max))))
(defun coffee-compile-region (start end)
"Compiles a region and displays the JavaScript in a buffer called
(interactive "r")
(let ((buffer (get-buffer coffee-compiled-buffer-name)))
(when buffer
(with-current-buffer buffer
(apply (apply-partially 'call-process-region start end coffee-command nil
(get-buffer-create coffee-compiled-buffer-name)
(append coffee-args-compile (list "-s" "-p")))
(let ((buffer (get-buffer coffee-compiled-buffer-name)))
(display-buffer buffer)
(with-current-buffer buffer
(let ((buffer-file-name "tmp.js")) (set-auto-mode)))))
(defun coffee-get-repl-proc ()
(unless (comint-check-proc coffee-repl-buffer)
(get-buffer-process coffee-repl-buffer))
(defun coffee-send-line ()
"Send the current line to the inferior Coffee process."
(coffee-send-region (line-beginning-position) (line-end-position)))
(defun coffee-send-region (start end)
"Send the current region to the inferior Coffee process."
(interactive "r")
(comint-simple-send (coffee-get-repl-proc)
(buffer-substring-no-properties start end)))
(defun coffee-send-buffer ()
"Send the current buffer to the inferior Coffee process."
(coffee-send-region (point-min) (point-max)))
(defun coffee-js2coffee-replace-region (start end)
"Convert JavaScript in the region into CoffeeScript."
(interactive "r")
(let ((buffer (get-buffer coffee-compiled-buffer-name)))
(when buffer
(kill-buffer buffer)))
(call-process-region start end
js2coffee-command nil
(delete-region start end))
(defun coffee-version ()
"Show the `coffee-mode' version in the echo area."
(message (concat "coffee-mode version " coffee-mode-version)))
(defun coffee-watch (dir-or-file)
"Run `coffee-run-cmd' with the --watch flag on a directory or file."
(interactive "fDirectory or File: ")
(let ((coffee-compiled-buffer-name coffee-watch-buffer-name)
(args (mapconcat 'identity (append coffee-args-compile (list "--watch" (expand-file-name dir-or-file))) " ")))
(coffee-run-cmd args)))
;; Menubar
(easy-menu-define coffee-mode-menu coffee-mode-map
"Menu for CoffeeScript mode"
["Compile File" coffee-compile-file]
["Compile Buffer" coffee-compile-buffer]
["Compile Region" coffee-compile-region]
["REPL" coffee-repl]
["Version" coffee-version]
;; Define Language Syntax
;; Instance variables (implicit this)
(defvar coffee-this-regexp "\\(?:@\\w+\\|\\<this\\)\\>")
;; Prototype::access
(defvar coffee-prototype-regexp "\\(\\(\\w\\|\\.\\| \\|$\\)+?\\)::\\(\\(\\w\\|\\.\\| \\|$\\)+?\\):")
;; Assignment
(defvar coffee-assign-regexp "\\(\\(\\w\\|\\.\\|$\\)+?\s*\\):")
;; Local Assignment
(defvar coffee-local-assign-regexp "\\(?:^\\|\\s-\\)\\(\\(\\w\\|\\$\\)+\\)\s+=[^>]")
;; Lambda
(defvar coffee-lambda-regexp "\\((.+)\\)?\\s *\\(->\\|=>\\)")
;; Namespaces
(defvar coffee-namespace-regexp "\\b\\(class\\s +\\(\\S +\\)\\)\\b")
;; Booleans
(defvar coffee-boolean-regexp "\\b\\(true\\|false\\|yes\\|no\\|on\\|off\\|null\\|undefined\\)\\b")
;; Regular expressions
(defvar coffee-regexp-regexp "\\s$.*\\s$")
;; String Interpolation(This regexp is taken from ruby-mode)
(defvar coffee-string-interpolation-regexp "#{[^}\n\\\\]*\\(?:\\\\.[^}\n\\\\]*\\)*}")
;; 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"
"class" "until" "loop"))
;; 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" "own"
"isnt" "not" "of" "by" "when"))
;; Iced CoffeeScript keywords
(defvar iced-coffee-cs-keywords
'("await" "defer"))
;; Regular expression combining the above three lists.
(defvar coffee-keywords-regexp
;; keywords can be member names.
(format "\\<%s\\>"
(regexp-opt (append coffee-js-reserved
iced-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-regexp-regexp . font-lock-constant-face)
(,coffee-this-regexp . font-lock-variable-name-face)
(,coffee-prototype-regexp . font-lock-variable-name-face)
(,coffee-assign-regexp . font-lock-type-face)
(,coffee-local-assign-regexp 1 font-lock-variable-name-face)
(,coffee-boolean-regexp . font-lock-constant-face)
(,coffee-lambda-regexp 2 font-lock-function-name-face)
(,coffee-keywords-regexp 1 font-lock-keyword-face)
(,coffee-string-interpolation-regexp 0 font-lock-variable-name-face t)))
;; Helper Functions
(defun coffee-comment-dwim (arg)
"Comment or uncomment current line or region in a smart way.
For details, see `comment-dwim'."
(interactive "*P")
(require 'newcomment)
(let ((deactivate-mark nil) (comment-start "#") (comment-end ""))
(comment-dwim arg)))
(defun coffee-command-compile (file-name)
"Run `coffee-command' to compile FILE."
(let* ((full-file-name
(expand-file-name file-name))
(coffee-compiled-file-name full-file-name))))
(if (not (file-exists-p output-dir))
(make-directory output-dir t))
(mapconcat 'identity (append (list (shell-quote-argument coffee-command))
(list "-o" (shell-quote-argument output-dir))
(list (shell-quote-argument full-file-name)))
" ")))
(defun coffee-run-cmd (args)
"Run `coffee-command' with the given arguments, and display the
output in a compilation buffer."
(interactive "sArguments: ")
(let ((compilation-buffer-name-function
(lambda (this-mode)
(generate-new-buffer-name coffee-compiled-buffer-name))))
(compile (concat coffee-command " " 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')
;; 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 + '.'
;; TODO:
;; 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.
(goto-char (point-min))
(let ((index-alist '()) assign pos indent ns-name ns-indent)
;; Go through every assignment that includes -> or => on the same
;; line or starts with `class'.
(while (re-search-forward
(concat "^\\(\\s *\\)"
;; If this is the start of a new namespace, save the namespace's
;; indentation level and name.
(when (match-string 8)
;; Set the name.
(setq ns-name (match-string 8))
;; If this is a class declaration, add :: to the namespace.
(setq ns-name (concat ns-name "::"))
;; Save the indentation level.
(setq ns-indent (length (match-string 1))))
;; If this is an assignment, save the token being
;; assigned. `Please.print:` will be `Please.print`, `block:`
;; will be `block`, etc.
(when (setq assign (match-string 3))
;; The position of the match in the buffer.
(setq pos (match-beginning 3))
;; The indent level of this match
(setq indent (length (match-string 1)))
;; If we're within the context of a namespace, add that to the
;; front of the assign, e.g.
;; constructor: => Policeman::constructor
(when (and ns-name (> indent ns-indent))
(setq assign (concat ns-name assign)))
;; Clear the namespace if we're no longer indented deeper
;; than it.
(when (and ns-name (<= indent ns-indent))
(setq ns-name nil)
(setq ns-indent nil))
;; 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."
(if (= (point) (point-at-bol))
(let ((prev-indent (coffee-previous-indent))
(cur-indent (current-indentation)))
;; Shift one column to the left
(when (= (point-at-bol) (point))
(forward-char coffee-tab-width))
;; We're too far, remove all indentation.
(when (> (- (current-indentation) prev-indent) coffee-tab-width)
(backward-to-indentation 0)
(delete-region (point-at-bol) (point)))))))
(defun coffee-previous-indent ()
"Return the indentation level of the previous non-blank line."
(forward-line -1)
(if (bobp)
(while (and (looking-at "^[ \t]*$") (not (bobp))) (forward-line -1))
(defun coffee-newline-and-indent ()
"Insert a newline and indent 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))
(delete-horizontal-space t)
(insert-tab (/ prev-indent coffee-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 "# ")))
(defun coffee-dedent-line-backspace (arg)
"Unindent to increment of `coffee-tab-width' with ARG==1 when
called from first non-blank char of line.
Delete ARG spaces if ARG!=1."
(interactive "*p")
(if (and (= 1 arg)
(= (point) (save-excursion
(not (bolp)))
(let ((extra-space-count (% (current-column) coffee-tab-width)))
(if (zerop extra-space-count)
(backward-delete-char-untabify arg)))
;; 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" "while")
"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."
(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 ()
"Return t if the current line should be indented relative to the
previous line."
(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))
(setq 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.
(let ((indenters coffee-indenters-eol))
(while indenters
(if (and (char-before) (/= (char-before) (car indenters)))
(setq indenters (cdr indenters))
(setq indenter-at-eol t)
(setq indenters nil)))))
;; If we found an indenter, return `t'.
(or indenter-at-bol indenter-at-eol))))
(defun coffee-previous-line-is-comment ()
"Return t if the previous line is a CoffeeScript comment."
(forward-line -1)
(defun coffee-line-is-comment ()
"Return t if the current line is a CoffeeScript comment."
(backward-to-indentation 0)
(= (char-after) (string-to-char "#"))))
;; (defun coffee-quote-syntax (n)
;; "Put `syntax-table' property correctly on triple quote.
;; Used for syntactic keywords. N is the match number (1, 2 or 3)."
;; ;; From python-mode...
;; ;;
;; ;; Given a triple quote, we have to check the context to know
;; ;; whether this is an opening or closing triple or whether it's
;; ;; quoted anyhow, and should be ignored. (For that we need to do
;; ;; the same job as `syntax-ppss' to be correct and it seems to be OK
;; ;; to use it here despite initial worries.) We also have to sort
;; ;; out a possible prefix -- well, we don't _have_ to, but I think it
;; ;; should be treated as part of the string.
;; ;; Test cases:
;; ;; ur"""ar""" x='"' # """
;; ;; x = ''' """ ' a
;; ;; '''
;; ;; x '"""' x """ \"""" x
;; (save-excursion
;; (goto-char (match-beginning 0))
;; (cond
;; ;; Consider property for the last char if in a fenced string.
;; ((= n 3)
;; (let* ((font-lock-syntactic-keywords nil)
;; (syntax (syntax-ppss)))
;; (when (eq t (nth 3 syntax)) ; after unclosed fence
;; (goto-char (nth 8 syntax)) ; fence position
;; ;; (skip-chars-forward "uUrR") ; skip any prefix
;; ;; Is it a matching sequence?
;; (if (eq (char-after) (char-after (match-beginning 2)))
;; (eval-when-compile (string-to-syntax "|"))))))
;; ;; Consider property for initial char, accounting for prefixes.
;; ((or (and (= n 2) ; leading quote (not prefix)
;; (not (match-end 1))) ; prefix is null
;; (and (= n 1) ; prefix
;; (match-end 1))) ; non-empty
;; (let ((font-lock-syntactic-keywords nil))
;; (unless (eq 'string (syntax-ppss-context (syntax-ppss)))
;; (eval-when-compile (string-to-syntax "|")))))
;; ;; Otherwise (we're in a non-matching string) the property is
;; ;; nil, which is OK.
;; )))
(defun coffee-indent-shift-amount (start end dir)
"Compute distance to the closest increment of `coffee-tab-width'."
(let ((min most-positive-fixnum) rem)
(goto-char start)
(while (< (point) end)
(let ((current (current-indentation)))
(when (< current min) (setq min current)))
(setq rem (% min coffee-tab-width))
(if (zerop rem)
(cond ((eq dir 'left) rem)
((eq dir 'right) (- coffee-tab-width rem))
(t 0))))))
(defun coffee-indent-shift-left (start end &optional count)
"Shift lines contained in region START END by COUNT columns to the left.
If COUNT is not given, indents to the closest increment of
`coffee-tab-width'. If region isn't active, the current line is
shifted. The shifted region includes the lines in which START and
END lie. An error is signaled if any lines in the region are
indented less than COUNT columns."
(if mark-active
(list (region-beginning) (region-end) current-prefix-arg)
(list (line-beginning-position) (line-end-position) current-prefix-arg)))
(let ((amount (if count (prefix-numeric-value count)
(coffee-indent-shift-amount start end 'left))))
(when (> amount 0)
(let (deactivate-mark)
(goto-char start)
;; Check that all lines can be shifted enough
(while (< (point) end)
(if (and (< (current-indentation) amount)
(not (looking-at "[ \t]*$")))
(error "Can't shift all lines enough"))
(indent-rigidly start end (- amount)))))))
(add-to-list 'debug-ignored-errors "^Can't shift all lines enough")
(defun coffee-indent-shift-right (start end &optional count)
"Shift lines contained in region START END by COUNT columns to the right.
if COUNT is not given, indents to the closest increment of
`coffee-tab-width'. If region isn't active, the current line is
shifted. The shifted region includes the lines in which START and
END lie."
(if mark-active
(list (region-beginning) (region-end) current-prefix-arg)
(list (line-beginning-position) (line-end-position) current-prefix-arg)))
(let (deactivate-mark
(amount (if count (prefix-numeric-value count)
(coffee-indent-shift-amount start end 'right))))
(indent-rigidly start end amount)))
;; Define Major Mode
(defun coffee-block-comment-delimiter (match)
(goto-char match)
(add-text-properties (point) (+ (point) 1) `(coffee-mode-syntax-table (14 . nil)))))
;; support coffescript block comments
;; examples:
;; at indent level 0
;; ###
;; foobar
;; ###
;; at indent level 0 with text following it
;; ### foobar
;; moretext
;; ###
;; at indent level > 0
;; ###
;; foobar
;; ###
;; examples of non-block comments:
;; #### foobar
(defun coffee-propertize-function (start end)
;; return if we don't have anything to parse
(unless (>= start end)
(goto-char start)
(let ((match (re-search-forward
"^[[:space:]]*###\\([[:space:]]+.*\\)?$" end t)))
(if match
(coffee-block-comment-delimiter match)
(goto-char match)
(coffee-propertize-function (point) end))))))))
;; For compatibility with Emacs < 24, derive conditionally
(defalias 'coffee-parent-mode
(if (fboundp 'prog-mode) 'prog-mode 'fundamental-mode))
(define-derived-mode coffee-mode coffee-parent-mode "Coffee"
"Major mode for editing CoffeeScript."
;; code for syntax highlighting
(setq font-lock-defaults '((coffee-font-lock-keywords)))
;; treat "_" as part of a word
(modify-syntax-entry ?_ "w" coffee-mode-syntax-table)
;; perl style comment: "# ..."
(modify-syntax-entry ?# "< b" coffee-mode-syntax-table)
(modify-syntax-entry ?\n "> b" coffee-mode-syntax-table)
;; Treat slashes as paired delimiters; useful for finding regexps.
(modify-syntax-entry ?/ "$" coffee-mode-syntax-table)
(set (make-local-variable 'comment-start) "#")
;; single quote strings
(modify-syntax-entry ?' "\"" coffee-mode-syntax-table)
;; (setq font-lock-syntactic-keywords
;; ;; Make outer chars of matching triple-quote sequences into generic
;; ;; string delimiters.
;; ;; First avoid a sequence preceded by an odd number of backslashes.
;; `((,(concat "\\(?:^\\|[^\\]\\(?:\\\\.\\)*\\)" ;Prefix.
;; "\\(?:\\('\\)\\('\\)\\('\\)\\|\\(?1:\"\\)\\(?2:\"\\)\\(?3:\"\\)\\)")
;; (1 (coffee-quote-syntax 1) nil lax)
;; (2 (coffee-quote-syntax 2))
;; (3 (coffee-quote-syntax 3)))))
;; indentation
(set (make-local-variable 'indent-line-function) #'coffee-indent-line)
(set (make-local-variable 'tab-width) coffee-tab-width)
(set (make-local-variable 'syntax-propertize-function) #'coffee-propertize-function)
;; imenu
(set (make-local-variable 'imenu-create-index-function) #'coffee-imenu-create-index)
;; Don't let electric-indent-mode break coffee-mode.
(set (make-local-variable 'electric-indent-functions)
(list (lambda (arg) 'no-indent)))
;; no tabs
(setq indent-tabs-mode nil))
;; Compile-on-Save minor mode
(defvar coffee-cos-mode-line " CoS")
(make-variable-buffer-local 'coffee-cos-mode-line)
(define-minor-mode coffee-cos-mode
"Toggle compile-on-save for coffee-mode.
Add `'(lambda () (coffee-cos-mode t))' to `coffee-mode-hook' to turn
it on by default."
:group 'coffee-cos :lighter coffee-cos-mode-line
(add-hook 'after-save-hook 'coffee-compile-file nil t))
(remove-hook 'after-save-hook 'coffee-compile-file t))))
(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 '("\\.iced\\'" . coffee-mode))
(add-to-list 'auto-mode-alist '("Cakefile\\'" . coffee-mode))
(add-to-list 'interpreter-mode-alist '("coffee" . coffee-mode))
;;; coffee-mode.el ends here
Jump to Line
Something went wrong with that request. Please try again.