X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/d6b78ed7025c79d77e78dccae9fa7ac3a08c0dbc..4ac3eaa76d4a25ea11ce4c7e5d380d54bdefeed2:/coffee-mode.el diff --git a/coffee-mode.el b/coffee-mode.el index 5f254b04e..27a2e6e25 100644 --- a/coffee-mode.el +++ b/coffee-mode.el @@ -1,29 +1,268 @@ +;;; coffee-mode.el --- Major mode to edit CoffeeScript files in Emacs + +;; Copyright (C) 2010 Chris Wanstrath + +;; Version 0.3.0 +;; Keywords: CoffeeScript major mode +;; Author: Chris Wanstrath +;; URL: http://github.com/defunkt/coffee-script + +;; 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 +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; 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 README.md or +;; http://github.com/defunkt/coffee-mode#readme + +;;; Installation + +;; In your shell: + +;; $ cd ~/.emacs.d/vendor +;; $ git clone git://github.com/defunkt/coffee-mode.git + +;; In your emacs config: + +;; (add-to-list 'load-path "~/.emacs.d/vendor/coffee-mode") +;; (require 'coffee-mode) + +;;; Thanks + ;; Major thanks to http://xahlee.org/emacs/elisp_syntax_coloring.html ;; the instructions. +;; 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.3.0" + "The version of this `coffee-mode'.") + +(defgroup coffee nil + "A CoffeeScript major mode." + :group 'languages) + +(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) + +(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. 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) + +(defcustom coffee-args-compile '("-c") + "The command line arguments to pass to `coffee-command' when compiling a file." + :type 'list + :group 'coffee) + +(defcustom coffee-compiled-buffer-name "*coffee-compiled*" + "The name of the scratch buffer used when compiling CoffeeScript." + :type 'string + :group 'coffee) + +(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 +;; + +(defun coffee-repl () + "Launch a CoffeeScript REPL using `coffee-command' as an inferior mode." + (interactive) + + (unless (comint-check-proc "*CoffeeREPL*") + (set-buffer + (apply 'make-comint "CoffeeREPL" + 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")) + +(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." + (interactive) + (save-excursion + (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) + nil + "-s" "-p" "--bare") + (switch-to-buffer (get-buffer coffee-compiled-buffer-name)) + (funcall coffee-js-mode) + (goto-char (point-min))) + +(defun coffee-show-version () + "Prints the `coffee-mode' version." + (interactive) + (message (concat "coffee-mode v" coffee-mode-version))) + +(defun coffee-open-reference () + "Open browser to CoffeeScript reference." + (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) + (browse-url "http://github.com/defunkt/coffee-mode")) + +;; +;; Menubar +;; + +(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] + ["node.js Reference" coffee-open-node-reference] + ["coffee-mode on GitHub" coffee-open-github] + ["Version" coffee-show-version] + )) ;; ;; Define Language Syntax ;; -;; Assignment -(defvar coffee-type-regexp ".+?:") +;; String literals +(defvar coffee-string-regexp "\"\\([^\\]\\|\\\\.\\)*?\"\\|'\\([^\\]\\|\\\\.\\)*?'") ;; Instance variables (implicit this) -(defvar coffee-constant-regexp "@\\w*\\|this") +(defvar coffee-this-regexp "@\\(\\w\\|_\\)*\\|this") + +;; Prototype::access +(defvar coffee-prototype-regexp "\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\)::\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\):") + +;; Assignment +(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-functions-regexp "\\b\\(true\\|false\\|yes\\|no\\|on\\|off\\)\\b") +(defvar coffee-boolean-regexp "\\b\\(true\\|false\\|yes\\|no\\|on\\|off\\|null\\)\\b") -;; Unused -(defvar coffee-event-regexp "") +;; 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" - "class")) + "class" "until" "loop")) ;; Reserved keywords either by JS or CS. (defvar coffee-js-reserved @@ -44,27 +283,30 @@ 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-type-regexp . font-lock-type-face) - (,coffee-constant-regexp . font-lock-variable-name-face) - (,coffee-event-regexp . font-lock-builtin-face) - (,coffee-functions-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'." @@ -73,59 +315,323 @@ 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 +;; + +;;; The theory is explained in the README. + (defun coffee-indent-line () - "Indent current line as CoffeeScript" + "Indent current line as CoffeeScript." (interactive) + (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 + (setd cur-indent (current-indentation)) + + ;; Shift one column to the left + (beginning-of-line) + (insert-tab) + + (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 - (let ((prev-indent 0) (cur-indent 0)) - (beginning-of-line) - (forward-line -1) - (setq prev-indent (current-indentation)) - (forward-line 1) - (setq cur-indent (current-indentation)) + (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." + (interactive) + + ;; 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) + (newline) + (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) + (insert-tab))) + + ;; 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." + (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." + (interactive) + + (save-excursion + (let ((indenter-at-bol) (indenter-at-eol)) + ;; Go back a line and to the first character. + (forward-line -1) (backward-to-indentation 0) - (insert-tab)))) + + ;; 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) + (end-of-line) + + ;; Optimized for speed - checks only the last character. + (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)))) + +(defun coffee-previous-line-is-comment () + "Returns `t' if the previous line is a CoffeeScript comment." + (save-excursion + (forward-line -1) + (coffee-line-is-comment))) + +(defun coffee-line-is-comment () + "Returns `t' if the current line is a CoffeeScript comment." + (save-excursion + (backward-to-indentation 0) + (= (char-after) (string-to-char "#")))) ;; ;; 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-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))) - ;; modify the keymap - (define-key coffee-mode-map [remap comment-dwim] 'coffee-comment-dwim) - ;; 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) - - ;; regular expressions - (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) + +;; +;; On Load +;; + +;; 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))