You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1221 lines
45 KiB

;;; clojure-mode.el --- Major mode for Clojure code -*- lexical-binding: t; -*-
;; Copyright © 2007-2015 Jeffrey Chu, Lennart Staflin, Phil Hagelberg
;; Copyright © 2013-2015 Bozhidar Batsov
;;
;; Authors: Jeffrey Chu <jochu0@gmail.com>
;; Lennart Staflin <lenst@lysator.liu.se>
;; Phil Hagelberg <technomancy@gmail.com>
;; Bozhidar Batsov <bozhidar@batsov.com>
;; URL: http://github.com/clojure-emacs/clojure-mode
;; Package-Version: 20151022.27
;; Keywords: languages clojure clojurescript lisp
;; Version: 5.0.0-cvs
;; Package-Requires: ((emacs "24.3"))
;; This file is not part of GNU Emacs.
;;; Commentary:
;; Provides font-lock, indentation, and navigation for the Clojure
;; programming language (http://clojure.org).
;; Using clojure-mode with paredit or smartparens is highly recommended.
;; Here are some example configurations:
;; ;; require or autoload paredit-mode
;; (add-hook 'clojure-mode-hook #'paredit-mode)
;; ;; require or autoload smartparens
;; (add-hook 'clojure-mode-hook #'smartparens-strict-mode)
;; See inf-clojure (http://github.com/clojure-emacs/inf-clojure) for
;; basic interaction with Clojure subprocesses.
;; See CIDER (http://github.com/clojure-emacs/cider) for
;; better interaction with subprocesses via nREPL.
;;; License:
;; 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 3
;; of the License, 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 GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Code:
(eval-when-compile
(defvar calculate-lisp-indent-last-sexp)
(defvar font-lock-beg)
(defvar font-lock-end)
(defvar paredit-space-for-delimiter-predicates)
(defvar paredit-version)
(defvar paredit-mode))
(require 'cl-lib)
(require 'imenu)
(require 'newcomment)
(declare-function lisp-fill-paragraph "lisp-mode" (&optional justify))
(defgroup clojure nil
"Major mode for editing Clojure code."
:prefix "clojure-"
:group 'languages
:link '(url-link :tag "Github" "https://github.com/clojure-emacs/clojure-mode")
:link '(emacs-commentary-link :tag "Commentary" "clojure-mode"))
(defconst clojure-mode-version "5.0.0-snapshot"
"The current version of `clojure-mode'.")
(defface clojure-keyword-face
'((t (:inherit font-lock-constant-face)))
"Face used to font-lock Clojure keywords (:something)."
:group 'clojure
:package-version '(clojure-mode . "3.0.0"))
(defface clojure-character-face
'((t (:inherit font-lock-string-face)))
"Face used to font-lock Clojure character literals."
:group 'clojure
:package-version '(clojure-mode . "3.0.0"))
(defface clojure-interop-method-face
'((t (:inherit font-lock-preprocessor-face)))
"Face used to font-lock interop method names (camelCase)."
:group 'clojure
:package-version '(clojure-mode . "3.0.0"))
(defcustom clojure-defun-style-default-indent nil
"When non-nil, use default indenting for functions and macros.
Otherwise check `define-clojure-indent' and `put-clojure-indent'."
:type 'boolean
:group 'clojure
:safe 'booleanp)
(defcustom clojure-use-backtracking-indent t
"When non-nil, enable context sensitive indentation."
:type 'boolean
:group 'clojure
:safe 'booleanp)
(defcustom clojure-max-backtracking 3
"Maximum amount to backtrack up a list to check for context."
:type 'integer
:group 'clojure
:safe 'integerp)
(defcustom clojure-docstring-fill-column fill-column
"Value of `fill-column' to use when filling a docstring."
:type 'integer
:group 'clojure
:safe 'integerp)
(defcustom clojure-docstring-fill-prefix-width 2
"Width of `fill-prefix' when filling a docstring.
The default value conforms with the de facto convention for
Clojure docstrings, aligning the second line with the opening
double quotes on the third column."
:type 'integer
:group 'clojure
:safe 'integerp)
(defcustom clojure-omit-space-between-tag-and-delimiters '(?\[ ?\{)
"Allowed opening delimiter characters after a reader literal tag.
For example, \[ is allowed in :db/id[:db.part/user]."
:type '(set (const :tag "[" ?\[)
(const :tag "{" ?\{)
(const :tag "(" ?\()
(const :tag "\"" ?\"))
:group 'clojure
:safe (lambda (value)
(and (listp value)
(cl-every 'characterp value))))
(defvar clojure-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-:") #'clojure-toggle-keyword-string)
(easy-menu-define clojure-mode-menu map "Clojure Mode Menu"
'("Clojure"
["Toggle between string & keyword" clojure-toggle-keyword-string]
"--"
["Insert ns form at point" clojure-insert-ns-form-at-point]
["Insert ns form at beginning" clojure-insert-ns-form]
["Update ns form" clojure-update-ns]
"--"
["Version" clojure-mode-display-version]))
map)
"Keymap for Clojure mode.")
(defvar clojure-mode-syntax-table
(let ((table (copy-syntax-table emacs-lisp-mode-syntax-table)))
(modify-syntax-entry ?~ "' " table)
(modify-syntax-entry ?\{ "(}" table)
(modify-syntax-entry ?\} "){" table)
(modify-syntax-entry ?\[ "(]" table)
(modify-syntax-entry ?\] ")[" table)
(modify-syntax-entry ?^ "'" table)
(modify-syntax-entry ?@ "'" table)
;; Make hash a usual word character
(modify-syntax-entry ?# "_ p" table)
table)
"Syntax table for Clojure mode.
Inherits from `emacs-lisp-mode-syntax-table'.")
(defconst clojure--prettify-symbols-alist
'(("fn" . )))
(defun clojure-mode-display-version ()
"Display the current `clojure-mode-version' in the minibuffer."
(interactive)
(message "clojure-mode (version %s)" clojure-mode-version))
(defun clojure-space-for-delimiter-p (endp delim)
"Prevent paredit from inserting useless spaces.
See `paredit-space-for-delimiter-predicates' for the meaning of
ENDP and DELIM."
(if (or (derived-mode-p 'clojure-mode)
(derived-mode-p 'cider-repl-mode))
(save-excursion
(backward-char)
(if (and (or (char-equal delim ?\()
(char-equal delim ?\")
(char-equal delim ?{))
(not endp))
(if (char-equal (char-after) ?#)
(and (not (bobp))
(or (char-equal ?w (char-syntax (char-before)))
(char-equal ?_ (char-syntax (char-before)))))
t)
t))
t))
(defun clojure-no-space-after-tag (endp delimiter)
"Prevent inserting a space after a reader-literal tag?
When a reader-literal tag is followed be an opening delimiter
listed in `clojure-omit-space-between-tag-and-delimiters', this
function returns t.
This allows you to write things like #db/id[:db.part/user]
without inserting a space between the tag and the opening
bracket.
See `paredit-space-for-delimiter-predicates' for the meaning of
ENDP and DELIMITER."
(if endp
t
(or (not (member delimiter clojure-omit-space-between-tag-and-delimiters))
(save-excursion
(let ((orig-point (point)))
(not (and (re-search-backward
"#\\([a-zA-Z0-9._-]+/\\)?[a-zA-Z0-9._-]+"
(line-beginning-position)
t)
(= orig-point (match-end 0)))))))))
(declare-function paredit-open-curly "ext:paredit")
(declare-function paredit-close-curly "ext:paredit")
(defun clojure-paredit-setup ()
"Make \"paredit-mode\" play nice with `clojure-mode'."
(when (>= paredit-version 21)
(define-key clojure-mode-map "{" #'paredit-open-curly)
(define-key clojure-mode-map "}" #'paredit-close-curly)
(add-to-list 'paredit-space-for-delimiter-predicates
#'clojure-space-for-delimiter-p)
(add-to-list 'paredit-space-for-delimiter-predicates
#'clojure-no-space-after-tag)))
(defun clojure-mode-variables ()
"Set up initial buffer-local variables for Clojure mode."
(setq-local imenu-create-index-function
(lambda ()
(imenu--generic-function '((nil clojure-match-next-def 0)))))
(setq-local indent-tabs-mode nil)
(setq-local paragraph-ignore-fill-prefix t)
(setq-local outline-regexp ";;;\\(;* [^ \t\n]\\)\\|(")
(setq-local outline-level 'lisp-outline-level)
(setq-local comment-start ";")
(setq-local comment-start-skip ";+ *")
(setq-local comment-add 1) ; default to `;;' in comment-region
(setq-local comment-column 40)
(setq-local comment-use-syntax t)
(setq-local multibyte-syntax-as-symbol t)
(setq-local electric-pair-skip-whitespace 'chomp)
(setq-local electric-pair-open-newline-between-pairs nil)
(setq-local fill-paragraph-function #'clojure-fill-paragraph)
(setq-local adaptive-fill-function #'clojure-adaptive-fill-function)
(setq-local normal-auto-fill-function #'clojure-auto-fill-function)
(setq-local comment-start-skip
"\\(\\(^\\|[^\\\\\n]\\)\\(\\\\\\\\\\)*\\)\\(;+\\|#|\\) *")
(setq-local indent-line-function #'clojure-indent-line)
(setq-local lisp-indent-function #'clojure-indent-function)
(setq-local lisp-doc-string-elt-property 'clojure-doc-string-elt)
(setq-local parse-sexp-ignore-comments t)
(setq-local prettify-symbols-alist clojure--prettify-symbols-alist)
(setq-local open-paren-in-column-0-is-defun-start nil))
;;;###autoload
(define-derived-mode clojure-mode prog-mode "Clojure"
"Major mode for editing Clojure code.
\\{clojure-mode-map}"
(clojure-mode-variables)
(clojure-font-lock-setup)
(add-hook 'paredit-mode-hook #'clojure-paredit-setup))
(defsubst clojure-in-docstring-p ()
"Check whether point is in a docstring."
(eq (get-text-property (point) 'face) 'font-lock-doc-face))
(defsubst clojure-docstring-fill-prefix ()
"The prefix string used by `clojure-fill-paragraph'.
It is simply `clojure-docstring-fill-prefix-width' number of spaces."
(make-string clojure-docstring-fill-prefix-width ? ))
(defun clojure-adaptive-fill-function ()
"Clojure adaptive fill function.
This only takes care of filling docstring correctly."
(when (clojure-in-docstring-p)
(clojure-docstring-fill-prefix)))
(defun clojure-fill-paragraph (&optional justify)
"Like `fill-paragraph', but can handle Clojure docstrings.
If JUSTIFY is non-nil, justify as well as fill the paragraph."
(if (clojure-in-docstring-p)
(let ((paragraph-start
(concat paragraph-start
"\\|\\s-*\\([(;:\"[]\\|~@\\|`(\\|#'(\\)"))
(paragraph-separate
(concat paragraph-separate "\\|\\s-*\".*[,\\.]$"))
(fill-column (or clojure-docstring-fill-column fill-column))
(fill-prefix (clojure-docstring-fill-prefix)))
(fill-paragraph justify))
(let ((paragraph-start (concat paragraph-start
"\\|\\s-*\\([(;:\"[]\\|`(\\|#'(\\)"))
(paragraph-separate
(concat paragraph-separate "\\|\\s-*\".*[,\\.[]$")))
(or (fill-comment-paragraph justify)
(fill-paragraph justify))
;; Always return `t'
t)))
(defun clojure-auto-fill-function ()
"Clojure auto-fill function."
;; Check if auto-filling is meaningful.
(let ((fc (current-fill-column)))
(when (and fc (> (current-column) fc))
(let ((fill-column (if (clojure-in-docstring-p)
clojure-docstring-fill-column
fill-column))
(fill-prefix (clojure-adaptive-fill-function)))
(do-auto-fill)))))
(defun clojure-match-next-def ()
"Scans the buffer backwards for the next \"top-level\" definition.
Called by `imenu--generic-function'."
;; we have to take into account namespace-definition forms
;; e.g. s/defn
(when (re-search-backward "^(\\([a-z0-9.-]+/\\)?def\\sw*" nil t)
(save-excursion
(let (found?
(start (point)))
(down-list)
(forward-sexp)
(while (not found?)
(forward-sexp)
(or (if (char-equal ?[ (char-after (point)))
(backward-sexp))
(if (char-equal ?) (char-after (point)))
(backward-sexp)))
(cl-destructuring-bind (def-beg . def-end) (bounds-of-thing-at-point 'sexp)
(if (char-equal ?^ (char-after def-beg))
(progn (forward-sexp) (backward-sexp))
(setq found? t)
(set-match-data (list def-beg def-end)))))
(goto-char start)))))
(defconst clojure-font-lock-keywords
(eval-when-compile
`(;; Top-level variable definition
(,(concat "(\\(?:clojure.core/\\)?\\("
(regexp-opt '("def" "defonce"))
;; variable declarations
"\\)\\>"
;; Any whitespace
"[ \r\n\t]*"
;; Possibly type or metadata
"\\(?:#?^\\(?:{[^}]*}\\|\\sw+\\)[ \r\n\t]*\\)*"
"\\(\\sw+\\)?")
(1 font-lock-keyword-face)
(2 font-lock-variable-name-face nil t))
;; Type definition
(,(concat "(\\(?:clojure.core/\\)?\\("
(regexp-opt '("defstruct" "deftype" "defprotocol"
"defrecord"))
;; type declarations
"\\)\\>"
;; Any whitespace
"[ \r\n\t]*"
;; Possibly type or metadata
"\\(?:#?^\\(?:{[^}]*}\\|\\sw+\\)[ \r\n\t]*\\)*"
"\\(\\sw+\\)?")
(1 font-lock-keyword-face)
(2 font-lock-type-face nil t))
;; Function definition (anything that starts with def and is not
;; listed above)
(,(concat "(\\(?:[a-z\.-]+/\\)?\\(def[^ \r\n\t]*\\)"
;; Function declarations
"\\>"
;; Any whitespace
"[ \r\n\t]*"
;; Possibly type or metadata
"\\(?:#?^\\(?:{[^}]*}\\|\\sw+\\)[ \r\n\t]*\\)*"
"\\(\\sw+\\)?")
(1 font-lock-keyword-face)
(2 font-lock-function-name-face nil t))
;; (fn name? args ...)
(,(concat "(\\(?:clojure.core/\\)?\\(fn\\)[ \t]+"
;; Possibly type
"\\(?:#?^\\sw+[ \t]*\\)?"
;; Possibly name
"\\(t\\sw+\\)?" )
(1 font-lock-keyword-face)
(2 font-lock-function-name-face nil t))
;; lambda arguments - %, %1, %2, etc
("\\<%[1-9]?" (0 font-lock-variable-name-face))
;; Special forms
(,(concat
"("
(regexp-opt
'("def" "do" "if" "let" "var" "fn" "loop"
"recur" "throw" "try" "catch" "finally"
"set!" "new" "."
"monitor-enter" "monitor-exit" "quote") t)
"\\>")
1 font-lock-keyword-face)
;; Built-in binding and flow of control forms
(,(concat
"(\\(?:clojure.core/\\)?"
(regexp-opt
'("letfn" "case" "cond" "cond->" "cond->>" "condp"
"for" "when" "when-not" "when-let" "when-first" "when-some"
"if-let" "if-not" "if-some"
".." "->" "->>" "as->" "doto" "and" "or"
"dosync" "doseq" "dotimes" "dorun" "doall"
"ns" "in-ns"
"with-open" "with-local-vars" "binding"
"with-redefs" "with-redefs-fn"
"declare") t)
"\\>")
1 font-lock-keyword-face)
(,(concat
"\\<"
(regexp-opt
'("*1" "*2" "*3" "*agent*"
"*allow-unresolved-vars*" "*assert*" "*clojure-version*"
"*command-line-args*" "*compile-files*"
"*compile-path*" "*data-readers*" "*default-data-reader-fn*"
"*e" "*err*" "*file*" "*flush-on-newline*"
"*in*" "*macro-meta*" "*math-context*" "*ns*" "*out*"
"*print-dup*" "*print-length*" "*print-level*"
"*print-meta*" "*print-readably*"
"*read-eval*" "*source-path*"
"*unchecked-math*"
"*use-context-classloader*" "*warn-on-reflection*")
t)
"\\>")
0 font-lock-builtin-face)
;; Dynamic variables - *something* or @*something*
("\\(?:\\<\\|/\\)@?\\(\\*[a-z-]*\\*\\)\\>" 1 font-lock-variable-name-face)
;; Global constants - nil, true, false
(,(concat
"\\<"
(regexp-opt
'("true" "false" "nil") t)
"\\>")
0 font-lock-constant-face)
;; Character literals - \1, \a, \newline, \u0000
("\\\\\\([[:punct:]]\\|[a-z0-9]+\\>\\)" 0 'clojure-character-face)
;; foo/ Foo/ @Foo/ /FooBar
("\\(?:\\<:?\\|\\.\\)@?\\([a-zA-Z][.a-zA-Z0-9$_-]*\\)/" 1 font-lock-type-face)
;; Constant values (keywords), including as metadata e.g. ^:static
("\\<^?\\(:\\(\\sw\\|\\s_\\)+\\(\\>\\|\\_>\\)\\)" 1 'clojure-keyword-face append)
;; Java interop highlighting
;; CONST SOME_CONST (optionally prefixed by /)
("\\(?:\\<\\|/\\)\\([A-Z]+\\|\\([A-Z]+_[A-Z1-9_]+\\)\\)\\>" 1 font-lock-constant-face)
;; .foo .barBaz .qux01 .-flibble .-flibbleWobble
("\\<\\.-?[a-z][a-zA-Z0-9]*\\>" 0 'clojure-interop-method-face)
;; Foo Bar$Baz Qux_ World_OpenUDP Foo. Babylon15.
("\\(?:\\<\\|\\.\\|/\\|#?^\\)\\([A-Z][a-zA-Z0-9_]*[a-zA-Z0-9$_]+\\.?\\>\\)" 1 font-lock-type-face)
;; foo.bar.baz
("\\<^?\\([a-z][a-z0-9_-]+\\.\\([a-z][a-z0-9_-]*\\.?\\)+\\)" 1 font-lock-type-face)
;; (ns namespace) - special handling for single segment namespaces
(,(concat "(\\<ns\\>[ \r\n\t]*"
;; Possibly metadata
"\\(?:\\^?{[^}]+}[ \r\n\t]*\\)*"
;; namespace
"\\([a-z0-9-]+\\)")
(1 font-lock-type-face nil t))
;; fooBar
("\\(?:\\<\\|/\\)\\([a-z]+[A-Z]+[a-zA-Z0-9$]*\\>\\)" 1 'clojure-interop-method-face)
;; Highlight `code` marks, just like `elisp'.
(,(rx "`" (group-n 1 (+ (or (syntax symbol) (syntax word)))) "`")
(1 'font-lock-constant-face prepend))
;; Highlight grouping constructs in regular expressions
(clojure-font-lock-regexp-groups
(1 'font-lock-regexp-grouping-construct prepend))))
"Default expressions to highlight in Clojure mode.")
(defun clojure-font-lock-syntactic-face-function (state)
"Find and highlight text with a Clojure-friendly syntax table.
This function is passed to `font-lock-syntactic-face-function',
which is called with a single parameter, STATE (which is, in
turn, returned by `parse-partial-sexp' at the beginning of the
highlighted region)."
(if (nth 3 state)
;; This might be a (doc)string or a |...| symbol.
(let ((startpos (nth 8 state)))
(if (eq (char-after startpos) ?|)
;; This is not a string, but a |...| symbol.
nil
(let* ((listbeg (nth 1 state))
(firstsym (and listbeg
(save-excursion
(goto-char listbeg)
(and (looking-at "([ \t\n]*\\(\\(\\sw\\|\\s_\\)+\\)")
(match-string 1)))))
(docelt (and firstsym
(function-get (intern-soft firstsym)
lisp-doc-string-elt-property))))
(if (and docelt
;; It's a string in a form that can have a docstring.
;; Check whether it's in docstring position.
(save-excursion
(when (functionp docelt)
(goto-char (match-end 1))
(setq docelt (funcall docelt)))
(goto-char listbeg)
(forward-char 1)
(condition-case nil
(while (and (> docelt 0) (< (point) startpos)
(progn (forward-sexp 1) t))
;; ignore metadata and type hints
(unless (looking-at "[ \n\t]*\\(\\^[A-Z:].+\\|\\^?{.+\\)")
(setq docelt (1- docelt))))
(error nil))
(and (zerop docelt) (<= (point) startpos)
(progn (forward-comment (point-max)) t)
(= (point) (nth 8 state)))))
font-lock-doc-face
font-lock-string-face))))
font-lock-comment-face))
(defun clojure-font-lock-setup ()
"Configures font-lock for editing Clojure code."
(setq-local font-lock-multiline t)
(add-to-list 'font-lock-extend-region-functions
#'clojure-font-lock-extend-region-def t)
(setq font-lock-defaults
'(clojure-font-lock-keywords ; keywords
nil nil
(("+-*/.<>=!?$%_&~^:@" . "w")) ; syntax alist
nil
(font-lock-mark-block-function . mark-defun)
(font-lock-syntactic-face-function
. clojure-font-lock-syntactic-face-function))))
(defun clojure-font-lock-def-at-point (point)
"Range between the top-most def* and the fourth element after POINT.
Note that this means that there is no guarantee of proper font
locking in def* forms that are not at top level."
(goto-char point)
(condition-case nil
(beginning-of-defun)
(error nil))
(let ((beg-def (point)))
(when (and (not (= point beg-def))
(looking-at "(def"))
(condition-case nil
(progn
;; move forward as much as possible until failure (or success)
(forward-char)
(dotimes (_ 4)
(forward-sexp)))
(error nil))
(cons beg-def (point)))))
(defun clojure-font-lock-extend-region-def ()
"Set region boundaries to include the first four elements of def* forms."
(let ((changed nil))
(let ((def (clojure-font-lock-def-at-point font-lock-beg)))
(when def
(cl-destructuring-bind (def-beg . def-end) def
(when (and (< def-beg font-lock-beg)
(< font-lock-beg def-end))
(setq font-lock-beg def-beg
changed t)))))
(let ((def (clojure-font-lock-def-at-point font-lock-end)))
(when def
(cl-destructuring-bind (def-beg . def-end) def
(when (and (< def-beg font-lock-end)
(< font-lock-end def-end))
(setq font-lock-end def-end
changed t)))))
changed))
(defun clojure-font-lock-regexp-groups (bound)
"Highlight grouping constructs in regular expression.
BOUND denotes the maximum number of characters (relative to the
point) to check."
(catch 'found
(while (re-search-forward (concat
;; A group may start using several alternatives:
"\\(\\(?:"
;; 1. (? special groups
"(\\?\\(?:"
;; a) non-capturing group (?:X)
;; b) independent non-capturing group (?>X)
;; c) zero-width positive lookahead (?=X)
;; d) zero-width negative lookahead (?!X)
"[:=!>]\\|"
;; e) zero-width positive lookbehind (?<=X)
;; f) zero-width negative lookbehind (?<!X)
"<[=!]\\|"
;; g) named capturing group (?<name>X)
"<[[:alnum:]]+>"
"\\)\\|" ;; end of special groups
;; 2. normal capturing groups (
;; 3. we also highlight alternative
;; separarators |, and closing parens )
"[|()]"
"\\)\\)")
bound t)
(let ((face (get-text-property (1- (point)) 'face)))
(when (and (or (and (listp face)
(memq 'font-lock-string-face face))
(eq 'font-lock-string-face face))
(clojure-string-start t))
(throw 'found t))))))
;; Docstring positions
(put 'ns 'clojure-doc-string-elt 2)
(put 'def 'clojure-doc-string-elt 2)
(put 'defn 'clojure-doc-string-elt 2)
(put 'defn- 'clojure-doc-string-elt 2)
(put 'defmulti 'clojure-doc-string-elt 2)
(put 'defmacro 'clojure-doc-string-elt 2)
(put 'definline 'clojure-doc-string-elt 2)
(put 'defprotocol 'clojure-doc-string-elt 2)
(defun clojure-indent-line ()
"Indent current line as Clojure code."
(if (clojure-in-docstring-p)
(save-excursion
(beginning-of-line)
(when (and (looking-at "^\\s-*")
(<= (string-width (match-string-no-properties 0))
(string-width (clojure-docstring-fill-prefix))))
(replace-match (clojure-docstring-fill-prefix))))
(lisp-indent-line)))
(defvar clojure-get-indent-function nil
"Function to get the indent spec of a symbol.
This function should take one argument, the name of the symbol as
a string. This name will be exactly as it appears in the buffer,
so it might start with a namespace alias.
This function is analogous to the `clojure-indent-function'
symbol property, and its return value should match one of the
allowed values of this property. See `clojure-indent-function'
for more information.")
(defun clojure--get-indent-method (function-name)
"Return the indent spec for the symbol named FUNCTION-NAME.
FUNCTION-NAME is a string. If it contains a `/', also try only
the part after the `/'.
Look for a spec using `clojure-get-indent-function', then try the
`clojure-indent-function' and `clojure-backtracking-indent'
symbol properties."
(or (when (functionp clojure-get-indent-function)
(funcall clojure-get-indent-function function-name))
(get (intern-soft function-name) 'clojure-indent-function)
(get (intern-soft function-name) 'clojure-backtracking-indent)
(when (string-match "/\\([^/]+\\)\\'" function-name)
(or (get (intern-soft (match-string 1 function-name))
'clojure-indent-function)
(get (intern-soft (match-string 1 function-name))
'clojure-backtracking-indent)))))
(defvar clojure--current-backtracking-depth 0)
(defun clojure--find-indent-spec-backtracking ()
"Return the indent sexp that applies to the sexp at point.
Implementation function for `clojure--find-indent-spec'."
(when (and (>= clojure-max-backtracking clojure--current-backtracking-depth)
(not (looking-at "^")))
(let ((clojure--current-backtracking-depth (1+ clojure--current-backtracking-depth))
(pos 0))
;; Count how far we are from the start of the sexp.
(while (ignore-errors (clojure-backward-logical-sexp 1)
(not (or (bobp)
(eq (char-before) ?\n))))
(cl-incf pos))
(let* ((function (thing-at-point 'symbol))
(method (or (when function ;; Is there a spec here?
(clojure--get-indent-method function))
;; `up-list' errors on unbalanced sexps.
(ignore-errors
(up-list) ;; Otherwise look higher up.
(clojure-backward-logical-sexp 1)
(clojure--find-indent-spec-backtracking)))))
(when (numberp method)
(setq method (list method)))
(pcase method
((pred sequencep)
(pcase (length method)
(`0 nil)
(`1 (let ((head (elt method 0)))
(when (or (= pos 0) (sequencep head))
head)))
(l (if (>= pos l)
(elt method (1- l))
(elt method pos)))))
((or `defun `:defn)
(when (= pos 0)
:defn))
((pred functionp)
(when (= pos 0)
method))
(_
(message "Invalid indent spec for `%s': %s" function method)
nil))))))
(defun clojure--find-indent-spec ()
"Return the indent spec that applies to current sexp.
If `clojure-use-backtracking-indent' is non-nil, also do
backtracking up to a higher-level sexp in order to find the
spec."
(if clojure-use-backtracking-indent
(save-excursion
(clojure--find-indent-spec-backtracking))
(let ((function (thing-at-point 'symbol)))
(clojure--get-indent-method function))))
(defun clojure--normal-indent (last-sexp)
"Return the normal indentation column for a sexp.
LAST-SEXP is the start of the previous sexp."
(goto-char last-sexp)
(forward-sexp 1)
(clojure-backward-logical-sexp 1)
(let ((last-sexp-start nil))
(unless (ignore-errors
(while (string-match
"[^[:blank:]]"
(buffer-substring (line-beginning-position) (point)))
(setq last-sexp-start (prog1 (point)
(forward-sexp -1))))
t)
;; If the last sexp was on the same line.
(when (and last-sexp-start
(> (line-end-position) last-sexp-start))
(goto-char last-sexp-start)))
(current-column)))
(defun clojure--not-function-form-p ()
"Non-nil if form at point doesn't represent a function call."
(or (member (char-after) '(?\[ ?\{))
(save-excursion ;; Catch #?@ (:cljs ...)
(skip-chars-backward "\r\n[:blank:]")
(when (eq (char-before) ?@)
(forward-char -1))
(and (eq (char-before) ?\?)
(eq (char-before (1- (point))) ?\#)))
;; Car of form is not a symbol.
(not (looking-at ".\\(?:\\sw\\|\\s_\\)"))))
(defun clojure-indent-function (indent-point state)
"When indenting a line within a function call, indent properly.
INDENT-POINT is the position where the user typed TAB, or equivalent.
Point is located at the point to indent under (for default indentation);
STATE is the `parse-partial-sexp' state for that position.
If the current line is in a call to a Clojure function with a
non-nil property `clojure-indent-function', that specifies how to do
the indentation.
The property value can be
- `defun', meaning indent `defun'-style;
- an integer N, meaning indent the first N arguments specially
like ordinary function arguments and then indent any further
arguments like a body;
- a function to call just as this function was called.
If that function returns nil, that means it doesn't specify
the indentation.
- a list, which is used by `clojure-backtracking-indent'.
This function also returns nil meaning don't specify the indentation."
;; Goto to the open-paren.
(goto-char (elt state 1))
;; Maps, sets, vectors and reader conditionals.
(if (clojure--not-function-form-p)
(1+ (current-column))
;; Function or macro call.
(forward-char 1)
(let ((method (clojure--find-indent-spec))
(containing-form-column (1- (current-column))))
(pcase method
((or (pred integerp) `(,method))
(let ((pos -1))
(condition-case nil
(while (and (<= (point) indent-point)
(not (eobp)))
(clojure-forward-logical-sexp 1)
(cl-incf pos))
;; If indent-point is _after_ the last sexp in the
;; current sexp, we detect that by catching the
;; `scan-error'. In that case, we should return the
;; indentation as if there were an extra sexp at point.
(scan-error (cl-incf pos)))
(cond
((= pos (1+ method))
(+ lisp-body-indent containing-form-column))
((> pos (1+ method))
(clojure--normal-indent calculate-lisp-indent-last-sexp))
(t
(+ (* 2 lisp-body-indent) containing-form-column)))))
(`:defn
(+ lisp-body-indent containing-form-column))
((pred functionp)
(funcall method indent-point state))
((and `nil
(guard (let ((function (thing-at-point 'sexp)))
(or (and clojure-defun-style-default-indent
;; largely to preserve useful alignment of :require, etc in ns
(not (string-match "^:" function)))
(string-match "\\`\\(?:\\S +/\\)?\\(def\\|with-\\)"
function)))))
(+ lisp-body-indent containing-form-column))
(_ (clojure--normal-indent calculate-lisp-indent-last-sexp))))))
;;; Setting indentation
(defun put-clojure-indent (sym indent)
"Instruct `clojure-indent-function' to indent the body of SYM by INDENT."
(put sym 'clojure-indent-function indent))
(defmacro define-clojure-indent (&rest kvs)
"Call `put-clojure-indent' on a series, KVS."
`(progn
,@(mapcar (lambda (x) `(put-clojure-indent
(quote ,(car x)) ,(cadr x)))
kvs)))
(defun add-custom-clojure-indents (name value)
"Allow `clojure-defun-indents' to indent user-specified macros.
Requires the macro's NAME and a VALUE."
(custom-set-default name value)
(mapcar (lambda (x)
(put-clojure-indent x 'defun))
value))
(defcustom clojure-defun-indents nil
"List of additional symbols with defun-style indentation in Clojure.
You can use this to let Emacs indent your own macros the same way
that it indents built-in macros like with-open. To manually set
it from Lisp code, use (put-clojure-indent 'some-symbol :defn)."
:type '(repeat symbol)
:group 'clojure
:set 'add-custom-clojure-indents)
(define-clojure-indent
;; built-ins
(ns 1)
(fn :defn)
(def :defn)
(defn :defn)
(bound-fn :defn)
(if 1)
(if-not 1)
(case 1)
(cond 0)
(condp 2)
(cond-> 1)
(cond->> 1)
(when 1)
(while 1)
(when-not 1)
(when-first 1)
(do 0)
(future 0)
(comment 0)
(doto 1)
(locking 1)
(proxy '(2 nil nil (1)))
(as-> 2)
(reify '(1 nil (1)))
(deftype '(2 nil nil (1)))
(defrecord '(2 nil nil (1)))
(defprotocol '(1))
(extend 1)
(extend-protocol '(1 (1)))
(extend-type '(1 (1)))
(specify '(1 (1)))
(specify! '(1 (1)))
(implement '(1 (1)))
(try 0)
(catch 2)
(finally 0)
;; binding forms
(let 1)
(letfn '(1 ((1)) nil))
(binding 1)
(loop 1)
(for 1)
(doseq 1)
(dotimes 1)
(when-let 1)
(if-let 1)
(when-some 1)
(if-some 1)
(defmethod :defn)
;; clojure.test
(testing 1)
(deftest :defn)
(are 2)
(use-fixtures :defn)
;; core.logic
(run :defn)
(run* :defn)
(fresh :defn)
;; core.async
(alt! 0)
(alt!! 0)
(go 0)
(go-loop 1)
(thread 0))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Better docstring filling for clojure-mode
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun clojure-string-start (&optional regex)
"Return the position of the \" that begins the string at point.
If REGEX is non-nil, return the position of the # that begins the
regex at point. If point is not inside a string or regex, return
nil."
(when (nth 3 (syntax-ppss)) ;; Are we really in a string?
(save-excursion
(save-match-data
;; Find a quote that appears immediately after whitespace,
;; beginning of line, hash, or an open paren, brace, or bracket
(re-search-backward "\\(\\s-\\|^\\|#\\|(\\|\\[\\|{\\)\\(\"\\)")
(let ((beg (match-beginning 2)))
(when beg
(if regex
(and (char-before beg) (char-equal ?# (char-before beg)) (1- beg))
(when (not (char-equal ?# (char-before beg)))
beg))))))))
(defun clojure-char-at-point ()
"Return the char at point or nil if at buffer end."
(when (not (= (point) (point-max)))
(buffer-substring-no-properties (point) (1+ (point)))))
(defun clojure-char-before-point ()
"Return the char before point or nil if at buffer beginning."
(when (not (= (point) (point-min)))
(buffer-substring-no-properties (point) (1- (point)))))
(defun clojure-toggle-keyword-string ()
"Convert the string or keyword at point to keyword or string."
(interactive)
(let ((original-point (point)))
(while (and (> (point) 1)
(not (equal "\"" (buffer-substring-no-properties (point) (+ 1 (point)))))
(not (equal ":" (buffer-substring-no-properties (point) (+ 1 (point))))))
(backward-char))
(cond
((equal 1 (point))
(error "Beginning of file reached, this was probably a mistake"))
((equal "\"" (buffer-substring-no-properties (point) (+ 1 (point))))
(insert ":" (substring (clojure-delete-and-extract-sexp) 1 -1)))
((equal ":" (buffer-substring-no-properties (point) (+ 1 (point))))
(insert "\"" (substring (clojure-delete-and-extract-sexp) 1) "\"")))
(goto-char original-point)))
(defun clojure-delete-and-extract-sexp ()
"Delete the sexp and return it."
(interactive)
(let ((begin (point)))
(forward-sexp)
(let ((result (buffer-substring-no-properties begin (point))))
(delete-region begin (point))
result)))
(defconst clojure-namespace-name-regex
(rx line-start
(zero-or-more whitespace)
"("
(zero-or-one (group (regexp "clojure.core/")))
(zero-or-one (submatch "in-"))
"ns"
(zero-or-one "+")
(one-or-more (any whitespace "\n"))
(zero-or-more (or (submatch (zero-or-one "#")
"^{"
(zero-or-more (not (any "}")))
"}")
(zero-or-more "^:"
(one-or-more (not (any whitespace)))))
(one-or-more (any whitespace "\n")))
;; why is this here? oh (in-ns 'foo) or (ns+ :user)
(zero-or-one (any ":'"))
(group (one-or-more (not (any "()\"" whitespace))) word-end)))
;; for testing clojure-namespace-name-regex, you can evaluate this code and make
;; sure foo (or whatever the namespace name is) shows up in results. some of
;; these currently fail.
;; (mapcar (lambda (s) (let ((n (string-match clojure-namespace-name-regex s)))
;; (if n (match-string 4 s))))
;; '("(ns foo)"
;; "(ns
;; foo)"
;; "(ns foo.baz)"
;; "(ns ^:bar foo)"
;; "(ns ^:bar ^:baz foo)"
;; "(ns ^{:bar true} foo)"
;; "(ns #^{:bar true} foo)"
;; "(ns #^{:fail {}} foo)"
;; "(ns ^{:fail2 {}} foo.baz)"
;; "(ns ^{} foo)"
;; "(ns ^{:skip-wiki true}
;; aleph.netty
;; "
;; "(ns
;; foo)"
;; "foo"))
(defun clojure-project-dir (&optional dir-name)
"Return the absolute path to the project's root directory.
Use `default-directory' if DIR-NAME is nil.
Return nil if not inside a project."
(let ((dir-name (or dir-name default-directory)))
(let ((lein-project-dir (locate-dominating-file dir-name "project.clj"))
(boot-project-dir (locate-dominating-file dir-name "build.boot")))
(when (or lein-project-dir boot-project-dir)
(file-truename
(cond ((not lein-project-dir) boot-project-dir)
((not boot-project-dir) lein-project-dir)
(t (if (file-in-directory-p lein-project-dir boot-project-dir)
lein-project-dir
boot-project-dir))))))))
(defun clojure-project-relative-path (path)
"Denormalize PATH by making it relative to the project root."
(file-relative-name path (clojure-project-dir)))
(defun clojure-expected-ns (&optional path)
"Return the namespace matching PATH.
PATH is expected to be an absolute file path.
If PATH is nil, use the path to the file backing the current buffer."
(let* ((path (or path (file-truename (buffer-file-name))))
(relative (clojure-project-relative-path path))
(sans-file-type (substring relative 0 (- (length (file-name-extension path t)))))
(sans-file-sep (mapconcat 'identity (cdr (split-string sans-file-type "/")) "."))
(sans-underscores (replace-regexp-in-string "_" "-" sans-file-sep)))
;; Drop prefix from ns for projects with structure src/{clj,cljs,cljc}
(replace-regexp-in-string "\\`clj[scx]?\\." "" sans-underscores)))
(defun clojure-insert-ns-form-at-point ()
"Insert a namespace form at point."
(interactive)
(insert (format "(ns %s)" (clojure-expected-ns))))
(defun clojure-insert-ns-form ()
"Insert a namespace form at the beginning of the buffer."
(interactive)
(widen)
(goto-char (point-min))
(clojure-insert-ns-form-at-point))
(defun clojure-update-ns ()
"Update the namespace of the current buffer.
Useful if a file has been renamed."
(interactive)
(let ((nsname (clojure-expected-ns)))
(when nsname
(save-excursion
(save-match-data
(if (clojure-find-ns)
(replace-match nsname nil nil nil 4)
(error "Namespace not found")))))))
(defun clojure-find-ns ()
"Find the namespace of the current Clojure buffer."
(let ((regexp clojure-namespace-name-regex))
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(when (re-search-forward regexp nil t)
(match-string-no-properties 4))))))
(defun clojure-find-def ()
"Find the var declaration macro and symbol name of the current form.
Returns a list pair, e.g. (\"defn\" \"abc\") or (\"deftest\" \"some-test\")."
(let ((re (concat "(\\(?:\\(?:\\sw\\|\\s_\\)+/\\)?"
;; Declaration
"\\(def\\sw*\\)\\>"
;; Any whitespace
"[ \r\n\t]*"
;; Possibly type or metadata
"\\(?:#?^\\(?:{[^}]*}\\|\\(?:\\sw\\|\\s_\\)+\\)[ \r\n\t]*\\)*"
;; Symbol name
"\\(\\(?:\\sw\\|\\s_\\)+\\)")))
(save-excursion
(unless (looking-at re)
(beginning-of-defun))
(when (search-forward-regexp re nil t)
(list (match-string-no-properties 1)
(match-string-no-properties 2))))))
;;; Sexp navigation
(defun clojure--looking-at-non-logical-sexp ()
"Return non-nil if sexp after point represents code.
Sexps that don't represent code are ^metadata or #reader.macros."
(comment-normalize-vars)
(comment-forward (point-max))
(looking-at-p "\\^\\|#[?[:alpha:]]"))
(defun clojure-forward-logical-sexp (&optional n)
"Move forward N logical sexps.
This will skip over sexps that don't represent objects, so that ^hints and
#reader.macros are considered part of the following sexp."
(interactive "p")
(if (< n 0)
(clojure-backward-logical-sexp (- n))
(let ((forward-sexp-function nil))
(while (> n 0)
(while (clojure--looking-at-non-logical-sexp)
(forward-sexp 1))
;; The actual sexp
(forward-sexp 1)
(setq n (1- n))))))
(defun clojure-backward-logical-sexp (&optional n)
"Move backward N logical sexps.
This will skip over sexps that don't represent objects, so that ^hints and
#reader.macros are considered part of the following sexp."
(interactive "p")
(if (< n 0)
(clojure-forward-logical-sexp (- n))
(let ((forward-sexp-function nil))
(while (> n 0)
;; The actual sexp
(backward-sexp 1)
;; Non-logical sexps.
(while (and (not (bobp))
(ignore-errors
(save-excursion
(backward-sexp 1)
(clojure--looking-at-non-logical-sexp))))
(backward-sexp 1))
(setq n (1- n))))))
(defconst clojurescript-font-lock-keywords
(eval-when-compile
`(;; ClojureScript built-ins
(,(concat "(\\(?:\.*/\\)?"
(regexp-opt '("js-obj" "js-delete" "clj->js" "js->clj"))
"\\>")
0 font-lock-builtin-face)))
"Additional font-locking for `clojurescrip-mode'.")
;;;###autoload
(define-derived-mode clojurescript-mode clojure-mode "ClojureScript"
"Major mode for editing ClojureScript code.
\\{clojurescript-mode-map}"
(font-lock-add-keywords nil clojurescript-font-lock-keywords))
;;;###autoload
(define-derived-mode clojurec-mode clojure-mode "ClojureC"
"Major mode for editing ClojureC code.
\\{clojurec-mode-map}")
(defconst clojurex-font-lock-keywords
;; cljx annotations (#+clj and #+cljs)
'(("#\\+cljs?\\>" 0 font-lock-preprocessor-face))
"Additional font-locking for `clojurex-mode'.")
;;;###autoload
(define-derived-mode clojurex-mode clojure-mode "ClojureX"
"Major mode for editing ClojureX code.
\\{clojurex-mode-map}"
(font-lock-add-keywords nil clojurex-font-lock-keywords))
;;;###autoload
(progn
(add-to-list 'auto-mode-alist
'("\\.\\(clj\\|dtm\\|edn\\)\\'" . clojure-mode))
(add-to-list 'auto-mode-alist '("\\.cljc\\'" . clojurec-mode))
(add-to-list 'auto-mode-alist '("\\.cljx\\'" . clojurex-mode))
(add-to-list 'auto-mode-alist '("\\.cljs\\'" . clojurescript-mode))
;; boot build scripts are Clojure source files
(add-to-list 'auto-mode-alist '("\\(?:build\\|profile\\)\\.boot\\'" . clojure-mode)))
(provide 'clojure-mode)
;; Local Variables:
;; coding: utf-8
;; indent-tabs-mode: nil
;; End:
;;; clojure-mode.el ends here