|
|
- ;;; nix-mode.el --- Major mode for editing .nix files -*- lexical-binding: t -*-
-
- ;; Maintainer: Matthew Bauer <mjbauer95@gmail.com>
- ;; Homepage: https://github.com/NixOS/nix-mode
- ;; Version: 1.4.1
- ;; Keywords: nix, languages, tools, unix
- ;; Package-Requires: ((emacs "25"))
-
- ;; This file is NOT part of GNU Emacs.
-
- ;;; Commentary:
-
- ;; A major mode for editing Nix expressions (.nix files). See the Nix manual
- ;; for more information available at https://nixos.org/nix/manual/.
-
- ;;; Code:
-
- (require 'nix)
- (require 'nix-format)
- (require 'nix-shebang)
- (require 'nix-shell)
- (require 'nix-repl)
- (require 'smie)
- (require 'ffap)
- (eval-when-compile (require 'subr-x))
-
- (defgroup nix-mode nil
- "Nix mode customizations"
- :group 'nix)
-
- (defcustom nix-indent-function 'smie-indent-line
- "The function to use to indent.
-
- Valid functions for this are:
-
- - ‘indent-relative’
- - ‘nix-indent-line' (buggy)
- - `smie-indent-line' (‘nix-mode-use-smie’ must be enabled)"
- :group 'nix-mode
- :type 'function)
-
- (defcustom nix-mode-use-smie t
- "Whether to use SMIE when editing Nix files.
- This is enabled by default, but can take a while to load with
- very large Nix files (all-packages.nix)."
- :group 'nix-mode
- :type 'boolean)
-
- (defgroup nix-faces nil
- "Nix faces."
- :group 'nix-mode
- :group 'faces)
-
- (defface nix-keyword-face
- '((t :inherit font-lock-keyword-face))
- "Face used to highlight Nix keywords."
- :group 'nix-faces)
-
- (defface nix-keyword-warning-face
- '((t :inherit font-lock-warning-face))
- "Face used to highlight Nix warning keywords."
- :group 'nix-faces)
-
- (defface nix-builtin-face
- '((t :inherit font-lock-builtin-face))
- "Face used to highlight Nix builtins."
- :group 'nix-faces)
-
- (defface nix-constant-face
- '((t :inherit font-lock-constant-face))
- "Face used to highlight Nix constants."
- :group 'nix-faces)
-
- (defface nix-attribute-face
- '((t :inherit font-lock-variable-name-face))
- "Face used to highlight Nix attributes."
- :group 'nix-faces)
-
- (defface nix-antiquote-face
- '((t :inherit font-lock-preprocessor-face))
- "Face used to highlight Nix antiquotes."
- :group 'nix-faces)
-
- ;;; Constants
-
- (defconst nix-system-types
- '("x86_64-linux" "i686-linux" "aarch64-linux" "x86_64-darwin")
- "List of supported systems.")
-
- (defconst nix-keywords
- '("if" "then"
- "else" "with"
- "let" "in"
- "rec" "inherit"
- "or"))
-
- (defconst nix-builtins
- '("builtins" "baseNameOf"
- "derivation" "dirOf"
- "true" "false" "null"
- "isNull" "toString"
- "fetchTarball" "import"
- "map" "removeAttrs"))
-
- (defconst nix-warning-keywords
- '("assert" "abort" "throw"))
-
- ;;; Regexps
-
- (defconst nix-re-file-path
- "[a-zA-Z0-9._\\+-]*\\(/[a-zA-Z0-9._\\+-]+\\)+")
-
- (defconst nix-re-url
- "[a-zA-Z][a-zA-Z0-9\\+-\\.]*:[a-zA-Z0-9%/\\?:@&=\\+\\$,_\\.!~\\*'-]+")
-
- (defconst nix-re-bracket-path
- "<[a-zA-Z0-9._\\+-]+\\(/[a-zA-Z0-9._\\+-]+\\)*>")
-
- (defconst nix-re-variable-assign
- "\\<\\([a-zA-Z_][a-zA-Z0-9_'\-\.]*\\)[ \t]*=[^=]")
-
- (defconst nix-re-caps
- " =[ \n]\\|\(\\|\{\\|\\[\\|\\bwith\\b\\|\\blet\\b\\|\\binherit\\b")
-
- (defconst nix-re-ends ";\\|\)\\|\\]\\|\}\\|\\bin\\b")
-
- (defconst nix-re-quotes "''\\|\"")
-
- (defconst nix-re-comments "#\\|/*\\|*/")
-
- (defun nix-re-keywords (keywords)
- "Produce a regexp matching some keywords of Nix.
- KEYWORDS a list of strings to match as Nix keywords."
- (concat
- "\\(?:[[:space:];:{}()]\\|^\\)"
- (regexp-opt keywords t)
- "\\(?:[[:space:];:{}()]\\|$\\)"
- ))
-
- (defconst nix-font-lock-keywords
- `((,(nix-re-keywords nix-keywords) 1 'nix-keyword-face)
- (,(nix-re-keywords nix-warning-keywords) 1 'nix-keyword-warning-face)
- (,(nix-re-keywords nix-builtins) 1 'nix-builtin-face)
- (,nix-re-url 0 'nix-constant-face)
- (,nix-re-file-path 0 'nix-constant-face)
- (,nix-re-variable-assign 1 'nix-attribute-face)
- (,nix-re-bracket-path 0 'nix-constant-face)
- (nix--syntax-match-antiquote 0 'nix-antiquote-face t))
- "Font lock keywords for nix.")
-
- (defconst nix--variable-char "[a-zA-Z0-9_'\-]")
-
- (defvar nix-mode-abbrev-table
- (make-abbrev-table)
- "Abbrev table for Nix mode.")
-
- (defvar nix-mode-syntax-table
- (let ((table (make-syntax-table)))
- (modify-syntax-entry ?/ ". 14" table)
- (modify-syntax-entry ?* ". 23" table)
- (modify-syntax-entry ?# "< b" table)
- (modify-syntax-entry ?\n "> b" 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)
- ;; We handle strings
- (modify-syntax-entry ?\" "." table)
- ;; We handle escapes
- (modify-syntax-entry ?\\ "." table)
- table)
- "Syntax table for Nix mode.")
-
- (defun nix--syntax-match-antiquote (limit)
- "Find antiquote within a Nix expression up to LIMIT."
- (unless (> (point) limit)
- (if (get-text-property (point) 'nix-syntax-antiquote)
- (progn
- (set-match-data (list (point) (1+ (point))))
- (forward-char 1)
- t)
- (let ((pos (next-single-char-property-change (point) 'nix-syntax-antiquote
- nil limit)))
- (when (and pos (not (> pos limit)))
- (goto-char pos)
- (let ((char (char-after pos)))
- (pcase char
- (`?{
- (forward-char 1)
- (set-match-data (list (1- pos) (point)))
- t)
- (`?}
- (forward-char 1)
- (set-match-data (list pos (point)))
- t))))))))
-
- (defun nix--mark-string (pos string-type)
- "Mark string as a Nix string.
-
- POS position of start of string
- STRING-TYPE type of string based off of Emacs syntax table types"
- (put-text-property pos (1+ pos)
- 'syntax-table (string-to-syntax "|"))
- (put-text-property pos (1+ pos)
- 'nix-string-type string-type))
-
- (defun nix--get-parse-state (pos)
- "Get the result of `syntax-ppss' at POS."
- (save-excursion (save-match-data (syntax-ppss pos))))
-
- (defun nix--get-string-type (parse-state)
- "Get the type of string based on PARSE-STATE."
- (let ((string-start (nth 8 parse-state)))
- (and string-start (get-text-property string-start 'nix-string-type))))
-
- (defun nix--open-brace-string-type (parse-state)
- "Determine if this is an open brace string type based on PARSE-STATE."
- (let ((open-brace (nth 1 parse-state)))
- (and open-brace (get-text-property open-brace 'nix-string-type))))
-
- (defun nix--open-brace-antiquote-p (parse-state)
- "Determine if this is an open brace antiquote based on PARSE-STATE."
- (let ((open-brace (nth 1 parse-state)))
- (and open-brace (get-text-property open-brace 'nix-syntax-antiquote))))
-
- (defun nix--single-quotes ()
- "Handle Nix single quotes."
- (let* ((start (match-beginning 0))
- (end (match-end 0))
- (context (nix--get-parse-state start))
- (string-type (nix--get-string-type context)))
- (unless (or (equal string-type ?\")
- (and (equal string-type nil)
- (< 1 start)
- (string-match-p nix--variable-char
- (buffer-substring (1- start) start))))
- (when (equal string-type nil)
- (nix--mark-string start ?\')
- (setq start (+ 2 start)))
- (when (equal (mod (- end start) 3) 2)
- (let ((str-peek (buffer-substring end (min (point-max) (+ 2 end)))))
- (if (member str-peek '("${" "\\n" "\\r" "\\t"))
- (goto-char (+ 2 end))
- (nix--mark-string (1- end) ?\')))))))
-
- (defun nix--escaped-antiquote-dq-style ()
- "Handle Nix escaped antiquote dq style."
- (let* ((start (match-beginning 0))
- (ps (nix--get-parse-state start))
- (string-type (nix--get-string-type ps)))
- (when (equal string-type ?\')
- (nix--antiquote-open-at (1+ start) ?\'))))
-
- (defun nix--double-quotes ()
- "Handle Nix double quotes."
- (let* ((pos (match-beginning 0))
- (ps (nix--get-parse-state pos))
- (string-type (nix--get-string-type ps)))
- (unless (equal string-type ?\')
- (nix--mark-string pos ?\"))))
-
- (defun nix--antiquote-open-at (pos string-type)
- "Handle Nix antiquote open at based on POS and STRING-TYPE."
- (put-text-property pos (1+ pos)
- 'syntax-table (string-to-syntax "|"))
- (put-text-property pos (+ 2 pos)
- 'nix-string-type string-type)
- (put-text-property (1+ pos) (+ 2 pos)
- 'nix-syntax-antiquote t))
-
- (defun nix--antiquote-open ()
- "Handle Nix antiquote open."
- (let* ((start (match-beginning 0))
- (ps (nix--get-parse-state start))
- (string-type (nix--get-string-type ps)))
- (when string-type
- (nix--antiquote-open-at start string-type))))
-
- (defun nix--antiquote-close-open ()
- "Handle Nix antiquote close then open."
- (let* ((start (match-beginning 0))
- (ps (nix--get-parse-state start))
- (string-type (nix--get-string-type ps)))
- (if string-type
- (nix--antiquote-open-at (1+ start) string-type)
- (when (nix--open-brace-antiquote-p ps)
- (let ((string-type (nix--open-brace-string-type ps)))
- (put-text-property start (+ 3 start)
- 'nix-string-type string-type)
- (put-text-property start (1+ start)
- 'nix-syntax-antiquote t)
- (put-text-property (+ 2 start) (+ 3 start)
- 'nix-syntax-antiquote t))))))
-
- (defun nix--antiquote-close ()
- "Handle Nix antiquote close."
- (let* ((start (match-beginning 0))
- (ps (nix--get-parse-state start)))
- (unless (nix--get-string-type ps)
- (let ((string-type (nix--open-brace-string-type ps)))
- (when string-type
- (put-text-property start (1+ start)
- 'nix-string-type string-type)
- (put-text-property start (1+ start)
- 'nix-syntax-antiquote t)
- (let ((ahead (buffer-substring (1+ start)
- (min (point-max) (+ 5 start)))))
- (pcase string-type
- (`?\" (cond
- ((or (string-match "^\\\\\"" ahead)
- (string-match "^\\\\\\${" ahead))
- (nix--mark-string (1+ start) string-type)
- (goto-char (+ start (match-end 0) 1)))
- ((string-match-p "^\"" ahead)
- (goto-char (+ 2 start)))
- ((< (1+ start) (point-max))
- (nix--mark-string (1+ start) string-type)
- (goto-char (+ 2 start)))))
- (`?\' (cond
- ((or (string-match "^'''" ahead)
- (string-match "^''\\${" ahead)
- (string-match "^''\\\\[nrt]" ahead))
- (nix--mark-string (1+ start) string-type)
- (goto-char (+ start (match-end 0) 1)))
- ((string-match-p "^''" ahead)
- (goto-char (+ 3 start)))
- ((< (1+ start) (point-max))
- (nix--mark-string (1+ start) string-type)
- (goto-char (+ 2 start))))))))))))
-
- (defun nix-syntax-propertize (start end)
- "Special syntax properties for Nix from START to END."
- (goto-char start)
- (remove-text-properties start end
- '(nix-string-type nil nix-syntax-antiquote nil))
- (funcall
- (syntax-propertize-rules
- ("\\\\\\\\"
- (0 nil))
- ("\\\\\""
- (0 nil))
- ("\\\\\\${"
- (0 (ignore (nix--escaped-antiquote-dq-style))))
- ("'\\{2,\\}"
- (0 (ignore (nix--single-quotes))))
- ("}\\${"
- (0 (ignore (nix--antiquote-close-open))))
- ("\\${"
- (0 (ignore (nix--antiquote-open))))
- ("}"
- (0 (ignore (nix--antiquote-close))))
- ("\""
- (0 (ignore (nix--double-quotes)))))
- start end))
-
- ;; Indentation using SMIE
-
- (defconst nix-smie-grammar
- (smie-prec2->grammar
- (smie-merge-prec2s
- (smie-bnf->prec2
- '((id)
- (expr (arg ":" expr)
- ("if" expr "then" expr "else" expr)
- ("let" decls "in" expr)
- ("with" expr "nonsep-;" expr)
- ("assert" expr "nonsep-;" expr)
- (attrset)
- (id))
- (attrset ("{" decls "}"))
- (decls (decls ";" decls)
- (id "=" expr))
- (arg (id) ("{" args "}"))
- (args (args "," args) (id "arg-?" expr)))
- '((assoc ";"))
- '((assoc ","))
- ;; resolve "(with foo; a) <op> b" vs "with foo; (a <op> b)"
- ;; in favor of the latter.
- '((nonassoc "nonsep-;") (nonassoc " -dummy- "))
- ;; resolve "(if ... then ... else a) <op> b"
- ;; vs "if ... then ... else (a <op> b)" in favor of the latter.
- '((nonassoc "in") (nonassoc " -dummy- ")))
- (smie-precs->prec2
- '((nonassoc " -dummy- ")
- (nonassoc "=")
- ;; " -bexpskip- " and " -fexpskip- " are handy tokens for skipping over
- ;; whole expressions.
- ;; For instance, suppose we have a line looking like this:
- ;; "{ foo.bar // { x = y }"
- ;; and point is at the end of the line. We can skip the whole
- ;; expression (i.e. so the point is just before "foo") using
- ;; `(smie-backward-sexp " -bexpskip- ")'. `(backward-sexp)' would
- ;; skip over "{ x = y }", not over the whole expression.
- (right " -bexpskip- ")
- (left " -fexpskip- ")
- (nonassoc "else")
- (right ":")
- (right "->")
- (assoc "||")
- (assoc "&&")
- (nonassoc "==" "!=")
- (nonassoc "<" "<=" ">" ">=")
- (left "//")
- (nonassoc "!")
- (assoc "-" "+")
- (assoc "*" "/")
- (assoc "++")
- (left "?")
- ;; Tokens for skipping sequences of sexps
- ;; (i.e. identifiers or balanced parens).
- ;; For instance, suppose we have a line looking like this:
- ;; "{ foo.bar // f x "
- ;; and point is at the end of the line. We can skip the "f x"
- ;; part by doing `(smie-backward-sexp " -bseqskip- ")'.
- (right " -bseqskip- ")
- (left " -fseqskip- "))))))
-
- (defconst nix-smie--symbol-chars ":->|&=!</-+*?,;!")
-
- (defconst nix-smie--infix-symbols-re
- (regexp-opt '(":" "->" "||" "&&" "==" "!=" "<" "<=" ">" ">="
- "//" "-" "+" "*" "/" "++" "?")))
-
- (defconst nix-smie-indent-tokens-re
- (regexp-opt '("{" "(" "[" "=" "let" "if" "then" "else")))
-
- ;; The core indentation algorithm is very simple:
- ;; - If the last token on the previous line matches `nix-smie-indent-tokens-re',
- ;; then the current line is indented by `tab-width' relative to the
- ;; previous line's 'anchor'.
- ;; - Otherwise, let SMIE handle it.
- ;; The 'anchor' of a line is defined as follows:
- ;; - If the line contains an assignment, it is the beginning of the
- ;; left-hand side of the first assignment on that line.
- ;; - Otherwise, it is the position of the first token on that line.
- (defun nix-smie-rules (kind token)
- "Core smie rules."
- (pcase (cons kind token)
- (`(:after . ,(guard (string-match-p nix-smie-indent-tokens-re
- token)))
- (nix-smie--indent-anchor))
- (`(,_ . "in")
- (let ((bol (line-beginning-position)))
- (forward-word)
- ;; Go back to the corresponding "let".
- (smie-backward-sexp t)
- (pcase kind
- (:before
- (if (smie-rule-hanging-p)
- (nix-smie--indent-anchor 0)
- `(column . ,(current-column))))
- (:after
- (cond
- ((bolp) '(column . 0))
- ((<= bol (point))
- `(column . ,(current-column))))))))
- (`(:after . "nonsep-;")
- (forward-char)
- (backward-sexp)
- (if (smie-rule-bolp)
- `(column . ,(current-column))
- (nix-smie--indent-anchor)))
- (`(:after . ":")
- (or (nix-smie--indent-args-line)
- (nix-smie--indent-anchor)))
- (`(:after . ",")
- (smie-rule-parent tab-width))
- (`(:before . ",")
- ;; The parent is either the enclosing "{" or some previous ",".
- ;; In both cases this is what we want to align to.
- (smie-rule-parent))
- (`(:before . "if")
- (let ((bol (line-beginning-position)))
- (save-excursion
- (and
- (equal (nix-smie--backward-token) "else")
- (<= bol (point))
- `(column . ,(current-column))))))
- (`(:before . ,(guard (string-match-p nix-smie--infix-symbols-re token)))
- (forward-comment (- (point)))
- (let ((bol (line-beginning-position)))
- (smie-backward-sexp token)
- (if (< (point) bol)
- (nix-smie--indent-anchor 0))))))
-
- (defun nix-smie--anchor ()
- "Return the anchor's offset from the beginning of the current line."
- (save-excursion
- (beginning-of-line)
- (let ((eol (line-end-position))
- anchor
- tok)
- (forward-comment (point-max))
- (unless (or (eobp) (< eol (point)))
- (setq anchor (current-column))
- (catch 'break
- (while (and (not (eobp))
- (progn
- (setq tok (car (smie-indent-forward-token)))
- (<= (point) eol)))
- (when (equal "=" tok)
- (backward-char)
- (smie-backward-sexp " -bseqskip- ")
- (setq anchor (current-column))
- (throw 'break nil))))
- anchor))))
-
- (defun nix-smie--indent-anchor (&optional indent)
- "Intended for use only in the rules function."
- (let ((indent (or indent tab-width)))
- `(column . ,(+ indent (nix-smie--anchor)))))
-
- (defun nix-smie--indent-args-line ()
- "Indent the body of a lambda whose argument(s) are on a line of their own."
- (save-excursion
- ;; Assume that point is right before ':', skip it
- (forward-char)
- (let ((tok ":"))
- (catch 'break
- (while (equal tok ":")
- (setq tok (nth 2 (smie-backward-sexp t)))
- (when (smie-rule-bolp)
- (throw 'break `(column . ,(current-column)))))))))
-
- (defconst nix-smie--path-chars "a-zA-Z0-9-+_.:/~")
-
- (defun nix-smie--skip-angle-path-forward ()
- "Skip forward a path enclosed in angle brackets, e.g <nixpkgs>"
- (let ((start (point)))
- (when (eq (char-after) ?<)
- (forward-char)
- (if (and (nix-smie--skip-path 'forward t)
- (eq (char-after) ?>))
- (progn
- (forward-char)
- (buffer-substring-no-properties start (point)))
- (ignore (goto-char start))))))
-
- (defun nix-smie--skip-angle-path-backward ()
- "Skip backward a path enclosed in angle brackets, e.g <nixpkgs>"
- (let ((start (point)))
- (when (eq (char-before) ?>)
- (backward-char)
- (if (and (nix-smie--skip-path 'backward t)
- (eq (char-before) ?<))
- (progn
- (backward-char)
- (buffer-substring-no-properties start (point)))
- (ignore (goto-char start))))))
-
- (defun nix-smie--skip-path (how &optional no-sep-check)
- "Skip path related characters."
- (let ((start (point)))
- (pcase-exhaustive how
- ('forward (skip-chars-forward nix-smie--path-chars))
- ('backward (skip-chars-backward nix-smie--path-chars)))
- (let ((sub (buffer-substring-no-properties start (point))))
- (if (or (and no-sep-check
- (< 0 (length sub)))
- (string-match-p "/" sub))
- sub
- (ignore (goto-char start))))))
-
- (defun nix-smie--forward-token-1 ()
- "Move forward one token."
- (forward-comment (point-max))
- (or (nix-smie--skip-angle-path-forward)
- (nix-smie--skip-path 'forward)
- (buffer-substring-no-properties
- (point)
- (progn
- (or (/= 0 (skip-syntax-forward "'w_"))
- (/= 0 (skip-chars-forward nix-smie--symbol-chars))
- (skip-syntax-forward ".'"))
- (point)))))
-
- (defun nix-smie--forward-token ()
- "Move forward one token, skipping certain characters."
- (let ((sym (nix-smie--forward-token-1)))
- (if (member sym '(";" "?"))
- ;; The important lexer for indentation's performance is the backward
- ;; lexer, so for the forward lexer we delegate to the backward one.
- (save-excursion (nix-smie--backward-token))
- sym)))
-
- (defun nix-smie--backward-token-1 ()
- "Move backward one token."
- (forward-comment (- (point)))
- (or (nix-smie--skip-angle-path-backward)
- (nix-smie--skip-path 'backward)
- (buffer-substring-no-properties
- (point)
- (progn
- (or (/= 0 (skip-syntax-backward "'w_"))
- (/= 0 (skip-chars-backward nix-smie--symbol-chars))
- (skip-syntax-backward ".'"))
- (point)))))
-
- (defun nix-smie--backward-token ()
- "Move backward one token, skipping certain characters."
- (let ((sym (nix-smie--backward-token-1)))
- (unless (zerop (length sym))
- (pcase sym
- (";" (if (nix-smie--nonsep-semicolon-p) "nonsep-;" ";"))
- ("?" (if (nix-smie--arg-?-p) "arg-?" "?"))
- (_ sym)))))
-
- (defun nix-smie--nonsep-semicolon-p ()
- "Whether the semicolon at point terminates a `with' or `assert'."
- (save-excursion
- (member (nth 2 (smie-backward-sexp " -bexpskip- ")) '("with" "assert"))))
-
- (defun nix-smie--arg-?-p ()
- "Whether the question mark at point is part of an argument declaration."
- (member
- (nth 2 (progn
- (smie-backward-sexp)
- (smie-backward-sexp)))
- '("{" ",")))
-
- (defun nix-smie--eol-p ()
- "Whether there are no tokens after point on the current line."
- (let ((eol (line-end-position)))
- (save-excursion
- (forward-comment (point-max))
- (or (eobp)
- (< eol (point))))))
-
- (defun nix-smie--indent-close ()
- "Align close paren with opening paren."
- (save-excursion
- (when (looking-at "\\s)")
- (forward-char 1)
- (condition-case nil
- (progn
- (backward-sexp 1)
- ;; If the opening paren is not the last token on its line,
- ;; and it's either '[' or '{', align to the opening paren's
- ;; position. Otherwise, align its line's anchor.
- (if (and (memq (char-after) '(?\[ ?{))
- (not (save-excursion (forward-char) (nix-smie--eol-p))))
- (current-column)
- (nix-smie--anchor)))
- (scan-error nil)))))
-
- (defun nix-smie--indent-exps ()
- "This function replaces and is based on `smie-indent-exps'.
- An argument to a function is indented relative to the function,
- not to any other arguments."
- (save-excursion
- (let (parent ;; token enclosing the expression list
- skipped) ;; whether we skipped at least one expression
- (let ((start (point)))
- (setq parent (nth 2 (smie-backward-sexp " -bseqskip- ")))
- (setq skipped (not (eq start (point))))
- (cond
- ((not skipped)
- ;; We're the first expression of the list. In that case, the
- ;; indentation should be (have been) determined by its context.
- nil)
- ((equal parent "[")
- ;; It's a list, align with the first expression.
- (current-column))
- ;; We're an argument.
- (t
- ;; We can use (current-column) or (current-indentation) here.
- ;; (current-column) will indent relative to the first expression
- ;; in the sequence, and (current-indentation) will indent relative
- ;; to the indentation of the line on which the first expression
- ;; begins. I'm not sure which one is better.
- (+ tab-width (current-indentation))))))))
-
- ;;; Indentation not using SMIE
-
- (defun nix-find-backward-matching-token ()
- "Find the previous Nix token."
- (cond
- ((looking-at "in\\b")
- (let ((counter 1))
- (while (and (> counter 0)
- (re-search-backward "\\b\\(let\\|in\\)\\b" nil t))
- (unless (or (nix--get-string-type (nix--get-parse-state (point)))
- (nix-is-comment-p))
- (setq counter (cond ((looking-at "let") (- counter 1))
- ((looking-at "in") (+ counter 1))))))
- counter ))
- ((looking-at "}")
- (backward-up-list) t)
- ((looking-at "]")
- (backward-up-list) t)
- ((looking-at ")")
- (backward-up-list) t)))
-
- (defun nix-indent-to-backward-match ()
- "Match the previous line’s indentation."
- (let ((matching-indentation (save-excursion
- (beginning-of-line)
- (skip-chars-forward "[:space:]")
- (if (nix-find-backward-matching-token)
- (current-indentation)))))
- (when matching-indentation (indent-line-to matching-indentation) t)))
-
- (defun nix-indent-first-line-in-block ()
- "Indent the first line in a block."
-
- (let ((matching-indentation (save-excursion
- ;; Go back to previous line that contain anything useful to check the
- ;; contents of that line.
- (beginning-of-line)
- (skip-chars-backward "\n[:space:]")
-
- ;; Grab the full string of the line before the one we're indenting
- (let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
- ;; Then regex-match strings at the end of the line to detect if we need to indent the line after.
- ;; We could probably add more things to look for here in the future.
- (if (or (string-match "let$" line)
- (string-match "import$" line)
- (string-match "\\[$" line)
- (string-match "=$" line)
- (string-match "\($" line)
- (string-match "\{$" line))
-
- ;; If it matches any of the regexes above, grab the indent level
- ;; of the line and add 2 to ident the line below this one.
- (+ 2 (current-indentation)))))))
- (when matching-indentation (indent-line-to matching-indentation) t)))
-
- (defun nix-mode-search-backward ()
- "Search backward for items of interest regarding indentation."
- (re-search-backward nix-re-ends nil t)
- (re-search-backward nix-re-quotes nil t)
- (re-search-backward nix-re-caps nil t))
-
- (defun nix-indent-expression-start ()
- "Indent the start of a nix expression."
- (let* ((ends 0)
- (once nil)
- (done nil)
- (indent (current-indentation)))
- (save-excursion
- ;; we want to indent this line, so we don't care what it
- ;; contains skip to the beginning so reverse searching doesn't
- ;; find any matches within
- (beginning-of-line)
- ;; search backward until an unbalanced cap is found or no cap or
- ;; end is found
- (while (and (not done) (nix-mode-search-backward))
- (cond
- ((looking-at nix-re-quotes)
- ;; skip over strings entirely
- (re-search-backward nix-re-quotes nil t))
- ((looking-at nix-re-comments)
- ;; skip over comments entirely
- (re-search-backward nix-re-comments nil t))
- ((looking-at nix-re-ends)
- ;; count the matched end
- ;; this means we expect to find at least one more cap
- (setq ends (+ ends 1)))
- ((looking-at nix-re-caps)
- ;; we found at least one cap
- ;; this means our function will return true
- ;; this signals to the caller we handled the indentation
- (setq once t)
- (if (> ends 0)
- ;; this cap corresponds to a previously matched end
- ;; reduce the number of unbalanced ends
- (setq ends (- ends 1))
- ;; no unbalanced ends correspond to this cap
- ;; this means we have found the expression that contains our line
- ;; we want to indent relative to this line
- (setq indent (current-indentation))
- ;; signal that the search loop should exit
- (setq done t))))))
- ;; done is t when we found an unbalanced expression cap
- (when done
- ;; indent relative to the indentation of the expression
- ;; containing our line
- (indent-line-to (+ tab-width indent)))
- ;; return t to the caller if we found at least one cap
- ;; this signals that we handled the indentation
- once))
-
- (defun nix-indent-prev-level ()
- "Get the indent level of the previous line."
- (save-excursion
- (beginning-of-line)
- (skip-chars-backward "\n[:space:]")
- (current-indentation)))
-
- ;;;###autoload
- (defun nix-mode-format ()
- "Format the entire `nix-mode' buffer."
- (interactive)
- (when (eq major-mode 'nix-mode)
- (save-excursion
- (goto-char (point-min))
- (while (not (equal (point) (point-max)))
- (if (equal (string-match-p "^[\s-]*$" (thing-at-point 'line)) 0)
- (delete-horizontal-space)
- (nix-indent-line))
- (forward-line)))))
-
- ;;;###autoload
- (defun nix-indent-line ()
- "Indent current line in a Nix expression."
- (interactive)
- (let ((end-of-indentation
- (save-excursion
- (cond
- ;; Indent first line of file to 0
- ((= (line-number-at-pos) 1)
- (indent-line-to 0))
-
- ;; comment
- ((save-excursion
- (beginning-of-line)
- (nix-is-comment-p))
- (indent-line-to (nix-indent-prev-level)))
-
- ;; string
- ((save-excursion
- (beginning-of-line)
- (nth 3 (syntax-ppss)))
- (indent-line-to (+ (nix-indent-prev-level)
- (* tab-width
- (+ (if (save-excursion
- (forward-line -1)
- (end-of-line)
- (skip-chars-backward "[:space:]")
- (looking-back "''" 0)) 1 0)
- (if (save-excursion
- (beginning-of-line)
- (skip-chars-forward
- "[:space:]")
- (looking-at "''")
- ) -1 0)
- )))))
-
- ;; dedent '}', ']', ')' 'in'
- ((nix-indent-to-backward-match))
-
- ;; indent line after 'let', 'import', '[', '=', '(', '{'
- ((nix-indent-first-line-in-block))
-
- ;; indent between = and ; + 2, or to 2
- ((nix-indent-expression-start))
-
- ;; else
- (t
- (indent-line-to (nix-indent-prev-level))))
- (point))))
- (when (> end-of-indentation (point)) (goto-char end-of-indentation))))
-
- (defun nix-is-comment-p ()
- "Whether we are in a comment."
- (nth 4 (syntax-ppss)))
-
- (defun nix-is-string-p ()
- "Whether we are in a string."
- (or (looking-at nix-re-quotes)
- (nix--get-string-type (nix--get-parse-state (point)))))
-
- ;;;###autoload
- (defun nix-indent-region (start end)
- "Indent on a whole region. Enabled by default.
- START where to start in region.
- END where to end the region."
- (interactive (list (region-beginning) (region-end)))
- (save-excursion
- (goto-char start)
- (while (< (point) end)
- (or (and (bolp) (eolp))
- (when (and
- ;; Skip if previous line is empty or a comment.
- (save-excursion
- (let ((line-is-comment-p (nix-is-comment-p)))
- (forward-line -1)
- (not
- (or (and (nix-is-comment-p)
- ;; Unless this line is a comment too.
- (not line-is-comment-p))
- (nix-is-comment-p)))))
- ;; Don't mess with strings.
- (nix-is-string-p))
- (funcall nix-indent-function)))
- (forward-line 1))))
-
- ;;;###autoload
- (defun nix-mode-ffap-nixpkgs-path (str)
- "Support `ffap' for <nixpkgs> declarations.
- If STR contains brackets, call `nix-instantiate' to find the
- location of STR. If `nix-instantiate' has a nonzero exit code,
- don’t do anything"
- (when (and (string-match nix-re-bracket-path str)
- (executable-find nix-instantiate-executable))
- (with-temp-buffer
- (when (eq (call-process nix-instantiate-executable nil (current-buffer)
- nil "--eval" "-E" str) 0)
- ;; Remove trailing newline
- (substring (buffer-string) 0 (- (buffer-size) 1))))))
-
- ;; Key maps
-
- (defvar nix-mode-menu (make-sparse-keymap "Nix")
- "Menu for Nix mode.")
-
- (defvar nix-mode-map (make-sparse-keymap)
- "Local keymap used for Nix mode.")
-
- (defun nix-create-keymap ()
- "Create the keymap associated with the Nix mode."
- )
-
- (defun nix-create-menu ()
- "Create the Nix menu as shown in the menu bar."
- (let ((m '("Nix"
- ["Format buffer" nix-format-buffer t])))
- (easy-menu-define nix-mode-menu nix-mode-map "Menu keymap for Nix mode" m)))
-
- (nix-create-keymap)
- (nix-create-menu)
-
- ;;;###autoload
- (define-derived-mode nix-mode prog-mode "Nix"
- "Major mode for editing Nix expressions.
-
- The following commands may be useful:
-
- '\\[newline-and-indent]'
- Insert a newline and move the cursor to align with the previous
- non-empty line.
-
- '\\[fill-paragraph]'
- Refill a paragraph so that all lines are at most `fill-column'
- lines long. This should do the right thing for comments beginning
- with `#'. However, this command doesn't work properly yet if the
- comment is adjacent to code (i.e., no intervening empty lines).
- In that case, select the text to be refilled and use
- `\\[fill-region]' instead.
-
- The hook `nix-mode-hook' is run when Nix mode is started.
-
- \\{nix-mode-map}
- "
- :group 'nix-mode
- :syntax-table nix-mode-syntax-table
- :abbrev-table nix-mode-abbrev-table
-
- ;; Disable hard tabs and set tab to 2 spaces
- ;; Recommended by nixpkgs manual: https://nixos.org/nixpkgs/manual/#sec-syntax
- (setq-local indent-tabs-mode nil)
- (setq-local tab-width 2)
- (setq-local electric-indent-chars '(?\n ?{ ?} ?\[ ?\] ?\( ?\)))
-
- ;; Font lock support.
- (setq-local font-lock-defaults '(nix-font-lock-keywords))
-
- ;; Special syntax properties for Nix
- (setq-local syntax-propertize-function 'nix-syntax-propertize)
-
- ;; Look at text properties when parsing
- (setq-local parse-sexp-lookup-properties t)
-
- ;; Setup SMIE integration
- (when nix-mode-use-smie
- (smie-setup nix-smie-grammar 'nix-smie-rules
- :forward-token 'nix-smie--forward-token
- :backward-token 'nix-smie--backward-token)
- (setq-local smie-indent-basic 2)
- (fset (make-local-variable 'smie-indent-exps)
- (symbol-function 'nix-smie--indent-exps))
- (fset (make-local-variable 'smie-indent-close)
- (symbol-function 'nix-smie--indent-close)))
-
- ;; Automatic indentation [C-j]
- (setq-local indent-line-function (lambda ()
- (if (and (not nix-mode-use-smie)
- (eq nix-indent-function 'smie-indent-line))
- (indent-relative)
- (funcall nix-indent-function))))
-
- ;; Indenting of comments
- (setq-local comment-start "# ")
- (setq-local comment-end "")
- (setq-local comment-start-skip "\\(^\\|\\s-\\);?#+ *")
- (setq-local comment-multi-line t)
-
- ;; Filling of comments
- (setq-local adaptive-fill-mode t)
- (setq-local paragraph-start "[ \t]*\\(#+[ \t]*\\)?$")
- (setq-local paragraph-separate paragraph-start)
-
- ;; Menu
- (easy-menu-add nix-mode-menu nix-mode-map)
-
- ;; Find file at point
- (push '(nix-mode . nix-mode-ffap-nixpkgs-path) ffap-alist)
- (push '(nix-mode "--:\\\\${}<>+@-Z_[:alpha:]~*?" "@" "@;.,!:")
- ffap-string-at-point-mode-alist))
-
- ;;;###autoload
- (add-to-list 'auto-mode-alist '("\\.nix\\'" . nix-mode))
-
- (provide 'nix-mode)
- ;;; nix-mode.el ends here
|