]> code.delx.au - gnu-emacs-elpa/blobdiff - coffee-mode.el
Small patch to fix indentation behavior; before, the global tab-width setting is...
[gnu-emacs-elpa] / coffee-mode.el
index e59e3d1656646f9c937ee97074fcd90df57a8b74..27a2e6e256e8916868d9aca88f86ba5c98bb6654 100644 (file)
@@ -2,7 +2,7 @@
 
 ;; Copyright (C) 2010 Chris Wanstrath
 
-;; Version 0.1.0
+;; Version 0.3.0
 ;; Keywords: CoffeeScript major mode
 ;; Author: Chris Wanstrath <chris@ozmm.org>
 ;; URL: http://github.com/defunkt/coffee-script
 ;; Major thanks to http://xahlee.org/emacs/elisp_syntax_coloring.html
 ;; the instructions.
 
-;; Also thanks to Jason Blevins's markdown-mode.el for guidance.
+;; Also thanks to Jason Blevins's markdown-mode.el and Steve Yegge's
+;; js2-mode for guidance.
 
+;; TODO:
+;; - Execute {buffer,region,line} and show output in new buffer
+;; - Make prototype accessor assignments like `String::length: -> 10` pretty.
+;; - mirror-mode - close brackets and parens automatically
+
+;;; Code:
+
+(require 'comint)
 (require 'easymenu)
 (require 'font-lock)
 
+(eval-when-compile
+  (require 'cl))
+
 ;;
 ;; Customizable Variables
 ;;
 
-(defconst coffee-mode-version "0.1.0"
+(defconst coffee-mode-version "0.3.0"
   "The version of this `coffee-mode'.")
 
-(defvar coffee-mode-hook nil
-  "A hook for you to run your own code when the mode is loaded.")
+(defgroup coffee nil
+  "A CoffeeScript major mode."
+  :group 'languages)
 
-(defvar coffee-command "coffee"
-  "The CoffeeScript command used for evaluating code. Must be in your
-path.")
+(defcustom coffee-debug-mode nil
+  "Whether to run in debug mode or not. Logs to `*Messages*'."
+  :type 'boolean
+  :group 'coffee-mode)
+
+(defcustom coffee-js-mode 'js2-mode
+  "The mode to use when viewing compiled JavaScript."
+  :type 'string
+  :group 'coffee)
+
+(defcustom coffee-cleanup-whitespace t
+  "Should we `delete-trailing-whitespace' on save? Probably."
+  :type 'boolean
+  :group 'coffee)
 
-(defvar coffee-repl-args '("-i")
-  "The command line arguments to pass to `coffee-command' to start a REPL.")
+(defcustom coffee-tab-width tab-width
+  "The tab width to use when indenting."
+  :type 'integer
+  :group 'coffee)
 
-(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.")
+(defcustom coffee-command "coffee"
+  "The CoffeeScript command used for evaluating code. Must be in your
+path."
+  :type 'string
+  :group 'coffee)
+
+(defcustom coffee-args-repl '("-i")
+  "The command line arguments to pass to `coffee-command' to start a REPL."
+  :type 'list
+  :group 'coffee)
 
-(defun coffee-command-full ()
-  "The full `coffee-command' complete with args."
-  (mapconcat 'identity (append (list coffee-command) coffee-command-args) " "))
+(defcustom coffee-args-compile '("-c")
+  "The command line arguments to pass to `coffee-command' when compiling a file."
+  :type 'list
+  :group 'coffee)
 
-(defvar coffee-js-mode 'js2-mode
-  "The mode to use when viewing compiled JavaScript.")
+(defcustom coffee-compiled-buffer-name "*coffee-compiled*"
+  "The name of the scratch buffer used when compiling CoffeeScript."
+  :type 'string
+  :group 'coffee)
 
-(defvar coffee-compiled-buffer-name "*coffee-compiled*"
-  "The name of the scratch buffer used when compiling CoffeeScript.")
+(defvar coffee-mode-hook nil
+  "A hook for you to run your own code when the mode is loaded.")
 
 (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 (and (boundp 'coffee-debug-mode) coffee-debug-mode)
+      `(progn
+         (coffee-debug "%s: %s" ',var ,val)
+         (setq ,var ,val))
+    `(setq ,var ,val)))
+
+(defun coffee-debug (string &rest args)
+  "Print a message when in debug mode."
+  (when coffee-debug-mode
+      (apply 'message (append (list string) args))))
+
+(defmacro coffee-line-as-string ()
+  "Returns the current line as a string."
+  `(buffer-substring (point-at-bol) (point-at-eol)))
+
 ;;
 ;; Commands
 ;;
@@ -95,9 +152,22 @@ print the compiled JavaScript.")
   (unless (comint-check-proc "*CoffeeREPL*")
     (set-buffer
      (apply 'make-comint "CoffeeREPL"
-            coffee-command nil coffee-repl-args)))
+            coffee-command nil coffee-args-repl)))
+
+  (pop-to-buffer "*CoffeeREPL*"))
+
+(defun coffee-compiled-file-name (&optional filename)
+  "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 (or filename (buffer-file-name))) ".js"))
 
-  (pop-to-buffer "*CoffeeScript*"))
+(defun coffee-compile-file ()
+  "Compiles and saves the current file to disk. Doesn't open in a buffer.."
+  (interactive)
+  (let ((compiler-output (shell-command-to-string (coffee-command-compile (buffer-file-name)))))
+    (if (string= compiler-output "")
+        (message "Compiled and saved %s" (coffee-compiled-file-name))
+      (message (car (split-string compiler-output "[\n\r]+"))))))
 
 (defun coffee-compile-buffer ()
   "Compiles the current buffer and displays the JS in another buffer."
@@ -116,10 +186,10 @@ print the compiled JavaScript.")
   (call-process-region start end coffee-command nil
                        (get-buffer-create coffee-compiled-buffer-name)
                        nil
-                       "-s" "-p" "--no-wrap")
-  (switch-to-buffer-other-frame (get-buffer coffee-compiled-buffer-name))
+                       "-s" "-p" "--bare")
+  (switch-to-buffer (get-buffer coffee-compiled-buffer-name))
   (funcall coffee-js-mode)
-  (beginning-of-buffer))
+  (goto-char (point-min)))
 
 (defun coffee-show-version ()
   "Prints the `coffee-mode' version."
@@ -131,6 +201,11 @@ print the compiled JavaScript.")
   (interactive)
   (browse-url "http://jashkenas.github.com/coffee-script/"))
 
+(defun coffee-open-node-reference ()
+  "Open browser to node.js documentation."
+  (interactive)
+  (browse-url "http://nodejs.org/docs/"))
+
 (defun coffee-open-github ()
   "Open browser to `coffee-mode' project on GithHub."
   (interactive)
@@ -143,11 +218,13 @@ print the compiled JavaScript.")
 (easy-menu-define coffee-mode-menu coffee-mode-map
   "Menu for CoffeeScript mode"
   '("CoffeeScript"
+    ["Compile File" coffee-compile-file]
     ["Compile Buffer" coffee-compile-buffer]
     ["Compile Region" coffee-compile-region]
     ["REPL" coffee-repl]
     "---"
-    ["CoffeeScript reference" coffee-open-reference]
+    ["CoffeeScript Reference" coffee-open-reference]
+    ["node.js Reference" coffee-open-node-reference]
     ["coffee-mode on GitHub" coffee-open-github]
     ["Version" coffee-show-version]
     ))
@@ -156,24 +233,36 @@ print the compiled JavaScript.")
 ;; Define Language Syntax
 ;;
 
+;; String literals
+(defvar coffee-string-regexp "\"\\([^\\]\\|\\\\.\\)*?\"\\|'\\([^\\]\\|\\\\.\\)*?'")
+
 ;; Instance variables (implicit this)
-(defvar coffee-this-regexp "@\\w*\\|this")
+(defvar coffee-this-regexp "@\\(\\w\\|_\\)*\\|this")
+
+;; Prototype::access
+(defvar coffee-prototype-regexp "\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\)::\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\):")
 
 ;; Assignment
-(defvar coffee-assign-regexp "\\(\\w\\|\\.\\|_\\| \\|$\\)+?:")
+(defvar coffee-assign-regexp "\\(\\(\\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\\)\\b")
+(defvar coffee-boolean-regexp "\\b\\(true\\|false\\|yes\\|no\\|on\\|off\\|null\\)\\b")
 
 ;; Regular Expressions
-(defvar coffee-regexp-regexp "\\/.+?\\/")
+(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"
-        "class"))
+        "class" "until" "loop"))
 
 ;; Reserved keywords either by JS or CS.
 (defvar coffee-js-reserved
@@ -194,26 +283,30 @@ print the compiled JavaScript.")
                                  coffee-cs-keywords) 'words))
 
 
-;; Create the list for font-lock.
-;; Each class of keyword is given a particular face
+;; Create the list for font-lock. Each class of keyword is given a
+;; particular face.
 (defvar coffee-font-lock-keywords
-      `(
-        (,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)
-
-        ;; note: order above matters. `coffee-keywords-regexp' goes last because
-        ;; otherwise the keyword "state" in the function "state_entry"
-        ;; would be highlighted.
-        ))
+  ;; *Note*: order below matters. `coffee-keywords-regexp' goes last
+  ;; because otherwise the keyword "state" in the function
+  ;; "state_entry" would be highlighted.
+  `((,coffee-string-regexp . font-lock-string-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-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-before-save ()
+  "Hook run before file is saved. Deletes whitespace if
+`coffee-cleanup-whitespace' is non-nil."
+  (when coffee-cleanup-whitespace
+    (delete-trailing-whitespace)))
+
 (defun coffee-comment-dwim (arg)
   "Comment or uncomment current line or region in a smart way.
 For detail, see `comment-dwim'."
@@ -222,6 +315,120 @@ For detail, see `comment-dwim'."
   (let ((deactivate-mark nil) (comment-start "#") (comment-end ""))
     (comment-dwim arg)))
 
+(defun coffee-command-compile (file-name)
+  "The `coffee-command' with args to compile a file."
+  (mapconcat 'identity (append (list coffee-command) coffee-args-compile (list file-name)) " "))
+
+;;
+;; 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 ' + this.name + '.'
+;;
+;; class Policeman extends Person
+;;   constructor: (rank) ->
+;;     @rank: rank
+;;   print: ->
+;;     print 'My name is ' + this.name + " 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."
+  (interactive)
+
+  ;; 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 *\\)"
+                    "\\("
+                      coffee-assign-regexp
+                      ".+?"
+                      coffee-lambda-regexp
+                    "\\|"
+                      coffee-namespace-regexp
+                    "\\)")
+            (point-max)
+            t)
+
+      (coffee-debug "Match: %s" (match-string 0))
+
+      ;; 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)))
+
+        ;; Debug
+        (coffee-debug "ns: Found %s with indent %s" ns-name ns-indent))
+
+      ;; 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)))
+
+          (coffee-debug "=: Found %s with indent %s" assign indent)
+
+          ;; Clear the namespace if we're no longer indented deeper
+          ;; than it.
+          (when (and ns-name (<= indent ns-indent))
+            (coffee-debug "ns: Clearing %s" ns-name)
+            (setq ns-name nil)
+            (setq ns-indent nil))
+
+          ;; Add this to the alist. Done.
+          (push (cons assign pos) index-alist)))
+
+    ;; Return the alist.
+    index-alist))
+
 ;;
 ;; Indentation
 ;;
@@ -232,24 +439,48 @@ For detail, see `comment-dwim'."
   "Indent current line as CoffeeScript."
   (interactive)
 
-  (save-excursion
-    (let ((prev-indent 0) (cur-indent 0))
-      ;; Figure out the indentation of the previous line
-      (forward-line -1)
-      (setq prev-indent (current-indentation))
+  (if (= (point) (point-at-bol))
+      (insert-tab)
+    (save-excursion
+      (let ((prev-indent 0) (cur-indent 0))
+        ;; Figure out the indentation of the previous line
+        (setd prev-indent (coffee-previous-indent))
 
-      ;; Figure out the current line's indentation
-      (forward-line 1)
-      (setq cur-indent (current-indentation))
+        ;; Figure out the current line's indentation
+        (setd cur-indent (current-indentation))
 
-      ;; Shift one column to the left
-      (backward-to-indentation 0)
-      (insert-tab)
+        ;; Shift one column to the left
+        (beginning-of-line)
+        (insert-tab)
 
-      ;; We're too far, remove all indentation.
-      (when (> (- (current-indentation) prev-indent) tab-width)
-        (backward-to-indentation 0)
-        (delete-region (point-at-bol) (point))))))
+        (coffee-debug "point: %s" (point))
+        (coffee-debug "point-at-bol: %s" (point-at-bol))
+
+        (when (= (point-at-bol) (point))
+          (forward-char coffee-tab-width))
+
+        (coffee-debug "New indent: %s" (current-indentation))
+
+        ;; 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."
+
+  (save-excursion
+    (forward-line -1)
+    (if (bobp)
+        0
+      (progn
+        (while (and (coffee-line-empty-p) (not (bobp))) (forward-line -1))
+        (current-indentation)))))
+
+(defun coffee-line-empty-p ()
+  "Is this line empty? Returns non-nil if so, nil if not."
+  (or (bobp)
+   (string-match "^\\s *$" (coffee-line-as-string))))
 
 (defun coffee-newline-and-indent ()
   "Inserts a newline and indents it to the same level as the previous line."
@@ -259,8 +490,9 @@ For detail, see `comment-dwim'."
   ;; 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)
     (newline)
-    (insert-tab (/ prev-indent tab-width))
+    (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)
@@ -283,7 +515,7 @@ 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)))
+  (regexp-opt coffee-indenters-bol 'words))
 
 (defvar coffee-indenters-eol '(?> ?{ ?\[)
   "Single characters at the end of a line that mean the next line
@@ -303,7 +535,7 @@ line? Returns `t' or `nil'. See the README for more details."
       ;; 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))
+        (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
@@ -312,10 +544,10 @@ line? Returns `t' or `nil'. See the README for more details."
         (end-of-line)
 
         ;; Optimized for speed - checks only the last character.
-        (when (select coffee-indenters-eol
-                      (lambda (char)
-                        (= (char-before) char)))
-          (setq indenter-at-eol t)))
+        (when (some (lambda (char)
+                        (= (char-before) char))
+                      coffee-indenters-eol)
+          (setd indenter-at-eol t)))
 
       ;; If we found an indenter, return `t'.
       (or indenter-at-bol indenter-at-eol))))
@@ -336,15 +568,18 @@ line? Returns `t' or `nil'. See the README for more details."
 ;; Define Major Mode
 ;;
 
+;;;###autoload
 (define-derived-mode coffee-mode fundamental-mode
-  "coffee-mode"
-  "Major mode for editing CoffeeScript..."
+  "Coffee"
+  "Major mode for editing CoffeeScript."
 
+  ;; key bindings
   (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-R") 'coffee-compile-region)
   (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)
+  (define-key coffee-mode-map "\C-c\C-o\C-s" 'coffee-cos-mode)
 
   ;; code for syntax highlighting
   (setq font-lock-defaults '((coffee-font-lock-keywords)))
@@ -352,25 +587,42 @@ line? Returns `t' or `nil'. See the README for more details."
   ;; perl style comment: "# ..."
   (modify-syntax-entry ?# "< b" coffee-mode-syntax-table)
   (modify-syntax-entry ?\n "> b" coffee-mode-syntax-table)
+  (make-local-variable 'comment-start)
   (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)
+  (set (make-local-variable 'tab-width) coffee-tab-width)
+
+  ;; 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
-  (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))
+  ;; hooks
+  (set (make-local-variable 'before-save-hook) 'coffee-before-save))
+
+;;
+;; 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."
+  :group 'coffee-cos :lighter coffee-cos-mode-line
+  (cond
+   (coffee-cos-mode
+    (add-hook 'after-save-hook 'coffee-compile-file nil t))
+   (t
+    (remove-hook 'after-save-hook 'coffee-compile-file t))))
 
 (provide 'coffee-mode)
 
@@ -379,5 +631,7 @@ line? Returns `t' or `nil'. See the README for more details."
 ;;
 
 ;; Run coffee-mode for files ending in .coffee.
+;;;###autoload
 (add-to-list 'auto-mode-alist '("\\.coffee$" . coffee-mode))
+;;;###autoload
 (add-to-list 'auto-mode-alist '("Cakefile" . coffee-mode))