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