1358 lines
48 KiB
EmacsLisp
1358 lines
48 KiB
EmacsLisp
;;; coffee-mode.el --- Major mode for CoffeeScript code -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2010 Chris Wanstrath
|
|
|
|
;; Version: 0.6.3
|
|
;; Package-Version: 20160808.1712
|
|
;; Keywords: CoffeeScript major mode
|
|
;; Author: Chris Wanstrath <chris@ozmm.org>
|
|
;; URL: http://github.com/defunkt/coffee-mode
|
|
;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
|
|
|
|
;; 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:
|
|
|
|
;; Provides syntax highlighting, indentation support, imenu support,
|
|
;; compiling to JavaScript, REPL, a menu bar, and a few cute commands.
|
|
|
|
;;; Code:
|
|
|
|
(require 'comint)
|
|
(require 'easymenu)
|
|
(require 'font-lock)
|
|
(require 'rx)
|
|
|
|
(require 'cl-lib)
|
|
|
|
(declare-function tramp-file-name-localname "tramp")
|
|
(declare-function tramp-dissect-file-name "tramp")
|
|
|
|
;;
|
|
;; Customizable Variables
|
|
;;
|
|
|
|
(defconst coffee-mode-version "0.6.3"
|
|
"The version of `coffee-mode'.")
|
|
|
|
(defgroup coffee nil
|
|
"A CoffeeScript major mode."
|
|
:group 'languages)
|
|
|
|
(defcustom coffee-tab-width tab-width
|
|
"The tab width to use when indenting."
|
|
:type 'integer
|
|
:safe 'integerp)
|
|
|
|
(defcustom coffee-command "coffee"
|
|
"The CoffeeScript command used for evaluating code."
|
|
:type 'string)
|
|
|
|
(defcustom coffee-js-directory ""
|
|
"The directory for compiled JavaScript files output. This can
|
|
be an absolute path starting with a `/`, or it can be path
|
|
relative to the directory containing the coffeescript sources to
|
|
be compiled."
|
|
:type 'string)
|
|
|
|
(defcustom js2coffee-command "js2coffee"
|
|
"The js2coffee command used for evaluating code."
|
|
:type 'string)
|
|
|
|
(defcustom coffee-args-repl '("-i")
|
|
"The arguments to pass to `coffee-command' to start a REPL."
|
|
:type '(repeat string))
|
|
|
|
(defcustom coffee-args-compile '("-c" "--no-header")
|
|
"The arguments to pass to `coffee-command' to compile a file."
|
|
:type '(repeat string))
|
|
|
|
(defcustom coffee-compiled-buffer-name "*coffee-compiled*"
|
|
"The name of the scratch buffer used for compiled CoffeeScript."
|
|
:type 'string)
|
|
|
|
(defcustom coffee-repl-buffer "*CoffeeREPL*"
|
|
"The name of the CoffeeREPL buffer."
|
|
:type 'string)
|
|
|
|
(defcustom coffee-compile-jump-to-error t
|
|
"Whether to jump to the first error if compilation fails.
|
|
Since the coffee compiler does not always include a line number in
|
|
its error messages, this is not always possible."
|
|
:type 'boolean)
|
|
|
|
(defcustom coffee-watch-buffer-name "*coffee-watch*"
|
|
"The name of the scratch buffer used when using the --watch flag
|
|
with CoffeeScript."
|
|
:type 'string)
|
|
|
|
(defcustom coffee-mode-hook nil
|
|
"Hook called by `coffee-mode'. Examples:
|
|
|
|
;; Compile '.coffee' files on every save
|
|
(and (file-exists-p (buffer-file-name))
|
|
(file-exists-p (coffee-compiled-file-name))
|
|
(coffee-cos-mode t)))"
|
|
:type 'hook)
|
|
|
|
(defcustom coffee-indent-tabs-mode nil
|
|
"Indentation can insert tabs if this is t."
|
|
:type 'boolean)
|
|
|
|
(defcustom coffee-show-mode 'js-mode
|
|
"Major mode to used to show the compiled Javascript."
|
|
:type 'function)
|
|
|
|
(defcustom coffee-after-compile-hook nil
|
|
"Hook called after compile to Javascript"
|
|
:type 'hook)
|
|
|
|
(defcustom coffee-indent-like-python-mode nil
|
|
"Indent like python-mode."
|
|
:type 'boolean)
|
|
|
|
(defcustom coffee-switch-to-compile-buffer nil
|
|
"Switch to compilation buffer `coffee-compiled-buffer-name' after compiling
|
|
a buffer or region."
|
|
:type 'boolean)
|
|
|
|
(defvar coffee-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
;; key bindings
|
|
(define-key map (kbd "A-r") 'coffee-compile-buffer)
|
|
(define-key map (kbd "C-c C-k") 'coffee-compile-buffer)
|
|
(define-key map (kbd "A-R") 'coffee-compile-region)
|
|
(define-key map (kbd "A-M-r") 'coffee-repl)
|
|
(define-key map (kbd "C-c C-z") 'coffee-repl)
|
|
(define-key map [remap comment-dwim] 'coffee-comment-dwim)
|
|
(define-key map [remap newline-and-indent] 'coffee-newline-and-indent)
|
|
(define-key map "\C-m" 'coffee-newline-and-indent)
|
|
(define-key map "\C-c\C-o\C-s" 'coffee-cos-mode)
|
|
(define-key map "\177" 'coffee-dedent-line-backspace)
|
|
(define-key map (kbd "C-c C-<") 'coffee-indent-shift-left)
|
|
(define-key map (kbd "C-c C->") 'coffee-indent-shift-right)
|
|
(define-key map (kbd "C-c C-l") 'coffee-send-line)
|
|
(define-key map (kbd "C-c C-r") 'coffee-send-region)
|
|
(define-key map (kbd "C-c C-b") 'coffee-send-buffer)
|
|
(define-key map (kbd "<backtab>") 'coffee-indent-shift-left)
|
|
(define-key map (kbd "C-M-a") 'coffee-beginning-of-defun)
|
|
(define-key map (kbd "C-M-e") 'coffee-end-of-block)
|
|
(define-key map (kbd "C-M-h") 'coffee-mark-defun)
|
|
map)
|
|
"Keymap for CoffeeScript major mode.")
|
|
|
|
(defvar coffee--process nil)
|
|
|
|
;;
|
|
;; Commands
|
|
;;
|
|
|
|
(defun coffee-comint-filter (string)
|
|
(ansi-color-apply
|
|
(replace-regexp-in-string
|
|
"\uFF00" "\n"
|
|
(replace-regexp-in-string "\x1b\\[.[GJK]" "" string))))
|
|
|
|
(defun coffee-repl ()
|
|
"Launch a CoffeeScript REPL using `coffee-command' as an inferior mode."
|
|
(interactive)
|
|
|
|
(unless (comint-check-proc coffee-repl-buffer)
|
|
(set-buffer
|
|
(apply 'make-comint "CoffeeREPL"
|
|
"env"
|
|
nil
|
|
"NODE_NO_READLINE=1"
|
|
coffee-command
|
|
coffee-args-repl))
|
|
;; Workaround for ansi colors
|
|
(add-hook 'comint-preoutput-filter-functions 'coffee-comint-filter nil t))
|
|
|
|
(pop-to-buffer coffee-repl-buffer))
|
|
|
|
(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.
|
|
(let ((input (expand-file-name (or filename (buffer-file-name)))))
|
|
(unless (string= coffee-js-directory "")
|
|
(setq input
|
|
(expand-file-name
|
|
(concat (unless (file-name-absolute-p coffee-js-directory)
|
|
(file-name-directory input))
|
|
(file-name-as-directory coffee-js-directory)
|
|
(file-name-nondirectory input)))))
|
|
(concat (file-name-sans-extension input) ".js")))
|
|
|
|
(defun coffee-revert-buffer-compiled-file (file-name)
|
|
"Revert a buffer of compiled file when the buffer exist and is not modified."
|
|
(let ((buffer (find-buffer-visiting file-name)))
|
|
(when (and buffer (not (buffer-modified-p buffer)))
|
|
(with-current-buffer buffer
|
|
(revert-buffer nil t)))))
|
|
|
|
(defun coffee-parse-error-output (compiler-errstr)
|
|
(let* ((msg (car (split-string compiler-errstr "[\n\r]+")))
|
|
line column)
|
|
(message msg)
|
|
(when (or (string-match "on line \\([0-9]+\\)" msg)
|
|
(string-match ":\\([0-9]+\\):\\([0-9]+\\): error:" msg))
|
|
(setq line (string-to-number (match-string 1 msg)))
|
|
(when (match-string 2 msg)
|
|
(setq column (string-to-number (match-string 2 msg))))
|
|
|
|
(when coffee-compile-jump-to-error
|
|
(goto-char (point-min))
|
|
(forward-line (1- line))
|
|
(when column
|
|
(move-to-column (1- column)))))))
|
|
|
|
(defun coffee-compile-file ()
|
|
"Compiles and saves the current file to disk in a file of the same
|
|
base name, with extension `.js'. Subsequent runs will overwrite the
|
|
file.
|
|
|
|
If there are compilation errors, point is moved to the first
|
|
See `coffee-compile-jump-to-error'."
|
|
(interactive)
|
|
(let* ((input (buffer-file-name))
|
|
(basename (file-name-sans-extension input))
|
|
(output (when (string-match-p "\\.js\\'" basename) ;; for Rails '.js.coffee' file
|
|
basename))
|
|
(compile-args (coffee-command-compile input output))
|
|
(compiler-output (with-temp-buffer
|
|
(unless (zerop (apply #'process-file coffee-command nil t nil compile-args))
|
|
(error "Failed: %s %s" coffee-command compile-args))
|
|
(buffer-substring-no-properties (point-min) (point-max)))))
|
|
(if (string= compiler-output "")
|
|
(let ((file-name (coffee-compiled-file-name (buffer-file-name))))
|
|
(message "Compiled and saved %s" (or output (concat basename ".js")))
|
|
(coffee-revert-buffer-compiled-file file-name))
|
|
(coffee-parse-error-output compiler-output))))
|
|
|
|
(defun coffee-compile-buffer ()
|
|
"Compiles the current buffer and displays the JavaScript in a buffer
|
|
called `coffee-compiled-buffer-name'."
|
|
(interactive)
|
|
(coffee-compile-region (point-min) (point-max)))
|
|
|
|
(defsubst coffee-generate-sourcemap-p ()
|
|
(cl-find-if (lambda (opt) (member opt '("-m" "--map"))) coffee-args-compile))
|
|
|
|
(defun coffee--coffeescript-version ()
|
|
(with-temp-buffer
|
|
(unless (zerop (process-file coffee-command nil t nil "--version"))
|
|
(error "Failed: 'coffee --version'"))
|
|
(goto-char (point-min))
|
|
(let ((line (buffer-substring-no-properties (point) (line-end-position))))
|
|
(when (string-match "[0-9.]+\\'" line)
|
|
(match-string-no-properties 0 line)))))
|
|
|
|
(defun coffee--map-file-name (coffee-file)
|
|
(let* ((version (coffee--coffeescript-version))
|
|
(extension (if (version<= "1.8" version) ".js.map" ".map")))
|
|
;; foo.js: foo.js.map(>= 1.8), foo.map(< 1.8)
|
|
(concat (file-name-sans-extension coffee-file) extension)))
|
|
|
|
(defmacro coffee-save-window-if (bool &rest body)
|
|
`(if ,bool (save-selected-window ,@body) ,@body))
|
|
(put 'coffee-save-window-if 'lisp-indent-function 1)
|
|
|
|
(defun coffee-compile-sentinel (buffer file line column)
|
|
(lambda (proc _event)
|
|
(when (eq (process-status proc) 'exit)
|
|
(setq coffee--process nil)
|
|
(coffee-save-window-if (not coffee-switch-to-compile-buffer)
|
|
(pop-to-buffer (get-buffer coffee-compiled-buffer-name))
|
|
(ansi-color-apply-on-region (point-min) (point-max))
|
|
(goto-char (point-min))
|
|
(if (not (= (process-exit-status proc) 0))
|
|
(let ((compile-output (buffer-string)))
|
|
(with-current-buffer buffer
|
|
(coffee-parse-error-output compile-output)))
|
|
(let ((props (list :sourcemap (coffee--map-file-name file)
|
|
:line line :column column :source file)))
|
|
(setq buffer-read-only t)
|
|
(when (fboundp coffee-show-mode)
|
|
(funcall coffee-show-mode))
|
|
(run-hook-with-args 'coffee-after-compile-hook props)))))))
|
|
|
|
(defun coffee-start-compile-process (curbuf line column)
|
|
(lambda (start end)
|
|
(let ((proc (apply 'start-file-process "coffee-mode"
|
|
(get-buffer-create coffee-compiled-buffer-name)
|
|
coffee-command (append coffee-args-compile '("-s" "-p"))))
|
|
(curfile (buffer-file-name curbuf)))
|
|
(set-process-query-on-exit-flag proc nil)
|
|
(set-process-sentinel
|
|
proc (coffee-compile-sentinel curbuf curfile line column))
|
|
(with-current-buffer curbuf
|
|
(process-send-region proc start end))
|
|
(process-send-string proc "\n")
|
|
(process-send-eof proc)
|
|
(setq coffee--process proc))))
|
|
|
|
(defun coffee-start-generate-sourcemap-process (start end)
|
|
;; so that sourcemap generation reads from the current buffer
|
|
(save-buffer)
|
|
(let* ((file (buffer-file-name))
|
|
(sourcemap-buf (get-buffer-create "*coffee-sourcemap*"))
|
|
(proc (start-file-process "coffee-sourcemap" sourcemap-buf
|
|
coffee-command "-m" file))
|
|
(curbuf (current-buffer))
|
|
(line (line-number-at-pos))
|
|
(column (current-column)))
|
|
(setq coffee--process proc)
|
|
(set-process-query-on-exit-flag proc nil)
|
|
(set-process-sentinel
|
|
proc
|
|
(lambda (proc _event)
|
|
(when (eq (process-status proc) 'exit)
|
|
(setq coffee--process nil)
|
|
(if (not (= (process-exit-status proc) 0))
|
|
(let ((sourcemap-output
|
|
(with-current-buffer sourcemap-buf (buffer-string))))
|
|
(with-current-buffer curbuf
|
|
(coffee-parse-error-output sourcemap-output)))
|
|
(kill-buffer sourcemap-buf)
|
|
(funcall (coffee-start-compile-process curbuf line column) start end)))))))
|
|
|
|
(defun coffee-cleanup-compile-buffer ()
|
|
(let ((buffer (get-buffer coffee-compiled-buffer-name)))
|
|
(when buffer
|
|
(with-current-buffer buffer
|
|
(setq buffer-read-only nil)
|
|
(erase-buffer)))))
|
|
|
|
(defun coffee-compile-region (start end)
|
|
"Compiles a region and displays the JavaScript in a buffer called
|
|
`coffee-compiled-buffer-name'."
|
|
(interactive "r")
|
|
(coffee-cleanup-compile-buffer)
|
|
(if (coffee-generate-sourcemap-p)
|
|
(coffee-start-generate-sourcemap-process start end)
|
|
(funcall (coffee-start-compile-process
|
|
(current-buffer) (line-number-at-pos) (current-column))
|
|
start end)))
|
|
|
|
(defun coffee-get-repl-proc ()
|
|
(unless (comint-check-proc coffee-repl-buffer)
|
|
(coffee-repl)
|
|
;; see issue #332
|
|
(sleep-for 0 100))
|
|
(get-buffer-process coffee-repl-buffer))
|
|
|
|
(defun coffee-send-line ()
|
|
"Send the current line to the inferior Coffee process."
|
|
(interactive)
|
|
(coffee-send-region (line-beginning-position) (line-end-position)))
|
|
|
|
(defun coffee-send-region (start end)
|
|
"Send the current region to the inferior Coffee process."
|
|
(interactive "r")
|
|
(deactivate-mark t)
|
|
(let* ((string (buffer-substring-no-properties start end))
|
|
(proc (coffee-get-repl-proc))
|
|
(multiline-escaped-string
|
|
(replace-regexp-in-string "\n" "\uFF00" string)))
|
|
(comint-simple-send proc multiline-escaped-string)))
|
|
|
|
(defun coffee-send-buffer ()
|
|
"Send the current buffer to the inferior Coffee process."
|
|
(interactive)
|
|
(coffee-send-region (point-min) (point-max)))
|
|
|
|
(defun coffee-js2coffee-replace-region (start end)
|
|
"Convert JavaScript in the region into CoffeeScript."
|
|
(interactive "r")
|
|
|
|
(let ((buffer (get-buffer coffee-compiled-buffer-name)))
|
|
(when buffer
|
|
(kill-buffer buffer)))
|
|
|
|
(call-process-region start end
|
|
js2coffee-command t
|
|
(current-buffer)))
|
|
|
|
(defun coffee-version ()
|
|
"Show the `coffee-mode' version in the echo area."
|
|
(interactive)
|
|
(message (concat "coffee-mode version " coffee-mode-version)))
|
|
|
|
(defun coffee-watch (dir-or-file)
|
|
"Run `coffee-run-cmd' with the --watch flag on a directory or file."
|
|
(interactive "fDirectory or File: ")
|
|
(let ((coffee-compiled-buffer-name coffee-watch-buffer-name)
|
|
(args (mapconcat 'identity (append coffee-args-compile (list "--watch" (expand-file-name dir-or-file))) " ")))
|
|
(coffee-run-cmd args)))
|
|
|
|
;;
|
|
;; 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]
|
|
"---"
|
|
["Version" coffee-version]
|
|
))
|
|
|
|
;;
|
|
;; Define Language Syntax
|
|
;;
|
|
|
|
;; Instance variables (implicit this)
|
|
(defvar coffee-this-regexp "\\(?:@[_[:word:]]+\\|\\<this\\)\\>")
|
|
|
|
;; Prototype::access
|
|
(defvar coffee-prototype-regexp "[_[:word:].$]+?::")
|
|
|
|
;; Assignment
|
|
(defvar coffee-assign-regexp "\\(@?[_[:word:].$]+?\\)\\s-*:")
|
|
|
|
;; Local Assignment
|
|
(defvar coffee-local-assign-regexp "\\s-*\\([_[:word:].$]+\\)\\s-*\\??=\\(?:[^>=]\\|$\\)")
|
|
|
|
;; Lambda
|
|
(defvar coffee-lambda-regexp "\\(?:([^)]*)\\)?\\s-*\\(->\\|=>\\)")
|
|
|
|
;; Namespaces
|
|
(defvar coffee-namespace-regexp "\\b\\(?:class\\s-+\\(\\S-+\\)\\)\\b")
|
|
|
|
;; Booleans
|
|
(defvar coffee-boolean-regexp
|
|
(rx (or bol (not (any ".")))
|
|
(group symbol-start
|
|
(or "true" "false" "yes" "no" "on" "off" "null" "undefined")
|
|
symbol-end)))
|
|
|
|
;; Regular expressions
|
|
(eval-and-compile
|
|
(defvar coffee-regexp-regexp "\\s/\\(\\(?:\\\\/\\|[^/\n\r]\\)*\\)\\s/"))
|
|
|
|
;; 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" "until" "loop" "yield"))
|
|
|
|
;; Reserved keywords either by JS or CS.
|
|
(defvar coffee-js-reserved
|
|
'("case" "default" "do" "function" "var" "void" "with"
|
|
"const" "let" "debugger" "enum" "export" "import" "native"
|
|
"__extends" "__hasProp"))
|
|
|
|
;; CoffeeScript keywords.
|
|
(defvar coffee-cs-keywords
|
|
'("then" "unless" "and" "or" "is" "own"
|
|
"isnt" "not" "of" "by" "when"))
|
|
|
|
;; Iced CoffeeScript keywords
|
|
(defvar iced-coffee-cs-keywords
|
|
'("await" "defer"))
|
|
|
|
;; Regular expression combining the above three lists.
|
|
(defvar coffee-keywords-regexp
|
|
;; keywords can be member names.
|
|
(concat "\\(?:^\\|[^.]\\)"
|
|
(regexp-opt (append coffee-js-reserved
|
|
coffee-js-keywords
|
|
coffee-cs-keywords
|
|
iced-coffee-cs-keywords) 'symbols)))
|
|
|
|
;; Create the list for font-lock. Each class of keyword is given a
|
|
;; particular face.
|
|
(defvar coffee-font-lock-keywords
|
|
;; *Note*: order below matters. `coffee-keywords-regexp' goes last
|
|
;; because otherwise the keyword "state" in the function
|
|
;; "state_entry" would be highlighted.
|
|
`((,coffee-regexp-regexp . font-lock-constant-face)
|
|
(,coffee-this-regexp . font-lock-variable-name-face)
|
|
(,coffee-prototype-regexp . font-lock-type-face)
|
|
(,coffee-assign-regexp . font-lock-type-face)
|
|
(,coffee-local-assign-regexp 1 font-lock-variable-name-face)
|
|
(,coffee-boolean-regexp 1 font-lock-constant-face)
|
|
(,coffee-lambda-regexp 1 font-lock-function-name-face)
|
|
(,coffee-keywords-regexp 1 font-lock-keyword-face)
|
|
(,(lambda (limit)
|
|
(let ((res nil)
|
|
start)
|
|
(while (and (not res) (search-forward "#{" limit t))
|
|
(let ((restart-pos (match-end 0)))
|
|
(setq start (match-beginning 0))
|
|
(let (finish)
|
|
(while (and (not finish) (search-forward "}" limit t))
|
|
(let ((end-pos (point)))
|
|
(save-excursion
|
|
(when (and (ignore-errors (backward-list 1))
|
|
(= start (1- (point))))
|
|
(setq res end-pos finish t)))))
|
|
(unless finish
|
|
(goto-char restart-pos)))))
|
|
(when (and res start)
|
|
(set-match-data (list start res)))
|
|
res))
|
|
(0 font-lock-variable-name-face t))))
|
|
|
|
;;
|
|
;; Helper Functions
|
|
;;
|
|
|
|
(defun coffee-comment-dwim (arg)
|
|
"Comment or uncomment current line or region in a smart way.
|
|
For details, see `comment-dwim'."
|
|
(interactive "*P")
|
|
(require 'newcomment)
|
|
(let ((deactivate-mark nil) (comment-start "#") (comment-end ""))
|
|
(comment-dwim arg)
|
|
(deactivate-mark t)))
|
|
|
|
(defsubst coffee-command-compile-options (output)
|
|
(if output
|
|
(append coffee-args-compile (list "-j" output))
|
|
coffee-args-compile))
|
|
|
|
(defun coffee-command-compile (input output)
|
|
"Run `coffee-command' to compile FILE-NAME to file with default
|
|
.js output file, or optionally to OUTPUT-FILE-NAME."
|
|
(let* ((expanded (expand-file-name input))
|
|
(filename (if (file-remote-p expanded)
|
|
(tramp-file-name-localname (tramp-dissect-file-name expanded))
|
|
(file-truename expanded)))
|
|
(output-file (coffee-compiled-file-name filename))
|
|
(output-dir (file-name-directory output-file)))
|
|
(unless (file-directory-p output-dir)
|
|
(make-directory output-dir t))
|
|
(append (coffee-command-compile-options output)
|
|
(list "-o" output-dir filename))))
|
|
|
|
(defun coffee-run-cmd (args)
|
|
"Run `coffee-command' with the given arguments, and display the
|
|
output in a compilation buffer."
|
|
(interactive "sArguments: ")
|
|
(let ((compilation-buffer-name-function
|
|
(lambda (_this-mode)
|
|
(generate-new-buffer-name coffee-compiled-buffer-name))))
|
|
(compile (concat coffee-command " " args))))
|
|
|
|
(defun coffee-toggle-fatness ()
|
|
"Toggle fatness of a coffee function arrow."
|
|
(interactive)
|
|
(save-excursion
|
|
(when (re-search-backward "[-=]>" nil t)
|
|
(cond ((looking-at "=") (replace-match "-"))
|
|
((looking-at "-") (replace-match "="))))))
|
|
|
|
;;
|
|
;; imenu support
|
|
;;
|
|
|
|
(defconst coffee-imenu-index-regexp
|
|
(concat "^\\(\\s-*\\)" ; $1
|
|
"\\(?:"
|
|
coffee-assign-regexp ; $2
|
|
"\\s-*"
|
|
coffee-lambda-regexp
|
|
"\\|"
|
|
coffee-namespace-regexp ; $4
|
|
"\\|"
|
|
"\\(@?[_[:word:]:.$]+\\)\\s-*=\\(?:[^>]\\|$\\)" ; $5 match prototype access too
|
|
"\\(?:" "\\s-*" "\\(" coffee-lambda-regexp "\\)" "\\)?" ; $6
|
|
"\\)"))
|
|
|
|
(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 '())
|
|
(ns-indent 0)
|
|
ns-name)
|
|
;; Go through every assignment that includes -> or => on the same
|
|
;; line or starts with `class'.
|
|
(while (re-search-forward coffee-imenu-index-regexp nil t)
|
|
(let ((current-indent (- (match-end 1) (match-beginning 1)))
|
|
(property-name (match-string-no-properties 2))
|
|
(class-name (match-string-no-properties 4))
|
|
(variable-name (match-string-no-properties 5))
|
|
(func-assign (match-string-no-properties 6)))
|
|
|
|
;; If this is the start of a new namespace, save the namespace's
|
|
;; indentation level and name.
|
|
(if class-name
|
|
(setq ns-name (concat class-name "::")
|
|
ns-indent current-indent)
|
|
(when (and variable-name (<= current-indent ns-indent))
|
|
(setq ns-name (concat variable-name ".")
|
|
ns-indent current-indent)))
|
|
|
|
(if func-assign
|
|
(push (cons variable-name (match-beginning 5)) index-alist)
|
|
(when (and ns-name property-name)
|
|
(let ((index-pos (match-beginning 2)))
|
|
(if (<= current-indent ns-indent)
|
|
;; Clear the namespace if we're no longer indented deeper
|
|
(setq ns-name nil ns-indent nil)
|
|
;; Register as index-name if we are within the context of a namespace
|
|
(push (cons (concat ns-name property-name) index-pos) index-alist)))))))
|
|
index-alist))
|
|
|
|
;;
|
|
;; Indentation
|
|
;;
|
|
|
|
(defsubst coffee-insert-spaces (count)
|
|
(if coffee-indent-tabs-mode
|
|
(insert-char (string-to-char "\t") (floor count coffee-tab-width))
|
|
(insert-char ? count)))
|
|
|
|
;;; The theory is explained in the README.
|
|
|
|
(defsubst coffee--in-string-or-comment-p ()
|
|
(nth 8 (syntax-ppss)))
|
|
|
|
(defun coffee--block-type ()
|
|
(save-excursion
|
|
(back-to-indentation)
|
|
(unless (coffee--in-string-or-comment-p)
|
|
(cond ((looking-at-p "else\\(\\s-+if\\)?\\_>") 'if-else)
|
|
((looking-at-p "\\(?:catch\\|finally\\)\\_>") 'try-catch)))))
|
|
|
|
(defun coffee--closed-if-else-p (curindent if-indent)
|
|
(let (else-if-p else-p)
|
|
(when (looking-at "else\\(?:\\s-+\\(if\\)\\)?\\_>")
|
|
(if (string= (match-string 1) "if")
|
|
(setq else-if-p t)
|
|
(setq else-p t)))
|
|
(or (and (not (or else-p else-if-p)) (<= curindent if-indent))
|
|
(and else-p (= curindent if-indent)))))
|
|
|
|
(defun coffee--closed-try-catch-p (curindent if-indent)
|
|
(and (not (looking-at-p "\\(?:finally\\|catch\\)\\_>"))
|
|
(<= curindent if-indent)))
|
|
|
|
(defun coffee--closed-block-p (type if-indent limit)
|
|
(let ((limit-line (line-number-at-pos limit))
|
|
(closed-pred (cl-case type
|
|
(if-else 'coffee--closed-if-else-p)
|
|
(try-catch 'coffee--closed-try-catch-p)))
|
|
finish)
|
|
(save-excursion
|
|
(while (and (not finish) (< (point) limit))
|
|
(forward-line 1)
|
|
(when (< (line-number-at-pos) limit-line)
|
|
(let ((curindent (current-indentation)))
|
|
(unless (coffee--in-string-or-comment-p)
|
|
(back-to-indentation)
|
|
(when (funcall closed-pred curindent if-indent)
|
|
(setq finish t))))))
|
|
finish)))
|
|
|
|
(defun coffee--find-if-else-indents (limit cmpfn)
|
|
(let (indents)
|
|
(while (re-search-forward "^\\s-*if\\_>" limit t)
|
|
(let ((indent (current-indentation)))
|
|
(unless (coffee--closed-block-p 'if-else indent limit)
|
|
(push indent indents))))
|
|
(sort indents cmpfn)))
|
|
|
|
(defun coffee--find-try-catch-indents (limit cmpfn)
|
|
(let (indents)
|
|
(while (re-search-forward "^\\s-*try\\_>" limit t)
|
|
(let ((indent (current-indentation)))
|
|
(unless (coffee--closed-block-p 'try-catch indent limit)
|
|
(push indent indents))))
|
|
(sort indents cmpfn)))
|
|
|
|
(defun coffee--find-indents (type limit cmpfn)
|
|
(save-excursion
|
|
(coffee-beginning-of-defun 1)
|
|
(cl-case type
|
|
(if-else (coffee--find-if-else-indents limit cmpfn))
|
|
(try-catch (coffee--find-try-catch-indents limit cmpfn)))))
|
|
|
|
(defsubst coffee--decide-indent (curindent if-indents cmpfn)
|
|
(cl-loop for if-indent in if-indents
|
|
when (funcall cmpfn if-indent curindent)
|
|
return if-indent
|
|
finally
|
|
return (car if-indents)))
|
|
|
|
(defun coffee--indent-insert-spaces (indent-size)
|
|
(unless (= (current-indentation) indent-size)
|
|
(save-excursion
|
|
(goto-char (line-beginning-position))
|
|
(delete-horizontal-space)
|
|
(coffee-insert-spaces indent-size)))
|
|
(when (< (current-column) (current-indentation))
|
|
(back-to-indentation)))
|
|
|
|
(defun coffee--indent-line-like-python-mode (prev-indent repeated)
|
|
(let ((next-indent (- (current-indentation) coffee-tab-width))
|
|
(indent-p (coffee-line-wants-indent)))
|
|
(if repeated
|
|
(if (< next-indent 0)
|
|
(+ prev-indent (if indent-p coffee-tab-width 0))
|
|
next-indent)
|
|
(+ prev-indent (if indent-p coffee-tab-width 0)))))
|
|
|
|
(defun coffee-indent-line ()
|
|
"Indent current line as CoffeeScript."
|
|
(interactive)
|
|
(let* ((curindent (current-indentation))
|
|
(limit (+ (line-beginning-position) curindent))
|
|
(type (coffee--block-type))
|
|
indent-size
|
|
begin-indents)
|
|
(if (and type (setq begin-indents (coffee--find-indents type limit '<)))
|
|
(setq indent-size (coffee--decide-indent curindent begin-indents '>))
|
|
(if coffee-indent-like-python-mode
|
|
(setq indent-size
|
|
(coffee--indent-line-like-python-mode
|
|
(coffee-previous-indent) (eq last-command this-command)))
|
|
(let ((prev-indent (coffee-previous-indent))
|
|
(next-indent-size (+ curindent coffee-tab-width)))
|
|
(if (> (- next-indent-size prev-indent) coffee-tab-width)
|
|
(setq indent-size 0)
|
|
(setq indent-size (+ curindent coffee-tab-width))))))
|
|
(coffee--indent-insert-spaces indent-size)))
|
|
|
|
(defun coffee-previous-indent ()
|
|
"Return the indentation level of the previous non-blank line."
|
|
(save-excursion
|
|
(forward-line -1)
|
|
(while (and (looking-at-p "^[ \t]*$") (not (bobp)))
|
|
(forward-line -1))
|
|
(current-indentation)))
|
|
|
|
(defun coffee-newline-and-indent ()
|
|
"Insert a newline and indent 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)))
|
|
(when (< (current-column) (current-indentation))
|
|
(move-to-column (current-indentation)))
|
|
(delete-horizontal-space t)
|
|
(newline)
|
|
|
|
(if (coffee-line-wants-indent)
|
|
;; We need to insert an additional tab because the last line was special.
|
|
(coffee-insert-spaces (+ (coffee-previous-indent) coffee-tab-width))
|
|
;; otherwise keep at the same indentation level
|
|
(coffee-insert-spaces prev-indent))
|
|
|
|
;; 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).
|
|
(unless (and auto-fill-function comment-auto-fill-only-comments)
|
|
(when (coffee-previous-line-is-single-line-comment)
|
|
(insert "# ")))))
|
|
|
|
(defun coffee-dedent-line-backspace (arg)
|
|
"Unindent to increment of `coffee-tab-width' with ARG==1 when
|
|
called from first non-blank char of line.
|
|
|
|
Delete ARG spaces if ARG!=1."
|
|
(interactive "*p")
|
|
(if (use-region-p)
|
|
(delete-region (region-beginning) (region-end))
|
|
(if (and (= 1 arg)
|
|
(= (point) (save-excursion
|
|
(back-to-indentation)
|
|
(point)))
|
|
(not (bolp)))
|
|
(let* ((extra-space-count (% (current-column) coffee-tab-width))
|
|
(deleted-chars (if (zerop extra-space-count)
|
|
coffee-tab-width
|
|
extra-space-count)))
|
|
(backward-delete-char-untabify deleted-chars))
|
|
(backward-delete-char-untabify arg))))
|
|
|
|
;; 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" "else" "unless" "while" "until"
|
|
"try" "catch" "finally" "switch" "when")
|
|
"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 ()
|
|
"Return t if the current line should be indented relative to the
|
|
previous line."
|
|
(save-excursion
|
|
(back-to-indentation)
|
|
(skip-chars-backward "\r\n\t ")
|
|
(let ((char-of-eol (char-before (line-end-position))))
|
|
(or (and char-of-eol (memq char-of-eol coffee-indenters-eol))
|
|
(progn
|
|
(back-to-indentation)
|
|
(and (looking-at-p (coffee-indenters-bol-regexp))
|
|
(not (re-search-forward "\\_<then\\_>" (line-end-position) t))))))))
|
|
|
|
(defun coffee-previous-line-is-single-line-comment ()
|
|
"Return t if the previous line is a CoffeeScript single line comment."
|
|
(save-excursion
|
|
(forward-line -1)
|
|
(back-to-indentation)
|
|
(and (looking-at-p "#")
|
|
(not (looking-at-p "###\\(?:\\s-+.*\\)?$"))
|
|
(progn
|
|
(goto-char (line-end-position))
|
|
(nth 4 (syntax-ppss))))))
|
|
|
|
(defun coffee-indent-shift-amount (start end dir)
|
|
"Compute distance to the closest increment of `coffee-tab-width'."
|
|
(let ((min most-positive-fixnum))
|
|
(save-excursion
|
|
(goto-char start)
|
|
(while (< (point) end)
|
|
(let ((current (current-indentation)))
|
|
(when (< current min)
|
|
(setq min current)))
|
|
(forward-line))
|
|
(let ((rem (% min coffee-tab-width)))
|
|
(if (zerop rem)
|
|
coffee-tab-width
|
|
(cond ((eq dir 'left) rem)
|
|
((eq dir 'right) (- coffee-tab-width rem))
|
|
(t 0)))))))
|
|
|
|
(defun coffee-indent-shift-left (start end &optional count)
|
|
"Shift lines contained in region START END by COUNT columns to the left.
|
|
If COUNT is not given, indents to the closest increment of
|
|
`coffee-tab-width'. If region isn't active, the current line is
|
|
shifted. The shifted region includes the lines in which START and
|
|
END lie. An error is signaled if any lines in the region are
|
|
indented less than COUNT columns."
|
|
(interactive
|
|
(if (use-region-p)
|
|
(list (region-beginning) (region-end) current-prefix-arg)
|
|
(list (line-beginning-position) (line-end-position) current-prefix-arg)))
|
|
(let ((amount (if count (* coffee-tab-width (prefix-numeric-value count))
|
|
(coffee-indent-shift-amount start end 'left))))
|
|
(when (> amount 0)
|
|
(let (deactivate-mark)
|
|
(save-excursion
|
|
(goto-char start)
|
|
;; Check that all lines can be shifted enough
|
|
(while (< (point) end)
|
|
(if (and (< (current-indentation) amount)
|
|
(not (looking-at-p "[ \t]*$")))
|
|
(error "Can't shift all lines enough"))
|
|
(forward-line))
|
|
(indent-rigidly start end (- amount)))))))
|
|
|
|
(add-to-list 'debug-ignored-errors "^Can't shift all lines enough")
|
|
|
|
(defun coffee-indent-shift-right (start end &optional count)
|
|
"Shift lines contained in region START END by COUNT columns to the right.
|
|
if COUNT is not given, indents to the closest increment of
|
|
`coffee-tab-width'. If region isn't active, the current line is
|
|
shifted. The shifted region includes the lines in which START and
|
|
END lie."
|
|
(interactive
|
|
(if (use-region-p)
|
|
(list (region-beginning) (region-end) current-prefix-arg)
|
|
(list (line-beginning-position) (line-end-position) current-prefix-arg)))
|
|
(let (deactivate-mark
|
|
(amount (if count (* coffee-tab-width (prefix-numeric-value count))
|
|
(coffee-indent-shift-amount start end 'right))))
|
|
(indent-rigidly start end amount)))
|
|
|
|
(defun coffee-indent-region (start end)
|
|
(interactive "r")
|
|
(save-excursion
|
|
(goto-char start)
|
|
(forward-line 1)
|
|
(while (and (not (eobp)) (< (point) end))
|
|
(let ((prev-indent (coffee-previous-indent))
|
|
(curindent (current-indentation))
|
|
indent-size)
|
|
(if (coffee-line-wants-indent)
|
|
(let ((expected (+ prev-indent coffee-tab-width)))
|
|
(when (/= curindent expected)
|
|
(setq indent-size expected)))
|
|
(when (> curindent prev-indent)
|
|
(setq indent-size prev-indent)))
|
|
(when indent-size
|
|
(save-excursion
|
|
(goto-char (line-beginning-position))
|
|
(delete-horizontal-space)
|
|
(coffee-insert-spaces indent-size))))
|
|
(forward-line 1))))
|
|
|
|
;;
|
|
;; Fill
|
|
;;
|
|
|
|
(defun coffee-fill-forward-paragraph-function (&optional count)
|
|
"`fill-forward-paragraph-function' which correctly handles block
|
|
comments such as the following:
|
|
|
|
class Klass
|
|
method: ->
|
|
###
|
|
This is a method doc comment that spans multiple lines.
|
|
If `fill-paragraph' is applied to this paragraph, the comment
|
|
should preserve its format, with the delimiters on separate lines.
|
|
###
|
|
..."
|
|
(let ((ret (forward-paragraph count)))
|
|
(when (and (= count -1)
|
|
(looking-at-p "[[:space:]]*###[[:space:]]*$"))
|
|
(forward-line))
|
|
ret))
|
|
|
|
;;
|
|
;; Define navigation functions
|
|
;;
|
|
|
|
(defconst coffee-defun-regexp
|
|
(concat "^\\s-*\\(?:"
|
|
coffee-assign-regexp
|
|
"\\s-*"
|
|
coffee-lambda-regexp
|
|
"\\|"
|
|
coffee-namespace-regexp
|
|
"\\|"
|
|
"@?[_[:word:]:.$]+\\s-*=\\(?:[^>]\\|$\\)"
|
|
"\\s-*"
|
|
coffee-lambda-regexp
|
|
"\\)"))
|
|
|
|
(defun coffee-in-comment-p ()
|
|
(unless (eobp)
|
|
(save-excursion
|
|
(back-to-indentation)
|
|
(when (eq (char-after) ?#)
|
|
(forward-char 1))
|
|
(nth 4 (syntax-ppss)))))
|
|
|
|
(defsubst coffee-current-line-empty-p ()
|
|
(let ((line (buffer-substring-no-properties
|
|
(line-beginning-position) (line-end-position))))
|
|
(string-match-p "^\\s-*$" line)))
|
|
|
|
(defun coffee-current-line-is-defun ()
|
|
(save-excursion
|
|
(goto-char (line-end-position))
|
|
(re-search-backward coffee-defun-regexp (line-beginning-position) t)))
|
|
|
|
(defun coffee-current-line-is-assignment ()
|
|
(save-excursion
|
|
(goto-char (line-end-position))
|
|
(re-search-backward "^[_[:word:].$]+\\s-*=\\(?:[^>]\\|$\\)"
|
|
(line-beginning-position) t)))
|
|
|
|
(defun coffee-curline-defun-type (parent-indent start-is-defun)
|
|
(save-excursion
|
|
(goto-char (line-end-position))
|
|
(if (not (re-search-backward coffee-defun-regexp (line-beginning-position) t))
|
|
(when (and (zerop parent-indent) (coffee-current-line-is-assignment))
|
|
'other)
|
|
(if (not start-is-defun)
|
|
'other
|
|
(if (< parent-indent (current-indentation))
|
|
'child
|
|
'other)))))
|
|
|
|
(defun coffee-same-block-p (block-indent start-is-defun)
|
|
(let ((type (coffee-curline-defun-type block-indent start-is-defun)))
|
|
(cond ((eq type 'child) t)
|
|
((eq type 'other) nil)
|
|
(t (>= (current-indentation) block-indent)))))
|
|
|
|
(defsubst coffee-skip-line-p ()
|
|
(or (coffee-in-comment-p) (coffee-current-line-empty-p)))
|
|
|
|
(defun coffee-skip-forward-lines (arg)
|
|
(let ((pred (if (> arg 0)
|
|
(lambda () (not (eobp)))
|
|
(lambda () (not (bobp))))))
|
|
(while (and (funcall pred) (coffee-skip-line-p))
|
|
(forward-line arg))))
|
|
|
|
(defun coffee-beginning-of-defun (&optional count)
|
|
(interactive "p")
|
|
(unless count
|
|
(setq count 1))
|
|
(let ((next-indent nil))
|
|
(when (coffee-skip-line-p)
|
|
(save-excursion
|
|
(coffee-skip-forward-lines +1)
|
|
(setq next-indent (current-indentation))))
|
|
(coffee-skip-forward-lines -1)
|
|
(let ((start-indent (or next-indent (current-indentation))))
|
|
(when (and (not (eq this-command 'coffee-mark-defun)) (looking-back "^\\s-*" (line-beginning-position)))
|
|
(forward-line -1))
|
|
(let ((finish nil))
|
|
(goto-char (line-end-position))
|
|
(while (and (not finish) (re-search-backward coffee-defun-regexp nil 'move))
|
|
(let ((cur-indent (current-indentation)))
|
|
(when (<= cur-indent start-indent)
|
|
(setq start-indent cur-indent)
|
|
(cl-decf count)))
|
|
(when (<= count 0)
|
|
(back-to-indentation)
|
|
(setq finish t)))))))
|
|
|
|
(defun coffee-end-of-block (&optional count)
|
|
"Move point to the end of the block."
|
|
(interactive "p")
|
|
(unless count
|
|
(setq count 1))
|
|
(dotimes (_i count)
|
|
(let* ((curline-is-defun (coffee-current-line-is-defun))
|
|
start-indent)
|
|
(coffee-skip-forward-lines 1)
|
|
(setq start-indent (current-indentation))
|
|
(when (and (zerop start-indent) (not curline-is-defun))
|
|
(when (re-search-forward coffee-defun-regexp nil 'move)
|
|
(back-to-indentation)
|
|
(setq curline-is-defun t)))
|
|
(let ((finish nil))
|
|
(while (not finish)
|
|
(forward-line 1)
|
|
(coffee-skip-forward-lines 1)
|
|
(when (or (not (coffee-same-block-p start-indent curline-is-defun))
|
|
(eobp))
|
|
(setq finish t)))
|
|
(forward-line -1)
|
|
(coffee-skip-forward-lines -1)
|
|
(forward-line 1)))))
|
|
|
|
(defun coffee-mark-defun ()
|
|
(interactive)
|
|
(let ((be-actived transient-mark-mode))
|
|
(push-mark (point))
|
|
(let ((cur-indent (current-indentation)))
|
|
(coffee-beginning-of-defun)
|
|
(push-mark (point))
|
|
(coffee-end-of-block)
|
|
(push-mark (point) nil be-actived)
|
|
(let ((next-indent nil))
|
|
(when (coffee-skip-line-p)
|
|
(save-excursion
|
|
(coffee-skip-forward-lines +1)
|
|
(setq next-indent (current-indentation))))
|
|
(when (and next-indent (< next-indent cur-indent))
|
|
(coffee-skip-forward-lines -1))
|
|
(coffee-beginning-of-defun)))))
|
|
|
|
;;
|
|
;; hs-minor-mode
|
|
;;
|
|
|
|
;; support for hs-minor-mode
|
|
(add-to-list 'hs-special-modes-alist
|
|
'(coffee-mode "\\s-*\\(?:class\\|.+[-=]>$\\)" nil "#"
|
|
coffee-end-of-block nil))
|
|
|
|
;;
|
|
;; Based on triple quote of python.el
|
|
;;
|
|
(eval-and-compile
|
|
(defconst coffee-block-strings-delimiter
|
|
(rx (and
|
|
;; Match even number of backslashes.
|
|
(or (not (any ?\\ ?\' ?\" ?/))
|
|
point
|
|
;; Quotes might be preceded by a escaped quote.
|
|
(and (or (not (any ?\\)) point)
|
|
?\\
|
|
(* ?\\ ?\\)
|
|
(any ?\' ?\" ?/)))
|
|
(* ?\\ ?\\)
|
|
;; Match single or triple quotes of any kind.
|
|
(group (or "'''" "\"\"\"" "///"))))))
|
|
|
|
(defsubst coffee-syntax-count-quotes (quote-char start-point limit)
|
|
(let ((i 0))
|
|
(while (and (< i 3)
|
|
(< (+ start-point i) limit)
|
|
(eq (char-after (+ start-point i)) quote-char))
|
|
(cl-incf i))
|
|
i))
|
|
|
|
(defun coffee-syntax-block-strings-stringify ()
|
|
(let* ((ppss (prog2
|
|
(backward-char 3)
|
|
(syntax-ppss)
|
|
(forward-char 3)))
|
|
(string-start (and (not (nth 4 ppss)) (nth 8 ppss)))
|
|
(quote-starting-pos (- (point) 3))
|
|
(quote-ending-pos (point))
|
|
(num-closing-quotes
|
|
(and string-start
|
|
(coffee-syntax-count-quotes
|
|
(char-before) string-start quote-starting-pos))))
|
|
(cond ((and string-start (= num-closing-quotes 0))
|
|
;; This set of quotes doesn't match the string starting
|
|
;; kind. Do nothing.
|
|
nil)
|
|
((not string-start)
|
|
;; This set of quotes delimit the start of a string.
|
|
(put-text-property quote-starting-pos (1+ quote-starting-pos)
|
|
'syntax-table (string-to-syntax "|")))
|
|
((= num-closing-quotes 3)
|
|
;; This set of quotes delimit the end of a string.
|
|
(put-text-property (1- quote-ending-pos) quote-ending-pos
|
|
'syntax-table (string-to-syntax "|"))))))
|
|
|
|
(defun coffee-syntax-propertize-block-comment ()
|
|
(let ((curpoint (point))
|
|
(inhibit-changing-match-data t))
|
|
(let* ((valid-comment-start nil)
|
|
(valid-comment-end (looking-at-p "#\\{0,2\\}\\s-*$"))
|
|
(ppss (prog2
|
|
(backward-char 3)
|
|
(syntax-ppss)
|
|
(setq valid-comment-start (looking-back "^\\s-*" (line-beginning-position)))
|
|
(forward-char 3)))
|
|
(in-comment (nth 4 ppss))
|
|
(in-string (nth 3 ppss)))
|
|
(when (or (and (not in-comment) (not in-string) valid-comment-start)
|
|
(and in-comment valid-comment-end))
|
|
(put-text-property (- curpoint 3) curpoint
|
|
'syntax-table (string-to-syntax "!"))))))
|
|
|
|
(defsubst coffee--in-string-p ()
|
|
(nth 3 (syntax-ppss)))
|
|
|
|
(defun coffee-syntax-string-interpolation ()
|
|
(let ((start (match-beginning 0))
|
|
(end (point)))
|
|
(if (not (coffee--in-string-p))
|
|
(put-text-property start (1+ start)
|
|
'syntax-table (string-to-syntax "< b"))
|
|
(goto-char start)
|
|
(let (finish res)
|
|
(while (and (not finish) (search-forward "}" nil t))
|
|
(let ((end-pos (match-end 0)))
|
|
(save-excursion
|
|
(when (and (ignore-errors (backward-list 1))
|
|
(= start (1- (point))))
|
|
(setq res end-pos finish t)))))
|
|
(goto-char end)
|
|
(when res
|
|
(while (re-search-forward "[\"'#]" res t)
|
|
(put-text-property (match-beginning 0) (match-end 0)
|
|
'syntax-table (string-to-syntax "_")))
|
|
(goto-char (1- res)))))))
|
|
|
|
(defun coffee-syntax-propertize-function (start end)
|
|
(goto-char start)
|
|
(funcall
|
|
(syntax-propertize-rules
|
|
(coffee-block-strings-delimiter
|
|
(0 (ignore (coffee-syntax-block-strings-stringify))))
|
|
("/"
|
|
(0 (ignore
|
|
(let ((curpoint (point))
|
|
(start (match-beginning 0))
|
|
(end (match-end 0)))
|
|
(goto-char start)
|
|
(let ((ppss (syntax-ppss)))
|
|
(cond ((nth 8 ppss)
|
|
(put-text-property start end
|
|
'syntax-table (string-to-syntax "_"))
|
|
(goto-char curpoint))
|
|
((looking-at coffee-regexp-regexp)
|
|
(put-text-property (match-beginning 1) (match-end 1)
|
|
'syntax-table (string-to-syntax "_"))
|
|
(goto-char (match-end 0)))
|
|
(t (goto-char curpoint))))))))
|
|
("#{" (0 (ignore (coffee-syntax-string-interpolation))))
|
|
("###"
|
|
(0 (ignore (coffee-syntax-propertize-block-comment)))))
|
|
(point) end))
|
|
|
|
(defun coffee-get-comment-info ()
|
|
(let* ((syntax (syntax-ppss))
|
|
(commentp (nth 4 syntax))
|
|
(comment-start-kinda (nth 8 syntax)))
|
|
(when commentp
|
|
(save-excursion
|
|
(if (and
|
|
(> comment-start-kinda 2) (< comment-start-kinda (point-max))
|
|
(string=
|
|
"###" (buffer-substring
|
|
(- comment-start-kinda 2) (1+ comment-start-kinda))))
|
|
'multiple-line
|
|
'single-line)))))
|
|
|
|
(defun coffee-comment-line-break-fn (&optional _)
|
|
(let ((comment-type (coffee-get-comment-info))
|
|
(coffee-indent-like-python-mode t))
|
|
(comment-indent-new-line)
|
|
(cond ((eq comment-type 'multiple-line)
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
(when (looking-at "[[:space:]]*\\(#\\)")
|
|
(replace-match "" nil nil nil 1))))
|
|
((eq comment-type 'single-line)
|
|
(coffee-indent-line)))))
|
|
|
|
(defun coffee-auto-fill-fn ()
|
|
(let ((comment-type (coffee-get-comment-info))
|
|
(fill-result (do-auto-fill))
|
|
(coffee-indent-like-python-mode t))
|
|
(when (and fill-result (eq comment-type 'single-line))
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
(when (looking-at "[[:space:]]*#")
|
|
(replace-match "#")))
|
|
(coffee-indent-line))))
|
|
|
|
;;
|
|
;; Define Major Mode
|
|
;;
|
|
|
|
(defvar coffee-mode-syntax-table
|
|
(let ((table (make-syntax-table)))
|
|
;; perl style comment: "# ..."
|
|
(modify-syntax-entry ?# "< b" table)
|
|
(modify-syntax-entry ?\n "> b" table)
|
|
|
|
;; Treat slashes as paired delimiters; useful for finding regexps.
|
|
(modify-syntax-entry ?/ "/" table)
|
|
|
|
;; single quote strings
|
|
(modify-syntax-entry ?' "\"" table)
|
|
table))
|
|
|
|
;;;###autoload
|
|
(define-derived-mode coffee-mode prog-mode "Coffee"
|
|
"Major mode for editing CoffeeScript."
|
|
|
|
;; code for syntax highlighting
|
|
(setq font-lock-defaults '((coffee-font-lock-keywords)))
|
|
|
|
;; fix comment filling function
|
|
(set (make-local-variable 'comment-line-break-function)
|
|
#'coffee-comment-line-break-fn)
|
|
(set (make-local-variable 'normal-auto-fill-function) #'coffee-auto-fill-fn)
|
|
|
|
(set (make-local-variable 'comment-start) "#")
|
|
|
|
;; indentation
|
|
(make-local-variable 'coffee-tab-width)
|
|
(make-local-variable 'coffee-indent-tabs-mode)
|
|
(set (make-local-variable 'indent-line-function) 'coffee-indent-line)
|
|
(set (make-local-variable 'indent-region-function) 'coffee-indent-region)
|
|
(set (make-local-variable 'tab-width) coffee-tab-width)
|
|
|
|
(set (make-local-variable 'syntax-propertize-function)
|
|
'coffee-syntax-propertize-function)
|
|
|
|
;; fill
|
|
(set (make-local-variable 'fill-forward-paragraph-function)
|
|
'coffee-fill-forward-paragraph-function)
|
|
|
|
(set (make-local-variable 'beginning-of-defun-function)
|
|
'coffee-beginning-of-defun)
|
|
(set (make-local-variable 'end-of-defun-function)
|
|
'coffee-end-of-block)
|
|
|
|
;; imenu
|
|
(set (make-local-variable 'imenu-create-index-function)
|
|
'coffee-imenu-create-index)
|
|
|
|
;; Don't let electric-indent-mode break coffee-mode.
|
|
(set (make-local-variable 'electric-indent-functions)
|
|
(list (lambda (_arg) 'no-indent)))
|
|
|
|
;; no tabs
|
|
(setq indent-tabs-mode coffee-indent-tabs-mode))
|
|
|
|
;;
|
|
;; Compile-on-Save minor mode
|
|
;;
|
|
|
|
(defcustom coffee-cos-mode-line " CoS"
|
|
"Lighter of `coffee-cos-mode'"
|
|
:type 'string)
|
|
|
|
(define-minor-mode coffee-cos-mode
|
|
"Toggle compile-on-save for coffee-mode.
|
|
|
|
Add `'(lambda () (coffee-cos-mode t))' to `coffee-mode-hook' to turn
|
|
it on by default."
|
|
:lighter coffee-cos-mode-line
|
|
(if coffee-cos-mode
|
|
(add-hook 'after-save-hook 'coffee-compile-file nil t)
|
|
(remove-hook 'after-save-hook 'coffee-compile-file t)))
|
|
|
|
;;
|
|
;; Live compile minor mode
|
|
;;
|
|
|
|
(defun coffee--live-compile (&rest _unused)
|
|
(when (or (not coffee--process)
|
|
(not (eq (process-status coffee--process) 'run)))
|
|
(coffee-compile-buffer)))
|
|
|
|
(defcustom coffee-live-compile-mode-line " LiveCS"
|
|
"Lighter of `coffee-live-compile-mode'"
|
|
:type 'string)
|
|
|
|
(define-minor-mode coffee-live-compile-mode
|
|
"Compile current buffer in real time"
|
|
:lighter coffee-live-comp-mode-line
|
|
(if coffee-live-compile-mode
|
|
(add-hook 'after-change-functions 'coffee--live-compile nil t)
|
|
(remove-hook 'after-change-functions 'coffee--live-compile 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 '("\\.iced\\'" . coffee-mode))
|
|
;;;###autoload
|
|
(add-to-list 'auto-mode-alist '("Cakefile\\'" . coffee-mode))
|
|
;;;###autoload
|
|
(add-to-list 'auto-mode-alist '("\\.cson\\'" . coffee-mode))
|
|
;;;###autoload
|
|
(add-to-list 'interpreter-mode-alist '("coffee" . coffee-mode))
|
|
|
|
;;; coffee-mode.el ends here
|