;;; nix-mode.el --- Major mode for editing .nix files -*- lexical-binding: t -*- ;; Maintainer: Matthew Bauer ;; Homepage: https://github.com/matthewbauer/nix-mode ;; Version: 1.2.1 ;; Keywords: nix, languages, tools, unix ;; Package-Requires: ((emacs "24.3")) ;; 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-format nil 'noerror) (defgroup nix nil "Nix-related customizations" :group 'languages) (defgroup nix-mode nil "Nix mode customizations" :group 'nix) (defgroup nix-faces nil "Nix faces." :group 'nix :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) (defvar nix-system-types '("x86_64-linux" "i686-linux" "aarch64-linux" "x86_64-darwin") "List of supported systems.") ;;; Syntax coloring (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")) (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-font-lock-keywords `( (,(regexp-opt nix-keywords 'symbols) 0 'nix-keyword-face) (,(regexp-opt nix-warning-keywords 'symbols) 0 'nix-keyword-warning-face) (,(regexp-opt nix-builtins 'symbols) 0 '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.") (makunbound 'nix-mode-syntax-table) (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) ;; 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 '(syntax-table nil 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 (defun nix-indent-level-parens () "Find indent level based on parens." (save-excursion (beginning-of-line) (let ((p1 (point)) (p2 (nth 1 (syntax-ppss))) (n 0)) ;; prevent moving beyond buffer (if (eq p2 1) (setq n (1+ n))) (while (and p2 (not (eq p2 1))) ;; make sure p2 > 1 (goto-char p2) (backward-char) (let ((l1 (line-number-at-pos p1)) (l2 (line-number-at-pos p2))) (if (not (eq l1 l2)) (setq n (1+ n)))) (setq p1 p2) (setq p2 (nth 1 (syntax-ppss))) ;; make sure we don't go beyond buffer (if (eq p2 1) (setq n (1+ n)))) n))) (defun nix-indent-level-is-closing () "Go forward from beginning of line." (save-excursion (beginning-of-line) (skip-chars-forward "[:space:]") (or ;; any of these should -1 indent level (looking-at ")") (looking-at "}") (looking-at "]") (looking-at "''") (looking-at ",") (looking-at "in[[:space:]]") (looking-at "in$")))) (defun nix-indent-level-is-hanging () "Is hanging?" (save-excursion (beginning-of-line) (skip-chars-forward "[:space:]") (if (or ;; (looking-at ",") (looking-at "{")) nil (forward-line -1) (end-of-line) (skip-chars-backward "\n[:space:]") ;; skip through any comments in the way (while (nth 4 (syntax-ppss)) (goto-char (nth 8 (syntax-ppss))) (skip-chars-backward "\n[:space:]")) (not (or (looking-back "{" 1) (looking-back "}" 1) (looking-back ":" 1) (looking-back ";" 1)))))) (defun nix-indent-prev-level-is-hanging () "Is the previous level hanging?" (save-excursion (beginning-of-line) (skip-chars-backward "\n[:space:]") (nix-indent-level-is-hanging))) (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))) (defun nix-indent-level () "Get current indent level." (if (nix-indent-level-is-hanging) (+ (nix-indent-prev-level) (* tab-width (+ (if (nix-indent-prev-level-is-hanging) 0 1) (if (nix-indent-level-is-closing) -1 0)))) (* tab-width (+ (nix-indent-level-parens) (if (nix-indent-level-is-closing) -1 0))))) (defun nix-indent-line () "Indent current line in a Nix expression." (interactive) (cond ;; comment ((save-excursion (beginning-of-line) (nth 4 (syntax-ppss))) (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) ))))) ;; else (t (indent-line-to (nix-indent-level))))) ;; 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." (define-key nix-mode-map "\C-c\C-r" 'nix-format-buffer)) (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 ada-mode-menu nix-mode-map "Menu keymap for Nix mode" m))) (nix-create-keymap) (nix-create-menu) (when (require 'company nil 'noerror) (require 'nix-company nil 'noerror)) (when (require 'mmm-mode nil 'noerror) (require 'nix-mode-mmm nil 'noerror)) ;;;###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) ;; 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) ;; Automatic indentation [C-j] (setq-local indent-line-function 'nix-indent-line) ;; 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) (easy-menu-add nix-mode-menu nix-mode-map)) ;;;###autoload (progn (add-to-list 'auto-mode-alist '("\\.nix\\'" . nix-mode)) (add-to-list 'auto-mode-alist '("\\.nix.in\\'" . nix-mode))) (provide 'nix-mode) ;;; nix-mode.el ends here