]> code.delx.au - gnu-emacs-elpa/blob - packages/coffee-mode/coffee-mode.el
Add coffee-mode.
[gnu-emacs-elpa] / packages / coffee-mode / 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.4.1
6 ;; Keywords: CoffeeScript major mode
7 ;; Author: Chris Wanstrath <chris@ozmm.org>
8 ;; URL: http://github.com/defunkt/coffee-mode
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.4.1"
70 "The version of `coffee-mode'.")
71
72 (defgroup coffee nil
73 "A CoffeeScript major mode."
74 :group 'languages)
75
76 (defcustom coffee-tab-width tab-width
77 "The tab width to use when indenting."
78 :type 'integer
79 :group 'coffee)
80
81 (defcustom coffee-command "coffee"
82 "The CoffeeScript command used for evaluating code."
83 :type 'string
84 :group 'coffee)
85
86 (defcustom js2coffee-command "js2coffee"
87 "The js2coffee command used for evaluating code."
88 :type 'string
89 :group 'coffee)
90
91 (defcustom coffee-args-repl '("-i")
92 "The arguments to pass to `coffee-command' to start a REPL."
93 :type 'list
94 :group 'coffee)
95
96 (defcustom coffee-args-compile '("-c")
97 "The arguments to pass to `coffee-command' to compile a file."
98 :type 'list
99 :group 'coffee)
100
101 (defcustom coffee-compiled-buffer-name "*coffee-compiled*"
102 "The name of the scratch buffer used for compiled CoffeeScript."
103 :type 'string
104 :group 'coffee)
105
106 (defcustom coffee-compile-jump-to-error t
107 "Whether to jump to the first error if compilation fails.
108 Please note that the coffee compiler doesn't always give a line
109 number for the issue and in that case it is not possible to jump
110 to the error."
111 :type 'boolean
112 :group 'coffee)
113
114 (defcustom coffee-watch-buffer-name "*coffee-watch*"
115 "The name of the scratch buffer used when using the --watch flag
116 with CoffeeScript."
117 :type 'string
118 :group 'coffee)
119
120 (defcustom coffee-mode-hook nil
121 "Hook called by `coffee-mode'."
122 :type 'hook
123 :group 'coffee)
124
125 (defvar coffee-mode-map (make-keymap)
126 "Keymap for CoffeeScript major mode.")
127
128 ;;
129 ;; Commands
130 ;;
131
132 (defun coffee-repl ()
133 "Launch a CoffeeScript REPL using `coffee-command' as an inferior mode."
134 (interactive)
135
136 (unless (comint-check-proc "*CoffeeREPL*")
137 (set-buffer
138 (apply 'make-comint "CoffeeREPL"
139 coffee-command nil coffee-args-repl)))
140
141 (pop-to-buffer "*CoffeeREPL*"))
142
143 (defun coffee-compiled-file-name (&optional filename)
144 "Returns the name of the JavaScript file compiled from a CoffeeScript file.
145 If FILENAME is omitted, the current buffer's file name is used."
146 (concat (file-name-sans-extension (or filename (buffer-file-name))) ".js"))
147
148 (defun coffee-compile-file ()
149 "Compiles and saves the current file to disk."
150 (interactive)
151 (let ((compiler-output (shell-command-to-string (coffee-command-compile (buffer-file-name)))))
152 (if (string= compiler-output "")
153 (message "Compiled and saved %s" (coffee-compiled-file-name))
154 (let* ((msg (car (split-string compiler-output "[\n\r]+")))
155 (line (and (string-match "on line \\([0-9]+\\)" msg)
156 (string-to-number (match-string 1 msg)))))
157 (message msg)
158 (when (and coffee-compile-jump-to-error line (> line 0))
159 (goto-char (point-min))
160 (forward-line (1- line)))))))
161
162 (defun coffee-compile-buffer ()
163 "Compiles the current buffer and displays the JavaScript in a buffer
164 called `coffee-compiled-buffer-name'."
165 (interactive)
166 (save-excursion
167 (coffee-compile-region (point-min) (point-max))))
168
169 (defun coffee-compile-region (start end)
170 "Compiles a region and displays the JavaScript in a buffer called
171 `coffee-compiled-buffer-name'."
172 (interactive "r")
173
174 (let ((buffer (get-buffer coffee-compiled-buffer-name)))
175 (when buffer
176 (kill-buffer buffer)))
177
178 (apply (apply-partially 'call-process-region start end coffee-command nil
179 (get-buffer-create coffee-compiled-buffer-name)
180 nil)
181 (append coffee-args-compile (list "-s" "-p")))
182 (switch-to-buffer (get-buffer coffee-compiled-buffer-name))
183 (let ((buffer-file-name "tmp.js")) (set-auto-mode))
184 (goto-char (point-min)))
185
186 (defun coffee-js2coffee-replace-region (start end)
187 "Convert JavaScript in the region into CoffeeScript."
188 (interactive "r")
189
190 (let ((buffer (get-buffer coffee-compiled-buffer-name)))
191 (when buffer
192 (kill-buffer buffer)))
193
194 (call-process-region start end
195 js2coffee-command nil
196 (current-buffer))
197 (delete-region start end))
198
199 (defun coffee-version ()
200 "Show the `coffee-mode' version in the echo area."
201 (interactive)
202 (message (concat "coffee-mode version " coffee-mode-version)))
203
204 (defun coffee-watch (dir-or-file)
205 "Run `coffee-run-cmd' with the --watch flag on a directory or file."
206 (interactive "fDirectory or File: ")
207 (let ((coffee-compiled-buffer-name coffee-watch-buffer-name)
208 (args (mapconcat 'identity (append coffee-args-compile (list "--watch" (expand-file-name dir-or-file))) " ")))
209 (coffee-run-cmd args)))
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 ["Version" coffee-show-version]
224 ))
225
226 ;;
227 ;; Define Language Syntax
228 ;;
229
230 ;; String literals
231 (defvar coffee-string-regexp "\"\\([^\\]\\|\\\\.\\)*?\"\\|'\\([^\\]\\|\\\\.\\)*?'")
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\\|\\.\\|_\\|$\\)+?\s*\\):")
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\\|undefined\\)\\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" "until" "loop"))
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-string-regexp . font-lock-string-face)
287 (,coffee-this-regexp . font-lock-variable-name-face)
288 (,coffee-prototype-regexp . font-lock-variable-name-face)
289 (,coffee-assign-regexp . font-lock-type-face)
290 (,coffee-regexp-regexp . font-lock-constant-face)
291 (,coffee-boolean-regexp . font-lock-constant-face)
292 (,coffee-keywords-regexp . font-lock-keyword-face)))
293
294 ;;
295 ;; Helper Functions
296 ;;
297
298 (defun coffee-comment-dwim (arg)
299 "Comment or uncomment current line or region in a smart way.
300 For details, see `comment-dwim'."
301 (interactive "*P")
302 (require 'newcomment)
303 (let ((deactivate-mark nil) (comment-start "#") (comment-end ""))
304 (comment-dwim arg)))
305
306 (defun coffee-command-compile (file-name)
307 "Run `coffee-command' to compile FILE."
308 (let ((full-file-name (expand-file-name file-name)))
309 (mapconcat 'identity (append (list coffee-command) coffee-args-compile (list full-file-name)) " ")))
310
311 (defun coffee-run-cmd (args)
312 "Run `coffee-command' with the given arguments, and display the
313 output in a compilation buffer."
314 (interactive "sArguments: ")
315 (let ((compilation-buffer-name-function (lambda (this-mode)
316 (generate-new-buffer-name coffee-compiled-buffer-name))))
317 (compile (concat coffee-command " " args))))
318
319 ;;
320 ;; imenu support
321 ;;
322
323 ;; This is a pretty naive but workable way of doing it. First we look
324 ;; for any lines that starting with `coffee-assign-regexp' that include
325 ;; `coffee-lambda-regexp' then add those tokens to the list.
326 ;;
327 ;; Should cover cases like these:
328 ;;
329 ;; minus: (x, y) -> x - y
330 ;; String::length: -> 10
331 ;; block: ->
332 ;; print('potion')
333 ;;
334 ;; Next we look for any line that starts with `class' or
335 ;; `coffee-assign-regexp' followed by `{` and drop into a
336 ;; namespace. This means we search one indentation level deeper for
337 ;; more assignments and add them to the alist prefixed with the
338 ;; namespace name.
339 ;;
340 ;; Should cover cases like these:
341 ;;
342 ;; class Person
343 ;; print: ->
344 ;; print 'My name is ' + this.name + '.'
345 ;;
346 ;; class Policeman extends Person
347 ;; constructor: (rank) ->
348 ;; @rank: rank
349 ;; print: ->
350 ;; print 'My name is ' + this.name + " and I'm a " + this.rank + '.'
351 ;;
352 ;; TODO:
353 ;; app = {
354 ;; window: {width: 200, height: 200}
355 ;; para: -> 'Welcome.'
356 ;; button: -> 'OK'
357 ;; }
358
359 (defun coffee-imenu-create-index ()
360 "Create an imenu index of all methods in the buffer."
361 (interactive)
362
363 ;; This function is called within a `save-excursion' so we're safe.
364 (goto-char (point-min))
365
366 (let ((index-alist '()) assign pos indent ns-name ns-indent)
367 ;; Go through every assignment that includes -> or => on the same
368 ;; line or starts with `class'.
369 (while (re-search-forward
370 (concat "^\\(\\s *\\)"
371 "\\("
372 coffee-assign-regexp
373 ".+?"
374 coffee-lambda-regexp
375 "\\|"
376 coffee-namespace-regexp
377 "\\)")
378 (point-max)
379 t)
380
381 ;; If this is the start of a new namespace, save the namespace's
382 ;; indentation level and name.
383 (when (match-string 8)
384 ;; Set the name.
385 (setq ns-name (match-string 8))
386
387 ;; If this is a class declaration, add :: to the namespace.
388 (setq ns-name (concat ns-name "::"))
389
390 ;; Save the indentation level.
391 (setq ns-indent (length (match-string 1))))
392
393 ;; If this is an assignment, save the token being
394 ;; assigned. `Please.print:` will be `Please.print`, `block:`
395 ;; will be `block`, etc.
396 (when (setq assign (match-string 3))
397 ;; The position of the match in the buffer.
398 (setq pos (match-beginning 3))
399
400 ;; The indent level of this match
401 (setq indent (length (match-string 1)))
402
403 ;; If we're within the context of a namespace, add that to the
404 ;; front of the assign, e.g.
405 ;; constructor: => Policeman::constructor
406 (when (and ns-name (> indent ns-indent))
407 (setq assign (concat ns-name assign)))
408
409 ;; Clear the namespace if we're no longer indented deeper
410 ;; than it.
411 (when (and ns-name (<= indent ns-indent))
412 (setq ns-name nil)
413 (setq ns-indent nil))
414
415 ;; Add this to the alist. Done.
416 (push (cons assign pos) index-alist)))
417
418 ;; Return the alist.
419 index-alist))
420
421 ;;
422 ;; Indentation
423 ;;
424
425 ;;; The theory is explained in the README.
426
427 (defun coffee-indent-line ()
428 "Indent current line as CoffeeScript."
429 (interactive)
430
431 (if (= (point) (point-at-bol))
432 (insert-tab)
433 (save-excursion
434 (let ((prev-indent (coffee-previous-indent))
435 (cur-indent (current-indentation)))
436 ;; Shift one column to the left
437 (beginning-of-line)
438 (insert-tab)
439
440 (when (= (point-at-bol) (point))
441 (forward-char coffee-tab-width))
442
443 ;; We're too far, remove all indentation.
444 (when (> (- (current-indentation) prev-indent) coffee-tab-width)
445 (backward-to-indentation 0)
446 (delete-region (point-at-bol) (point)))))))
447
448 (defun coffee-previous-indent ()
449 "Return the indentation level of the previous non-blank line."
450 (save-excursion
451 (forward-line -1)
452 (if (bobp)
453 0
454 (progn
455 (while (and (looking-at "^[ \t]*$") (not (bobp))) (forward-line -1))
456 (current-indentation)))))
457
458 (defun coffee-newline-and-indent ()
459 "Insert a newline and indent it to the same level as the previous line."
460 (interactive)
461
462 ;; Remember the current line indentation level,
463 ;; insert a newline, and indent the newline to the same
464 ;; level as the previous line.
465 (let ((prev-indent (current-indentation)) (indent-next nil))
466 (delete-horizontal-space t)
467 (newline)
468 (insert-tab (/ prev-indent coffee-tab-width))
469
470 ;; We need to insert an additional tab because the last line was special.
471 (when (coffee-line-wants-indent)
472 (insert-tab)))
473
474 ;; Last line was a comment so this one should probably be,
475 ;; too. Makes it easy to write multi-line comments (like the one I'm
476 ;; writing right now).
477 (when (coffee-previous-line-is-comment)
478 (insert "# ")))
479
480 ;; Indenters help determine whether the current line should be
481 ;; indented further based on the content of the previous line. If a
482 ;; line starts with `class', for instance, you're probably going to
483 ;; want to indent the next line.
484
485 (defvar coffee-indenters-bol '("class" "for" "if" "try")
486 "Keywords or syntax whose presence at the start of a line means the
487 next line should probably be indented.")
488
489 (defun coffee-indenters-bol-regexp ()
490 "Builds a regexp out of `coffee-indenters-bol' words."
491 (regexp-opt coffee-indenters-bol 'words))
492
493 (defvar coffee-indenters-eol '(?> ?{ ?\[)
494 "Single characters at the end of a line that mean the next line
495 should probably be indented.")
496
497 (defun coffee-line-wants-indent ()
498 "Return t if the current line should be indented relative to the
499 previous line."
500 (interactive)
501
502 (save-excursion
503 (let ((indenter-at-bol) (indenter-at-eol))
504 ;; Go back a line and to the first character.
505 (forward-line -1)
506 (backward-to-indentation 0)
507
508 ;; If the next few characters match one of our magic indenter
509 ;; keywords, we want to indent the line we were on originally.
510 (when (looking-at (coffee-indenters-bol-regexp))
511 (setq indenter-at-bol t))
512
513 ;; If that didn't match, go to the back of the line and check to
514 ;; see if the last character matches one of our indenter
515 ;; characters.
516 (when (not indenter-at-bol)
517 (end-of-line)
518
519 ;; Optimized for speed - checks only the last character.
520 (let ((indenters coffee-indenters-eol))
521 (while indenters
522 (if (/= (char-before) (car indenters))
523 (setq indenters (cdr indenters))
524 (setq indenter-at-eol t)
525 (setq indenters nil)))))
526
527 ;; If we found an indenter, return `t'.
528 (or indenter-at-bol indenter-at-eol))))
529
530 (defun coffee-previous-line-is-comment ()
531 "Return t if the previous line is a CoffeeScript comment."
532 (save-excursion
533 (forward-line -1)
534 (coffee-line-is-comment)))
535
536 (defun coffee-line-is-comment ()
537 "Return t if the current line is a CoffeeScript comment."
538 (save-excursion
539 (backward-to-indentation 0)
540 (= (char-after) (string-to-char "#"))))
541
542 ;;
543 ;; Define Major Mode
544 ;;
545
546 ;;;###autoload
547 (define-derived-mode coffee-mode fundamental-mode
548 "Coffee"
549 "Major mode for editing CoffeeScript."
550
551 ;; key bindings
552 (define-key coffee-mode-map (kbd "A-r") 'coffee-compile-buffer)
553 (define-key coffee-mode-map (kbd "A-R") 'coffee-compile-region)
554 (define-key coffee-mode-map (kbd "A-M-r") 'coffee-repl)
555 (define-key coffee-mode-map [remap comment-dwim] 'coffee-comment-dwim)
556 (define-key coffee-mode-map "\C-m" 'coffee-newline-and-indent)
557 (define-key coffee-mode-map "\C-c\C-o\C-s" 'coffee-cos-mode)
558
559 ;; code for syntax highlighting
560 (setq font-lock-defaults '((coffee-font-lock-keywords)))
561
562 ;; perl style comment: "# ..."
563 (modify-syntax-entry ?# "< b" coffee-mode-syntax-table)
564 (modify-syntax-entry ?\n "> b" coffee-mode-syntax-table)
565 (make-local-variable 'comment-start)
566 (setq comment-start "#")
567
568 ;; single quote strings
569 (modify-syntax-entry ?' "\"" coffee-mode-syntax-table)
570
571 ;; indentation
572 (make-local-variable 'indent-line-function)
573 (setq indent-line-function 'coffee-indent-line)
574 (set (make-local-variable 'tab-width) coffee-tab-width)
575
576 ;; imenu
577 (make-local-variable 'imenu-create-index-function)
578 (setq imenu-create-index-function 'coffee-imenu-create-index)
579
580 ;; no tabs
581 (setq indent-tabs-mode nil))
582
583 ;;
584 ;; Compile-on-Save minor mode
585 ;;
586
587 (defvar coffee-cos-mode-line " CoS")
588 (make-variable-buffer-local 'coffee-cos-mode-line)
589
590 (define-minor-mode coffee-cos-mode
591 "Toggle compile-on-save for coffee-mode."
592 :group 'coffee-cos :lighter coffee-cos-mode-line
593 (cond
594 (coffee-cos-mode
595 (add-hook 'after-save-hook 'coffee-compile-file nil t))
596 (t
597 (remove-hook 'after-save-hook 'coffee-compile-file t))))
598
599 (provide 'coffee-mode)
600
601 ;;
602 ;; On Load
603 ;;
604
605 ;; Run coffee-mode for files ending in .coffee.
606 ;;;###autoload
607 (add-to-list 'auto-mode-alist '("\\.coffee$" . coffee-mode))
608 ;;;###autoload
609 (add-to-list 'auto-mode-alist '("Cakefile" . coffee-mode))
610 ;;; coffee-mode.el ends here