]> code.delx.au - gnu-emacs-elpa/blob - coffee-mode.el
Fix compiling `coffee-mode' without loading it.
[gnu-emacs-elpa] / coffee-mode.el
1 ;;; coffee-mode.el --- Major mode to edit CoffeeScript files in Emacs
2
3 ;; Copyright (C) 2010 Chris Wanstrath
4
5 ;; Version 0.3.0
6 ;; Keywords: CoffeeScript major mode
7 ;; Author: Chris Wanstrath <chris@ozmm.org>
8 ;; URL: http://github.com/defunkt/coffee-script
9
10 ;; This file is not part of GNU Emacs.
11
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; any later version.
16
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
21
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program; if not, write to the Free Software
24 ;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
25
26 ;;; Commentary
27
28 ;; For commentary please see the README.md or
29 ;; http://github.com/defunkt/coffee-mode#readme
30
31 ;;; Installation
32
33 ;; In your shell:
34
35 ;; $ cd ~/.emacs.d/vendor
36 ;; $ git clone git://github.com/defunkt/coffee-mode.git
37
38 ;; In your emacs config:
39
40 ;; (add-to-list 'load-path "~/.emacs.d/vendor/coffee-mode")
41 ;; (require 'coffee-mode)
42
43 ;;; Thanks
44
45 ;; Major thanks to http://xahlee.org/emacs/elisp_syntax_coloring.html
46 ;; the instructions.
47
48 ;; Also thanks to Jason Blevins's markdown-mode.el and Steve Yegge's
49 ;; js2-mode for guidance.
50
51 ;; TODO:
52 ;; - Execute {buffer,region,line} and show output in new buffer
53 ;; - Make prototype accessor assignments like `String::length: -> 10` pretty.
54 ;; - mirror-mode - close brackets and parens automatically
55
56 ;;; Code:
57
58 (require 'comint)
59 (require 'easymenu)
60 (require 'font-lock)
61
62 (eval-when-compile
63 (require 'cl))
64
65 ;;
66 ;; Customizable Variables
67 ;;
68
69 (defconst coffee-mode-version "0.3.0"
70 "The version of this `coffee-mode'.")
71
72 (defgroup coffee nil
73 "A CoffeeScript major mode."
74 :group 'languages)
75
76 (defcustom coffee-debug-mode nil
77 "Whether to run in debug mode or not. Logs to `*Messages*'."
78 :type 'boolean
79 :group 'coffee-mode)
80
81 (defcustom coffee-js-mode 'js2-mode
82 "The mode to use when viewing compiled JavaScript."
83 :type 'string
84 :group 'coffee)
85
86 (defcustom coffee-cleanup-whitespace t
87 "Should we `delete-trailing-whitespace' on save? Probably."
88 :type 'boolean
89 :group 'coffee)
90
91 (defcustom coffee-tab-width tab-width
92 "The tab width to use when indenting."
93 :type 'integer
94 :group 'coffee)
95
96 (defcustom coffee-command "coffee"
97 "The CoffeeScript command used for evaluating code. Must be in your
98 path."
99 :type 'string
100 :group 'coffee)
101
102 (defcustom coffee-args-repl '("-i")
103 "The command line arguments to pass to `coffee-command' to start a REPL."
104 :type 'list
105 :group 'coffee)
106
107 (defcustom coffee-args-compile '("-s" "-p" "--no-wrap")
108 "The command line arguments to pass to `coffee-command' to get it to
109 print the compiled JavaScript."
110 :type 'list
111 :group 'coffee)
112
113 (defcustom coffee-compiled-buffer-name "*coffee-compiled*"
114 "The name of the scratch buffer used when compiling CoffeeScript."
115 :type 'string
116 :group 'coffee)
117
118 (defvar coffee-mode-hook nil
119 "A hook for you to run your own code when the mode is loaded.")
120
121 (defvar coffee-mode-map (make-keymap)
122 "Keymap for CoffeeScript major mode.")
123
124 ;;
125 ;; Macros
126 ;;
127
128 (defmacro setd (var val)
129 "Like setq but optionally logs the variable's value using `coffee-debug'."
130 (if (and (boundp 'coffee-debug-mode) coffee-debug-mode)
131 `(progn
132 (coffee-debug "%s: %s" ',var ,val)
133 (setq ,var ,val))
134 `(setq ,var ,val)))
135
136 (defun coffee-debug (string &rest args)
137 "Print a message when in debug mode."
138 (when coffee-debug-mode
139 (apply 'message (append (list string) args))))
140
141 (defmacro coffee-line-as-string ()
142 "Returns the current line as a string."
143 `(buffer-substring (point-at-bol) (point-at-eol)))
144
145 ;;
146 ;; Commands
147 ;;
148
149 (defun coffee-repl ()
150 "Launch a CoffeeScript REPL using `coffee-command' as an inferior mode."
151 (interactive)
152
153 (unless (comint-check-proc "*CoffeeREPL*")
154 (set-buffer
155 (apply 'make-comint "CoffeeREPL"
156 coffee-command nil coffee-args-repl)))
157
158 (pop-to-buffer "*CoffeeScript*"))
159
160 (defun coffee-compile-file ()
161 "Compiles and saves the current file to disk. Doesn't open in a buffer.."
162 (interactive)
163 (shell-command (concat coffee-command " -c " (buffer-file-name)))
164 (message "Compiled and saved %s"
165 (concat
166 (substring (buffer-file-name) 0 -6)
167 "js")))
168
169 (defun coffee-compile-buffer ()
170 "Compiles the current buffer and displays the JS in another buffer."
171 (interactive)
172 (save-excursion
173 (coffee-compile-region (point-min) (point-max))))
174
175 (defun coffee-compile-region (start end)
176 "Compiles a region and displays the JS in another buffer."
177 (interactive "r")
178
179 (let ((buffer (get-buffer coffee-compiled-buffer-name)))
180 (when buffer
181 (kill-buffer buffer)))
182
183 (call-process-region start end coffee-command nil
184 (get-buffer-create coffee-compiled-buffer-name)
185 nil
186 "-s" "-p" "--no-wrap")
187 (switch-to-buffer (get-buffer coffee-compiled-buffer-name))
188 (funcall coffee-js-mode)
189 (goto-char (point-min)))
190
191 (defun coffee-show-version ()
192 "Prints the `coffee-mode' version."
193 (interactive)
194 (message (concat "coffee-mode v" coffee-mode-version)))
195
196 (defun coffee-open-reference ()
197 "Open browser to CoffeeScript reference."
198 (interactive)
199 (browse-url "http://jashkenas.github.com/coffee-script/"))
200
201 (defun coffee-open-node-reference ()
202 "Open browser to node.js reference."
203 (interactive)
204 (browse-url "http://nodejs.org/api.html"))
205
206 (defun coffee-open-github ()
207 "Open browser to `coffee-mode' project on GithHub."
208 (interactive)
209 (browse-url "http://github.com/defunkt/coffee-mode"))
210
211 ;;
212 ;; Menubar
213 ;;
214
215 (easy-menu-define coffee-mode-menu coffee-mode-map
216 "Menu for CoffeeScript mode"
217 '("CoffeeScript"
218 ["Compile File" coffee-compile-file]
219 ["Compile Buffer" coffee-compile-buffer]
220 ["Compile Region" coffee-compile-region]
221 ["REPL" coffee-repl]
222 "---"
223 ["CoffeeScript Reference" coffee-open-reference]
224 ["node.js Reference" coffee-open-node-reference]
225 ["coffee-mode on GitHub" coffee-open-github]
226 ["Version" coffee-show-version]
227 ))
228
229 ;;
230 ;; Define Language Syntax
231 ;;
232
233 ;; Instance variables (implicit this)
234 (defvar coffee-this-regexp "@\\(\\w\\|_\\)*\\|this")
235
236 ;; Prototype::access
237 (defvar coffee-prototype-regexp "\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\)::\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\):")
238
239 ;; Assignment
240 (defvar coffee-assign-regexp "\\(\\(\\w\\|\\.\\|_\\| \\|$\\)+?\\):")
241
242 ;; Lambda
243 (defvar coffee-lambda-regexp "\\((.+)\\)?\\s *\\(->\\|=>\\)")
244
245 ;; Namespaces
246 (defvar coffee-namespace-regexp "\\b\\(class\\s +\\(\\S +\\)\\)\\b")
247
248 ;; Booleans
249 (defvar coffee-boolean-regexp "\\b\\(true\\|false\\|yes\\|no\\|on\\|off\\|null\\)\\b")
250
251 ;; Regular Expressions
252 (defvar coffee-regexp-regexp "\\/.+?\\/")
253
254 ;; JavaScript Keywords
255 (defvar coffee-js-keywords
256 '("if" "else" "new" "return" "try" "catch"
257 "finally" "throw" "break" "continue" "for" "in" "while"
258 "delete" "instanceof" "typeof" "switch" "super" "extends"
259 "class"))
260
261 ;; Reserved keywords either by JS or CS.
262 (defvar coffee-js-reserved
263 '("case" "default" "do" "function" "var" "void" "with"
264 "const" "let" "debugger" "enum" "export" "import" "native"
265 "__extends" "__hasProp"))
266
267 ;; CoffeeScript keywords.
268 (defvar coffee-cs-keywords
269 '("then" "unless" "and" "or" "is"
270 "isnt" "not" "of" "by" "where" "when"))
271
272 ;; Regular expression combining the above three lists.
273 (defvar coffee-keywords-regexp (regexp-opt
274 (append
275 coffee-js-reserved
276 coffee-js-keywords
277 coffee-cs-keywords) 'words))
278
279
280 ;; Create the list for font-lock. Each class of keyword is given a
281 ;; particular face.
282 (defvar coffee-font-lock-keywords
283 ;; *Note*: order below matters. `coffee-keywords-regexp' goes last
284 ;; because otherwise the keyword "state" in the function
285 ;; "state_entry" would be highlighted.
286 `((,coffee-this-regexp . font-lock-variable-name-face)
287 (,coffee-prototype-regexp . font-lock-variable-name-face)
288 (,coffee-assign-regexp . font-lock-type-face)
289 (,coffee-regexp-regexp . font-lock-constant-face)
290 (,coffee-boolean-regexp . font-lock-constant-face)
291 (,coffee-keywords-regexp . font-lock-keyword-face)))
292
293 ;;
294 ;; Helper Functions
295 ;;
296
297 (defun coffee-before-save ()
298 "Hook run before file is saved. Deletes whitespace if
299 `coffee-cleanup-whitespace' is non-nil."
300 (when coffee-cleanup-whitespace
301 (delete-trailing-whitespace)))
302
303 (defun coffee-comment-dwim (arg)
304 "Comment or uncomment current line or region in a smart way.
305 For detail, see `comment-dwim'."
306 (interactive "*P")
307 (require 'newcomment)
308 (let ((deactivate-mark nil) (comment-start "#") (comment-end ""))
309 (comment-dwim arg)))
310
311 (defun coffee-command-full ()
312 "The full `coffee-command' complete with args."
313 (mapconcat 'identity (append (list coffee-command) coffee-args-compile) " "))
314
315 ;;
316 ;; imenu support
317 ;;
318
319 ;; This is a pretty naive but workable way of doing it. First we look
320 ;; for any lines that starting with `coffee-assign-regexp' that include
321 ;; `coffee-lambda-regexp' then add those tokens to the list.
322 ;;
323 ;; Should cover cases like these:
324 ;;
325 ;; minus: (x, y) -> x - y
326 ;; String::length: -> 10
327 ;; block: ->
328 ;; print('potion')
329 ;;
330 ;; Next we look for any line that starts with `class' or
331 ;; `coffee-assign-regexp' followed by `{` and drop into a
332 ;; namespace. This means we search one indentation level deeper for
333 ;; more assignments and add them to the alist prefixed with the
334 ;; namespace name.
335 ;;
336 ;; Should cover cases like these:
337 ;;
338 ;; class Person
339 ;; print: ->
340 ;; print 'My name is ' + this.name + '.'
341 ;;
342 ;; class Policeman extends Person
343 ;; constructor: (rank) ->
344 ;; @rank: rank
345 ;; print: ->
346 ;; print 'My name is ' + this.name + " and I'm a " + this.rank + '.'
347 ;;
348 ;; TODO:
349 ;; app = {
350 ;; window: {width: 200, height: 200}
351 ;; para: -> 'Welcome.'
352 ;; button: -> 'OK'
353 ;; }
354
355 (defun coffee-imenu-create-index ()
356 "Create an imenu index of all methods in the buffer."
357 (interactive)
358
359 ;; This function is called within a `save-excursion' so we're safe.
360 (goto-char (point-min))
361
362 (let ((index-alist '()) assign pos indent ns-name ns-indent)
363 ;; Go through every assignment that includes -> or => on the same
364 ;; line or starts with `class'.
365 (while (re-search-forward
366 (concat "^\\(\\s *\\)"
367 "\\("
368 coffee-assign-regexp
369 ".+?"
370 coffee-lambda-regexp
371 "\\|"
372 coffee-namespace-regexp
373 "\\)")
374 (point-max)
375 t)
376
377 (coffee-debug "Match: %s" (match-string 0))
378
379 ;; If this is the start of a new namespace, save the namespace's
380 ;; indentation level and name.
381 (when (match-string 8)
382 ;; Set the name.
383 (setq ns-name (match-string 8))
384
385 ;; If this is a class declaration, add :: to the namespace.
386 (setq ns-name (concat ns-name "::"))
387
388 ;; Save the indentation level.
389 (setq ns-indent (length (match-string 1)))
390
391 ;; Debug
392 (coffee-debug "ns: Found %s with indent %s" ns-name ns-indent))
393
394 ;; If this is an assignment, save the token being
395 ;; assigned. `Please.print:` will be `Please.print`, `block:`
396 ;; will be `block`, etc.
397 (when (setq assign (match-string 3))
398 ;; The position of the match in the buffer.
399 (setq pos (match-beginning 3))
400
401 ;; The indent level of this match
402 (setq indent (length (match-string 1)))
403
404 ;; If we're within the context of a namespace, add that to the
405 ;; front of the assign, e.g.
406 ;; constructor: => Policeman::constructor
407 (when (and ns-name (> indent ns-indent))
408 (setq assign (concat ns-name assign)))
409
410 (coffee-debug "=: Found %s with indent %s" assign indent)
411
412 ;; Clear the namespace if we're no longer indented deeper
413 ;; than it.
414 (when (and ns-name (<= indent ns-indent))
415 (coffee-debug "ns: Clearing %s" ns-name)
416 (setq ns-name nil)
417 (setq ns-indent nil))
418
419 ;; Add this to the alist. Done.
420 (push (cons assign pos) index-alist)))
421
422 ;; Return the alist.
423 index-alist))
424
425 ;;
426 ;; Indentation
427 ;;
428
429 ;;; The theory is explained in the README.
430
431 (defun coffee-indent-line ()
432 "Indent current line as CoffeeScript."
433 (interactive)
434
435 (if (= (point) (point-at-bol))
436 (insert-tab)
437 (save-excursion
438 (let ((prev-indent 0) (cur-indent 0))
439 ;; Figure out the indentation of the previous line
440 (setd prev-indent (coffee-previous-indent))
441
442 ;; Figure out the current line's indentation
443 (setd cur-indent (current-indentation))
444
445 ;; Shift one column to the left
446 (beginning-of-line)
447 (insert-tab)
448
449 (coffee-debug "point: %s" (point))
450 (coffee-debug "point-at-bol: %s" (point-at-bol))
451
452 (when (= (point-at-bol) (point))
453 (forward-char coffee-tab-width))
454
455 (coffee-debug "New indent: %s" (current-indentation))
456
457 ;; We're too far, remove all indentation.
458 (when (> (- (current-indentation) prev-indent) coffee-tab-width)
459 (backward-to-indentation 0)
460 (delete-region (point-at-bol) (point)))))))
461
462 (defun coffee-previous-indent ()
463 "Return the indentation level of the previous non-blank line."
464
465 (save-excursion
466 (forward-line -1)
467 (if (bobp)
468 0
469 (progn
470 (while (coffee-line-empty-p) (forward-line -1))
471 (current-indentation)))))
472
473 (defun coffee-line-empty-p ()
474 "Is this line empty? Returns non-nil if so, nil if not."
475 (or (bobp)
476 (string-match "^\\s *$" (coffee-line-as-string))))
477
478 (defun coffee-newline-and-indent ()
479 "Inserts a newline and indents it to the same level as the previous line."
480 (interactive)
481
482 ;; Remember the current line indentation level,
483 ;; insert a newline, and indent the newline to the same
484 ;; level as the previous line.
485 (let ((prev-indent (current-indentation)) (indent-next nil))
486 (newline)
487 (insert-tab (/ prev-indent coffee-tab-width))
488
489 ;; We need to insert an additional tab because the last line was special.
490 (when (coffee-line-wants-indent)
491 (insert-tab)))
492
493 ;; Last line was a comment so this one should probably be,
494 ;; too. Makes it easy to write multi-line comments (like the one I'm
495 ;; writing right now).
496 (when (coffee-previous-line-is-comment)
497 (insert "# ")))
498
499 ;; Indenters help determine whether the current line should be
500 ;; indented further based on the content of the previous line. If a
501 ;; line starts with `class', for instance, you're probably going to
502 ;; want to indent the next line.
503
504 (defvar coffee-indenters-bol '("class" "for" "if" "try")
505 "Keywords or syntax whose presence at the start of a line means the
506 next line should probably be indented.")
507
508 (defun coffee-indenters-bol-regexp ()
509 "Builds a regexp out of `coffee-indenters-bol' words."
510 (concat "^" (regexp-opt coffee-indenters-bol 'words)))
511
512 (defvar coffee-indenters-eol '(?> ?{ ?\[)
513 "Single characters at the end of a line that mean the next line
514 should probably be indented.")
515
516 (defun coffee-line-wants-indent ()
517 "Does the current line want to be indented deeper than the previous
518 line? Returns `t' or `nil'. See the README for more details."
519 (interactive)
520
521 (save-excursion
522 (let ((indenter-at-bol) (indenter-at-eol))
523 ;; Go back a line and to the first character.
524 (forward-line -1)
525 (backward-to-indentation 0)
526
527 ;; If the next few characters match one of our magic indenter
528 ;; keywords, we want to indent the line we were on originally.
529 (when (looking-at (coffee-indenters-bol-regexp))
530 (setd indenter-at-bol t))
531
532 ;; If that didn't match, go to the back of the line and check to
533 ;; see if the last character matches one of our indenter
534 ;; characters.
535 (when (not indenter-at-bol)
536 (end-of-line)
537
538 ;; Optimized for speed - checks only the last character.
539 (when (some (lambda (char)
540 (= (char-before) char))
541 coffee-indenters-eol)
542 (setd indenter-at-eol t)))
543
544 ;; If we found an indenter, return `t'.
545 (or indenter-at-bol indenter-at-eol))))
546
547 (defun coffee-previous-line-is-comment ()
548 "Returns `t' if the previous line is a CoffeeScript comment."
549 (save-excursion
550 (forward-line -1)
551 (coffee-line-is-comment)))
552
553 (defun coffee-line-is-comment ()
554 "Returns `t' if the current line is a CoffeeScript comment."
555 (save-excursion
556 (backward-to-indentation 0)
557 (= (char-after) (string-to-char "#"))))
558
559 ;;
560 ;; Define Major Mode
561 ;;
562
563 ;;;###autoload
564 (define-derived-mode coffee-mode fundamental-mode
565 "coffee-mode"
566 "Major mode for editing CoffeeScript..."
567
568 ;; key bindings
569 (define-key coffee-mode-map (kbd "A-r") 'coffee-compile-buffer)
570 (define-key coffee-mode-map (kbd "A-R") 'coffee-compile-region)
571 (define-key coffee-mode-map (kbd "A-M-r") 'coffee-repl)
572 (define-key coffee-mode-map [remap comment-dwim] 'coffee-comment-dwim)
573 (define-key coffee-mode-map "\C-m" 'coffee-newline-and-indent)
574
575 ;; code for syntax highlighting
576 (setq font-lock-defaults '((coffee-font-lock-keywords)))
577
578 ;; perl style comment: "# ..."
579 (modify-syntax-entry ?# "< b" coffee-mode-syntax-table)
580 (modify-syntax-entry ?\n "> b" coffee-mode-syntax-table)
581 (setq comment-start "#")
582
583 ;; single quote strings
584 (modify-syntax-entry ?' "\"" coffee-mode-syntax-table)
585 (modify-syntax-entry ?' "\"" coffee-mode-syntax-table)
586
587 ;; indentation
588 (make-local-variable 'indent-line-function)
589 (setq indent-line-function 'coffee-indent-line)
590 (setq coffee-tab-width tab-width) ;; Just in case...
591
592 ;; imenu
593 (make-local-variable 'imenu-create-index-function)
594 (setq imenu-create-index-function 'coffee-imenu-create-index)
595
596 ;; no tabs
597 (setq indent-tabs-mode nil)
598
599 ;; hooks
600 (set (make-local-variable 'before-save-hook) 'coffee-before-save))
601
602 (provide 'coffee-mode)
603
604 ;;
605 ;; On Load
606 ;;
607
608 ;; Run coffee-mode for files ending in .coffee.
609 ;;;###autoload
610 (add-to-list 'auto-mode-alist '("\\.coffee$" . coffee-mode))
611 ;;;###autoload
612 (add-to-list 'auto-mode-alist '("Cakefile" . coffee-mode))