Klimi's new dotfiles with stow.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

1008 řádky
35 KiB

před 5 roky
  1. ;;; nix-mode.el --- Major mode for editing .nix files -*- lexical-binding: t -*-
  2. ;; Maintainer: Matthew Bauer <mjbauer95@gmail.com>
  3. ;; Homepage: https://github.com/NixOS/nix-mode
  4. ;; Version: 1.4.1
  5. ;; Keywords: nix, languages, tools, unix
  6. ;; Package-Requires: ((emacs "25"))
  7. ;; This file is NOT part of GNU Emacs.
  8. ;;; Commentary:
  9. ;; A major mode for editing Nix expressions (.nix files). See the Nix manual
  10. ;; for more information available at https://nixos.org/nix/manual/.
  11. ;;; Code:
  12. (require 'nix)
  13. (require 'nix-format)
  14. (require 'nix-shebang)
  15. (require 'nix-shell)
  16. (require 'nix-repl)
  17. (require 'smie)
  18. (require 'ffap)
  19. (eval-when-compile (require 'subr-x))
  20. (defgroup nix-mode nil
  21. "Nix mode customizations"
  22. :group 'nix)
  23. (defcustom nix-indent-function 'smie-indent-line
  24. "The function to use to indent.
  25. Valid functions for this are:
  26. - indent-relative
  27. - nix-indent-line' (buggy)
  28. - `smie-indent-line' (nix-mode-use-smie must be enabled)"
  29. :group 'nix-mode
  30. :type 'function)
  31. (defcustom nix-mode-use-smie t
  32. "Whether to use SMIE when editing Nix files.
  33. This is enabled by default, but can take a while to load with
  34. very large Nix files (all-packages.nix)."
  35. :group 'nix-mode
  36. :type 'boolean)
  37. (defgroup nix-faces nil
  38. "Nix faces."
  39. :group 'nix-mode
  40. :group 'faces)
  41. (defface nix-keyword-face
  42. '((t :inherit font-lock-keyword-face))
  43. "Face used to highlight Nix keywords."
  44. :group 'nix-faces)
  45. (defface nix-keyword-warning-face
  46. '((t :inherit font-lock-warning-face))
  47. "Face used to highlight Nix warning keywords."
  48. :group 'nix-faces)
  49. (defface nix-builtin-face
  50. '((t :inherit font-lock-builtin-face))
  51. "Face used to highlight Nix builtins."
  52. :group 'nix-faces)
  53. (defface nix-constant-face
  54. '((t :inherit font-lock-constant-face))
  55. "Face used to highlight Nix constants."
  56. :group 'nix-faces)
  57. (defface nix-attribute-face
  58. '((t :inherit font-lock-variable-name-face))
  59. "Face used to highlight Nix attributes."
  60. :group 'nix-faces)
  61. (defface nix-antiquote-face
  62. '((t :inherit font-lock-preprocessor-face))
  63. "Face used to highlight Nix antiquotes."
  64. :group 'nix-faces)
  65. ;;; Constants
  66. (defconst nix-system-types
  67. '("x86_64-linux" "i686-linux" "aarch64-linux" "x86_64-darwin")
  68. "List of supported systems.")
  69. (defconst nix-keywords
  70. '("if" "then"
  71. "else" "with"
  72. "let" "in"
  73. "rec" "inherit"
  74. "or"))
  75. (defconst nix-builtins
  76. '("builtins" "baseNameOf"
  77. "derivation" "dirOf"
  78. "true" "false" "null"
  79. "isNull" "toString"
  80. "fetchTarball" "import"
  81. "map" "removeAttrs"))
  82. (defconst nix-warning-keywords
  83. '("assert" "abort" "throw"))
  84. ;;; Regexps
  85. (defconst nix-re-file-path
  86. "[a-zA-Z0-9._\\+-]*\\(/[a-zA-Z0-9._\\+-]+\\)+")
  87. (defconst nix-re-url
  88. "[a-zA-Z][a-zA-Z0-9\\+-\\.]*:[a-zA-Z0-9%/\\?:@&=\\+\\$,_\\.!~\\*'-]+")
  89. (defconst nix-re-bracket-path
  90. "<[a-zA-Z0-9._\\+-]+\\(/[a-zA-Z0-9._\\+-]+\\)*>")
  91. (defconst nix-re-variable-assign
  92. "\\<\\([a-zA-Z_][a-zA-Z0-9_'\-\.]*\\)[ \t]*=[^=]")
  93. (defconst nix-re-caps
  94. " =[ \n]\\|\(\\|\{\\|\\[\\|\\bwith\\b\\|\\blet\\b\\|\\binherit\\b")
  95. (defconst nix-re-ends ";\\|\)\\|\\]\\|\}\\|\\bin\\b")
  96. (defconst nix-re-quotes "''\\|\"")
  97. (defconst nix-re-comments "#\\|/*\\|*/")
  98. (defun nix-re-keywords (keywords)
  99. "Produce a regexp matching some keywords of Nix.
  100. KEYWORDS a list of strings to match as Nix keywords."
  101. (concat
  102. "\\(?:[[:space:];:{}()]\\|^\\)"
  103. (regexp-opt keywords t)
  104. "\\(?:[[:space:];:{}()]\\|$\\)"
  105. ))
  106. (defconst nix-font-lock-keywords
  107. `((,(nix-re-keywords nix-keywords) 1 'nix-keyword-face)
  108. (,(nix-re-keywords nix-warning-keywords) 1 'nix-keyword-warning-face)
  109. (,(nix-re-keywords nix-builtins) 1 'nix-builtin-face)
  110. (,nix-re-url 0 'nix-constant-face)
  111. (,nix-re-file-path 0 'nix-constant-face)
  112. (,nix-re-variable-assign 1 'nix-attribute-face)
  113. (,nix-re-bracket-path 0 'nix-constant-face)
  114. (nix--syntax-match-antiquote 0 'nix-antiquote-face t))
  115. "Font lock keywords for nix.")
  116. (defconst nix--variable-char "[a-zA-Z0-9_'\-]")
  117. (defvar nix-mode-abbrev-table
  118. (make-abbrev-table)
  119. "Abbrev table for Nix mode.")
  120. (defvar nix-mode-syntax-table
  121. (let ((table (make-syntax-table)))
  122. (modify-syntax-entry ?/ ". 14" table)
  123. (modify-syntax-entry ?* ". 23" table)
  124. (modify-syntax-entry ?# "< b" table)
  125. (modify-syntax-entry ?\n "> b" table)
  126. (modify-syntax-entry ?_ "_" table)
  127. (modify-syntax-entry ?. "'" table)
  128. (modify-syntax-entry ?- "_" table)
  129. (modify-syntax-entry ?' "'" table)
  130. (modify-syntax-entry ?= "." table)
  131. (modify-syntax-entry ?< "." table)
  132. (modify-syntax-entry ?> "." table)
  133. ;; We handle strings
  134. (modify-syntax-entry ?\" "." table)
  135. ;; We handle escapes
  136. (modify-syntax-entry ?\\ "." table)
  137. table)
  138. "Syntax table for Nix mode.")
  139. (defun nix--syntax-match-antiquote (limit)
  140. "Find antiquote within a Nix expression up to LIMIT."
  141. (unless (> (point) limit)
  142. (if (get-text-property (point) 'nix-syntax-antiquote)
  143. (progn
  144. (set-match-data (list (point) (1+ (point))))
  145. (forward-char 1)
  146. t)
  147. (let ((pos (next-single-char-property-change (point) 'nix-syntax-antiquote
  148. nil limit)))
  149. (when (and pos (not (> pos limit)))
  150. (goto-char pos)
  151. (let ((char (char-after pos)))
  152. (pcase char
  153. (`?{
  154. (forward-char 1)
  155. (set-match-data (list (1- pos) (point)))
  156. t)
  157. (`?}
  158. (forward-char 1)
  159. (set-match-data (list pos (point)))
  160. t))))))))
  161. (defun nix--mark-string (pos string-type)
  162. "Mark string as a Nix string.
  163. POS position of start of string
  164. STRING-TYPE type of string based off of Emacs syntax table types"
  165. (put-text-property pos (1+ pos)
  166. 'syntax-table (string-to-syntax "|"))
  167. (put-text-property pos (1+ pos)
  168. 'nix-string-type string-type))
  169. (defun nix--get-parse-state (pos)
  170. "Get the result of `syntax-ppss' at POS."
  171. (save-excursion (save-match-data (syntax-ppss pos))))
  172. (defun nix--get-string-type (parse-state)
  173. "Get the type of string based on PARSE-STATE."
  174. (let ((string-start (nth 8 parse-state)))
  175. (and string-start (get-text-property string-start 'nix-string-type))))
  176. (defun nix--open-brace-string-type (parse-state)
  177. "Determine if this is an open brace string type based on PARSE-STATE."
  178. (let ((open-brace (nth 1 parse-state)))
  179. (and open-brace (get-text-property open-brace 'nix-string-type))))
  180. (defun nix--open-brace-antiquote-p (parse-state)
  181. "Determine if this is an open brace antiquote based on PARSE-STATE."
  182. (let ((open-brace (nth 1 parse-state)))
  183. (and open-brace (get-text-property open-brace 'nix-syntax-antiquote))))
  184. (defun nix--single-quotes ()
  185. "Handle Nix single quotes."
  186. (let* ((start (match-beginning 0))
  187. (end (match-end 0))
  188. (context (nix--get-parse-state start))
  189. (string-type (nix--get-string-type context)))
  190. (unless (or (equal string-type ?\")
  191. (and (equal string-type nil)
  192. (< 1 start)
  193. (string-match-p nix--variable-char
  194. (buffer-substring (1- start) start))))
  195. (when (equal string-type nil)
  196. (nix--mark-string start ?\')
  197. (setq start (+ 2 start)))
  198. (when (equal (mod (- end start) 3) 2)
  199. (let ((str-peek (buffer-substring end (min (point-max) (+ 2 end)))))
  200. (if (member str-peek '("${" "\\n" "\\r" "\\t"))
  201. (goto-char (+ 2 end))
  202. (nix--mark-string (1- end) ?\')))))))
  203. (defun nix--escaped-antiquote-dq-style ()
  204. "Handle Nix escaped antiquote dq style."
  205. (let* ((start (match-beginning 0))
  206. (ps (nix--get-parse-state start))
  207. (string-type (nix--get-string-type ps)))
  208. (when (equal string-type ?\')
  209. (nix--antiquote-open-at (1+ start) ?\'))))
  210. (defun nix--double-quotes ()
  211. "Handle Nix double quotes."
  212. (let* ((pos (match-beginning 0))
  213. (ps (nix--get-parse-state pos))
  214. (string-type (nix--get-string-type ps)))
  215. (unless (equal string-type ?\')
  216. (nix--mark-string pos ?\"))))
  217. (defun nix--antiquote-open-at (pos string-type)
  218. "Handle Nix antiquote open at based on POS and STRING-TYPE."
  219. (put-text-property pos (1+ pos)
  220. 'syntax-table (string-to-syntax "|"))
  221. (put-text-property pos (+ 2 pos)
  222. 'nix-string-type string-type)
  223. (put-text-property (1+ pos) (+ 2 pos)
  224. 'nix-syntax-antiquote t))
  225. (defun nix--antiquote-open ()
  226. "Handle Nix antiquote open."
  227. (let* ((start (match-beginning 0))
  228. (ps (nix--get-parse-state start))
  229. (string-type (nix--get-string-type ps)))
  230. (when string-type
  231. (nix--antiquote-open-at start string-type))))
  232. (defun nix--antiquote-close-open ()
  233. "Handle Nix antiquote close then open."
  234. (let* ((start (match-beginning 0))
  235. (ps (nix--get-parse-state start))
  236. (string-type (nix--get-string-type ps)))
  237. (if string-type
  238. (nix--antiquote-open-at (1+ start) string-type)
  239. (when (nix--open-brace-antiquote-p ps)
  240. (let ((string-type (nix--open-brace-string-type ps)))
  241. (put-text-property start (+ 3 start)
  242. 'nix-string-type string-type)
  243. (put-text-property start (1+ start)
  244. 'nix-syntax-antiquote t)
  245. (put-text-property (+ 2 start) (+ 3 start)
  246. 'nix-syntax-antiquote t))))))
  247. (defun nix--antiquote-close ()
  248. "Handle Nix antiquote close."
  249. (let* ((start (match-beginning 0))
  250. (ps (nix--get-parse-state start)))
  251. (unless (nix--get-string-type ps)
  252. (let ((string-type (nix--open-brace-string-type ps)))
  253. (when string-type
  254. (put-text-property start (1+ start)
  255. 'nix-string-type string-type)
  256. (put-text-property start (1+ start)
  257. 'nix-syntax-antiquote t)
  258. (let ((ahead (buffer-substring (1+ start)
  259. (min (point-max) (+ 5 start)))))
  260. (pcase string-type
  261. (`?\" (cond
  262. ((or (string-match "^\\\\\"" ahead)
  263. (string-match "^\\\\\\${" ahead))
  264. (nix--mark-string (1+ start) string-type)
  265. (goto-char (+ start (match-end 0) 1)))
  266. ((string-match-p "^\"" ahead)
  267. (goto-char (+ 2 start)))
  268. ((< (1+ start) (point-max))
  269. (nix--mark-string (1+ start) string-type)
  270. (goto-char (+ 2 start)))))
  271. (`?\' (cond
  272. ((or (string-match "^'''" ahead)
  273. (string-match "^''\\${" ahead)
  274. (string-match "^''\\\\[nrt]" ahead))
  275. (nix--mark-string (1+ start) string-type)
  276. (goto-char (+ start (match-end 0) 1)))
  277. ((string-match-p "^''" ahead)
  278. (goto-char (+ 3 start)))
  279. ((< (1+ start) (point-max))
  280. (nix--mark-string (1+ start) string-type)
  281. (goto-char (+ 2 start))))))))))))
  282. (defun nix-syntax-propertize (start end)
  283. "Special syntax properties for Nix from START to END."
  284. (goto-char start)
  285. (remove-text-properties start end
  286. '(nix-string-type nil nix-syntax-antiquote nil))
  287. (funcall
  288. (syntax-propertize-rules
  289. ("\\\\\\\\"
  290. (0 nil))
  291. ("\\\\\""
  292. (0 nil))
  293. ("\\\\\\${"
  294. (0 (ignore (nix--escaped-antiquote-dq-style))))
  295. ("'\\{2,\\}"
  296. (0 (ignore (nix--single-quotes))))
  297. ("}\\${"
  298. (0 (ignore (nix--antiquote-close-open))))
  299. ("\\${"
  300. (0 (ignore (nix--antiquote-open))))
  301. ("}"
  302. (0 (ignore (nix--antiquote-close))))
  303. ("\""
  304. (0 (ignore (nix--double-quotes)))))
  305. start end))
  306. ;; Indentation using SMIE
  307. (defconst nix-smie-grammar
  308. (smie-prec2->grammar
  309. (smie-merge-prec2s
  310. (smie-bnf->prec2
  311. '((id)
  312. (expr (arg ":" expr)
  313. ("if" expr "then" expr "else" expr)
  314. ("let" decls "in" expr)
  315. ("with" expr "nonsep-;" expr)
  316. ("assert" expr "nonsep-;" expr)
  317. (attrset)
  318. (id))
  319. (attrset ("{" decls "}"))
  320. (decls (decls ";" decls)
  321. (id "=" expr))
  322. (arg (id) ("{" args "}"))
  323. (args (args "," args) (id "arg-?" expr)))
  324. '((assoc ";"))
  325. '((assoc ","))
  326. ;; resolve "(with foo; a) <op> b" vs "with foo; (a <op> b)"
  327. ;; in favor of the latter.
  328. '((nonassoc "nonsep-;") (nonassoc " -dummy- "))
  329. ;; resolve "(if ... then ... else a) <op> b"
  330. ;; vs "if ... then ... else (a <op> b)" in favor of the latter.
  331. '((nonassoc "in") (nonassoc " -dummy- ")))
  332. (smie-precs->prec2
  333. '((nonassoc " -dummy- ")
  334. (nonassoc "=")
  335. ;; " -bexpskip- " and " -fexpskip- " are handy tokens for skipping over
  336. ;; whole expressions.
  337. ;; For instance, suppose we have a line looking like this:
  338. ;; "{ foo.bar // { x = y }"
  339. ;; and point is at the end of the line. We can skip the whole
  340. ;; expression (i.e. so the point is just before "foo") using
  341. ;; `(smie-backward-sexp " -bexpskip- ")'. `(backward-sexp)' would
  342. ;; skip over "{ x = y }", not over the whole expression.
  343. (right " -bexpskip- ")
  344. (left " -fexpskip- ")
  345. (nonassoc "else")
  346. (right ":")
  347. (right "->")
  348. (assoc "||")
  349. (assoc "&&")
  350. (nonassoc "==" "!=")
  351. (nonassoc "<" "<=" ">" ">=")
  352. (left "//")
  353. (nonassoc "!")
  354. (assoc "-" "+")
  355. (assoc "*" "/")
  356. (assoc "++")
  357. (left "?")
  358. ;; Tokens for skipping sequences of sexps
  359. ;; (i.e. identifiers or balanced parens).
  360. ;; For instance, suppose we have a line looking like this:
  361. ;; "{ foo.bar // f x "
  362. ;; and point is at the end of the line. We can skip the "f x"
  363. ;; part by doing `(smie-backward-sexp " -bseqskip- ")'.
  364. (right " -bseqskip- ")
  365. (left " -fseqskip- "))))))
  366. (defconst nix-smie--symbol-chars ":->|&=!</-+*?,;!")
  367. (defconst nix-smie--infix-symbols-re
  368. (regexp-opt '(":" "->" "||" "&&" "==" "!=" "<" "<=" ">" ">="
  369. "//" "-" "+" "*" "/" "++" "?")))
  370. (defconst nix-smie-indent-tokens-re
  371. (regexp-opt '("{" "(" "[" "=" "let" "if" "then" "else")))
  372. ;; The core indentation algorithm is very simple:
  373. ;; - If the last token on the previous line matches `nix-smie-indent-tokens-re',
  374. ;; then the current line is indented by `tab-width' relative to the
  375. ;; previous line's 'anchor'.
  376. ;; - Otherwise, let SMIE handle it.
  377. ;; The 'anchor' of a line is defined as follows:
  378. ;; - If the line contains an assignment, it is the beginning of the
  379. ;; left-hand side of the first assignment on that line.
  380. ;; - Otherwise, it is the position of the first token on that line.
  381. (defun nix-smie-rules (kind token)
  382. "Core smie rules."
  383. (pcase (cons kind token)
  384. (`(:after . ,(guard (string-match-p nix-smie-indent-tokens-re
  385. token)))
  386. (nix-smie--indent-anchor))
  387. (`(,_ . "in")
  388. (let ((bol (line-beginning-position)))
  389. (forward-word)
  390. ;; Go back to the corresponding "let".
  391. (smie-backward-sexp t)
  392. (pcase kind
  393. (:before
  394. (if (smie-rule-hanging-p)
  395. (nix-smie--indent-anchor 0)
  396. `(column . ,(current-column))))
  397. (:after
  398. (cond
  399. ((bolp) '(column . 0))
  400. ((<= bol (point))
  401. `(column . ,(current-column))))))))
  402. (`(:after . "nonsep-;")
  403. (forward-char)
  404. (backward-sexp)
  405. (if (smie-rule-bolp)
  406. `(column . ,(current-column))
  407. (nix-smie--indent-anchor)))
  408. (`(:after . ":")
  409. (or (nix-smie--indent-args-line)
  410. (nix-smie--indent-anchor)))
  411. (`(:after . ",")
  412. (smie-rule-parent tab-width))
  413. (`(:before . ",")
  414. ;; The parent is either the enclosing "{" or some previous ",".
  415. ;; In both cases this is what we want to align to.
  416. (smie-rule-parent))
  417. (`(:before . "if")
  418. (let ((bol (line-beginning-position)))
  419. (save-excursion
  420. (and
  421. (equal (nix-smie--backward-token) "else")
  422. (<= bol (point))
  423. `(column . ,(current-column))))))
  424. (`(:before . ,(guard (string-match-p nix-smie--infix-symbols-re token)))
  425. (forward-comment (- (point)))
  426. (let ((bol (line-beginning-position)))
  427. (smie-backward-sexp token)
  428. (if (< (point) bol)
  429. (nix-smie--indent-anchor 0))))))
  430. (defun nix-smie--anchor ()
  431. "Return the anchor's offset from the beginning of the current line."
  432. (save-excursion
  433. (beginning-of-line)
  434. (let ((eol (line-end-position))
  435. anchor
  436. tok)
  437. (forward-comment (point-max))
  438. (unless (or (eobp) (< eol (point)))
  439. (setq anchor (current-column))
  440. (catch 'break
  441. (while (and (not (eobp))
  442. (progn
  443. (setq tok (car (smie-indent-forward-token)))
  444. (<= (point) eol)))
  445. (when (equal "=" tok)
  446. (backward-char)
  447. (smie-backward-sexp " -bseqskip- ")
  448. (setq anchor (current-column))
  449. (throw 'break nil))))
  450. anchor))))
  451. (defun nix-smie--indent-anchor (&optional indent)
  452. "Intended for use only in the rules function."
  453. (let ((indent (or indent tab-width)))
  454. `(column . ,(+ indent (nix-smie--anchor)))))
  455. (defun nix-smie--indent-args-line ()
  456. "Indent the body of a lambda whose argument(s) are on a line of their own."
  457. (save-excursion
  458. ;; Assume that point is right before ':', skip it
  459. (forward-char)
  460. (let ((tok ":"))
  461. (catch 'break
  462. (while (equal tok ":")
  463. (setq tok (nth 2 (smie-backward-sexp t)))
  464. (when (smie-rule-bolp)
  465. (throw 'break `(column . ,(current-column)))))))))
  466. (defconst nix-smie--path-chars "a-zA-Z0-9-+_.:/~")
  467. (defun nix-smie--skip-angle-path-forward ()
  468. "Skip forward a path enclosed in angle brackets, e.g <nixpkgs>"
  469. (let ((start (point)))
  470. (when (eq (char-after) ?<)
  471. (forward-char)
  472. (if (and (nix-smie--skip-path 'forward t)
  473. (eq (char-after) ?>))
  474. (progn
  475. (forward-char)
  476. (buffer-substring-no-properties start (point)))
  477. (ignore (goto-char start))))))
  478. (defun nix-smie--skip-angle-path-backward ()
  479. "Skip backward a path enclosed in angle brackets, e.g <nixpkgs>"
  480. (let ((start (point)))
  481. (when (eq (char-before) ?>)
  482. (backward-char)
  483. (if (and (nix-smie--skip-path 'backward t)
  484. (eq (char-before) ?<))
  485. (progn
  486. (backward-char)
  487. (buffer-substring-no-properties start (point)))
  488. (ignore (goto-char start))))))
  489. (defun nix-smie--skip-path (how &optional no-sep-check)
  490. "Skip path related characters."
  491. (let ((start (point)))
  492. (pcase-exhaustive how
  493. ('forward (skip-chars-forward nix-smie--path-chars))
  494. ('backward (skip-chars-backward nix-smie--path-chars)))
  495. (let ((sub (buffer-substring-no-properties start (point))))
  496. (if (or (and no-sep-check
  497. (< 0 (length sub)))
  498. (string-match-p "/" sub))
  499. sub
  500. (ignore (goto-char start))))))
  501. (defun nix-smie--forward-token-1 ()
  502. "Move forward one token."
  503. (forward-comment (point-max))
  504. (or (nix-smie--skip-angle-path-forward)
  505. (nix-smie--skip-path 'forward)
  506. (buffer-substring-no-properties
  507. (point)
  508. (progn
  509. (or (/= 0 (skip-syntax-forward "'w_"))
  510. (/= 0 (skip-chars-forward nix-smie--symbol-chars))
  511. (skip-syntax-forward ".'"))
  512. (point)))))
  513. (defun nix-smie--forward-token ()
  514. "Move forward one token, skipping certain characters."
  515. (let ((sym (nix-smie--forward-token-1)))
  516. (if (member sym '(";" "?"))
  517. ;; The important lexer for indentation's performance is the backward
  518. ;; lexer, so for the forward lexer we delegate to the backward one.
  519. (save-excursion (nix-smie--backward-token))
  520. sym)))
  521. (defun nix-smie--backward-token-1 ()
  522. "Move backward one token."
  523. (forward-comment (- (point)))
  524. (or (nix-smie--skip-angle-path-backward)
  525. (nix-smie--skip-path 'backward)
  526. (buffer-substring-no-properties
  527. (point)
  528. (progn
  529. (or (/= 0 (skip-syntax-backward "'w_"))
  530. (/= 0 (skip-chars-backward nix-smie--symbol-chars))
  531. (skip-syntax-backward ".'"))
  532. (point)))))
  533. (defun nix-smie--backward-token ()
  534. "Move backward one token, skipping certain characters."
  535. (let ((sym (nix-smie--backward-token-1)))
  536. (unless (zerop (length sym))
  537. (pcase sym
  538. (";" (if (nix-smie--nonsep-semicolon-p) "nonsep-;" ";"))
  539. ("?" (if (nix-smie--arg-?-p) "arg-?" "?"))
  540. (_ sym)))))
  541. (defun nix-smie--nonsep-semicolon-p ()
  542. "Whether the semicolon at point terminates a `with' or `assert'."
  543. (save-excursion
  544. (member (nth 2 (smie-backward-sexp " -bexpskip- ")) '("with" "assert"))))
  545. (defun nix-smie--arg-?-p ()
  546. "Whether the question mark at point is part of an argument declaration."
  547. (member
  548. (nth 2 (progn
  549. (smie-backward-sexp)
  550. (smie-backward-sexp)))
  551. '("{" ",")))
  552. (defun nix-smie--eol-p ()
  553. "Whether there are no tokens after point on the current line."
  554. (let ((eol (line-end-position)))
  555. (save-excursion
  556. (forward-comment (point-max))
  557. (or (eobp)
  558. (< eol (point))))))
  559. (defun nix-smie--indent-close ()
  560. "Align close paren with opening paren."
  561. (save-excursion
  562. (when (looking-at "\\s)")
  563. (forward-char 1)
  564. (condition-case nil
  565. (progn
  566. (backward-sexp 1)
  567. ;; If the opening paren is not the last token on its line,
  568. ;; and it's either '[' or '{', align to the opening paren's
  569. ;; position. Otherwise, align its line's anchor.
  570. (if (and (memq (char-after) '(?\[ ?{))
  571. (not (save-excursion (forward-char) (nix-smie--eol-p))))
  572. (current-column)
  573. (nix-smie--anchor)))
  574. (scan-error nil)))))
  575. (defun nix-smie--indent-exps ()
  576. "This function replaces and is based on `smie-indent-exps'.
  577. An argument to a function is indented relative to the function,
  578. not to any other arguments."
  579. (save-excursion
  580. (let (parent ;; token enclosing the expression list
  581. skipped) ;; whether we skipped at least one expression
  582. (let ((start (point)))
  583. (setq parent (nth 2 (smie-backward-sexp " -bseqskip- ")))
  584. (setq skipped (not (eq start (point))))
  585. (cond
  586. ((not skipped)
  587. ;; We're the first expression of the list. In that case, the
  588. ;; indentation should be (have been) determined by its context.
  589. nil)
  590. ((equal parent "[")
  591. ;; It's a list, align with the first expression.
  592. (current-column))
  593. ;; We're an argument.
  594. (t
  595. ;; We can use (current-column) or (current-indentation) here.
  596. ;; (current-column) will indent relative to the first expression
  597. ;; in the sequence, and (current-indentation) will indent relative
  598. ;; to the indentation of the line on which the first expression
  599. ;; begins. I'm not sure which one is better.
  600. (+ tab-width (current-indentation))))))))
  601. ;;; Indentation not using SMIE
  602. (defun nix-find-backward-matching-token ()
  603. "Find the previous Nix token."
  604. (cond
  605. ((looking-at "in\\b")
  606. (let ((counter 1))
  607. (while (and (> counter 0)
  608. (re-search-backward "\\b\\(let\\|in\\)\\b" nil t))
  609. (unless (or (nix--get-string-type (nix--get-parse-state (point)))
  610. (nix-is-comment-p))
  611. (setq counter (cond ((looking-at "let") (- counter 1))
  612. ((looking-at "in") (+ counter 1))))))
  613. counter ))
  614. ((looking-at "}")
  615. (backward-up-list) t)
  616. ((looking-at "]")
  617. (backward-up-list) t)
  618. ((looking-at ")")
  619. (backward-up-list) t)))
  620. (defun nix-indent-to-backward-match ()
  621. "Match the previous line’s indentation."
  622. (let ((matching-indentation (save-excursion
  623. (beginning-of-line)
  624. (skip-chars-forward "[:space:]")
  625. (if (nix-find-backward-matching-token)
  626. (current-indentation)))))
  627. (when matching-indentation (indent-line-to matching-indentation) t)))
  628. (defun nix-indent-first-line-in-block ()
  629. "Indent the first line in a block."
  630. (let ((matching-indentation (save-excursion
  631. ;; Go back to previous line that contain anything useful to check the
  632. ;; contents of that line.
  633. (beginning-of-line)
  634. (skip-chars-backward "\n[:space:]")
  635. ;; Grab the full string of the line before the one we're indenting
  636. (let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
  637. ;; Then regex-match strings at the end of the line to detect if we need to indent the line after.
  638. ;; We could probably add more things to look for here in the future.
  639. (if (or (string-match "let$" line)
  640. (string-match "import$" line)
  641. (string-match "\\[$" line)
  642. (string-match "=$" line)
  643. (string-match "\($" line)
  644. (string-match "\{$" line))
  645. ;; If it matches any of the regexes above, grab the indent level
  646. ;; of the line and add 2 to ident the line below this one.
  647. (+ 2 (current-indentation)))))))
  648. (when matching-indentation (indent-line-to matching-indentation) t)))
  649. (defun nix-mode-search-backward ()
  650. "Search backward for items of interest regarding indentation."
  651. (re-search-backward nix-re-ends nil t)
  652. (re-search-backward nix-re-quotes nil t)
  653. (re-search-backward nix-re-caps nil t))
  654. (defun nix-indent-expression-start ()
  655. "Indent the start of a nix expression."
  656. (let* ((ends 0)
  657. (once nil)
  658. (done nil)
  659. (indent (current-indentation)))
  660. (save-excursion
  661. ;; we want to indent this line, so we don't care what it
  662. ;; contains skip to the beginning so reverse searching doesn't
  663. ;; find any matches within
  664. (beginning-of-line)
  665. ;; search backward until an unbalanced cap is found or no cap or
  666. ;; end is found
  667. (while (and (not done) (nix-mode-search-backward))
  668. (cond
  669. ((looking-at nix-re-quotes)
  670. ;; skip over strings entirely
  671. (re-search-backward nix-re-quotes nil t))
  672. ((looking-at nix-re-comments)
  673. ;; skip over comments entirely
  674. (re-search-backward nix-re-comments nil t))
  675. ((looking-at nix-re-ends)
  676. ;; count the matched end
  677. ;; this means we expect to find at least one more cap
  678. (setq ends (+ ends 1)))
  679. ((looking-at nix-re-caps)
  680. ;; we found at least one cap
  681. ;; this means our function will return true
  682. ;; this signals to the caller we handled the indentation
  683. (setq once t)
  684. (if (> ends 0)
  685. ;; this cap corresponds to a previously matched end
  686. ;; reduce the number of unbalanced ends
  687. (setq ends (- ends 1))
  688. ;; no unbalanced ends correspond to this cap
  689. ;; this means we have found the expression that contains our line
  690. ;; we want to indent relative to this line
  691. (setq indent (current-indentation))
  692. ;; signal that the search loop should exit
  693. (setq done t))))))
  694. ;; done is t when we found an unbalanced expression cap
  695. (when done
  696. ;; indent relative to the indentation of the expression
  697. ;; containing our line
  698. (indent-line-to (+ tab-width indent)))
  699. ;; return t to the caller if we found at least one cap
  700. ;; this signals that we handled the indentation
  701. once))
  702. (defun nix-indent-prev-level ()
  703. "Get the indent level of the previous line."
  704. (save-excursion
  705. (beginning-of-line)
  706. (skip-chars-backward "\n[:space:]")
  707. (current-indentation)))
  708. ;;;###autoload
  709. (defun nix-mode-format ()
  710. "Format the entire `nix-mode' buffer."
  711. (interactive)
  712. (when (eq major-mode 'nix-mode)
  713. (save-excursion
  714. (goto-char (point-min))
  715. (while (not (equal (point) (point-max)))
  716. (if (equal (string-match-p "^[\s-]*$" (thing-at-point 'line)) 0)
  717. (delete-horizontal-space)
  718. (nix-indent-line))
  719. (forward-line)))))
  720. ;;;###autoload
  721. (defun nix-indent-line ()
  722. "Indent current line in a Nix expression."
  723. (interactive)
  724. (let ((end-of-indentation
  725. (save-excursion
  726. (cond
  727. ;; Indent first line of file to 0
  728. ((= (line-number-at-pos) 1)
  729. (indent-line-to 0))
  730. ;; comment
  731. ((save-excursion
  732. (beginning-of-line)
  733. (nix-is-comment-p))
  734. (indent-line-to (nix-indent-prev-level)))
  735. ;; string
  736. ((save-excursion
  737. (beginning-of-line)
  738. (nth 3 (syntax-ppss)))
  739. (indent-line-to (+ (nix-indent-prev-level)
  740. (* tab-width
  741. (+ (if (save-excursion
  742. (forward-line -1)
  743. (end-of-line)
  744. (skip-chars-backward "[:space:]")
  745. (looking-back "''" 0)) 1 0)
  746. (if (save-excursion
  747. (beginning-of-line)
  748. (skip-chars-forward
  749. "[:space:]")
  750. (looking-at "''")
  751. ) -1 0)
  752. )))))
  753. ;; dedent '}', ']', ')' 'in'
  754. ((nix-indent-to-backward-match))
  755. ;; indent line after 'let', 'import', '[', '=', '(', '{'
  756. ((nix-indent-first-line-in-block))
  757. ;; indent between = and ; + 2, or to 2
  758. ((nix-indent-expression-start))
  759. ;; else
  760. (t
  761. (indent-line-to (nix-indent-prev-level))))
  762. (point))))
  763. (when (> end-of-indentation (point)) (goto-char end-of-indentation))))
  764. (defun nix-is-comment-p ()
  765. "Whether we are in a comment."
  766. (nth 4 (syntax-ppss)))
  767. (defun nix-is-string-p ()
  768. "Whether we are in a string."
  769. (or (looking-at nix-re-quotes)
  770. (nix--get-string-type (nix--get-parse-state (point)))))
  771. ;;;###autoload
  772. (defun nix-indent-region (start end)
  773. "Indent on a whole region. Enabled by default.
  774. START where to start in region.
  775. END where to end the region."
  776. (interactive (list (region-beginning) (region-end)))
  777. (save-excursion
  778. (goto-char start)
  779. (while (< (point) end)
  780. (or (and (bolp) (eolp))
  781. (when (and
  782. ;; Skip if previous line is empty or a comment.
  783. (save-excursion
  784. (let ((line-is-comment-p (nix-is-comment-p)))
  785. (forward-line -1)
  786. (not
  787. (or (and (nix-is-comment-p)
  788. ;; Unless this line is a comment too.
  789. (not line-is-comment-p))
  790. (nix-is-comment-p)))))
  791. ;; Don't mess with strings.
  792. (nix-is-string-p))
  793. (funcall nix-indent-function)))
  794. (forward-line 1))))
  795. ;;;###autoload
  796. (defun nix-mode-ffap-nixpkgs-path (str)
  797. "Support `ffap' for <nixpkgs> declarations.
  798. If STR contains brackets, call `nix-instantiate' to find the
  799. location of STR. If `nix-instantiate' has a nonzero exit code,
  800. dont do anything"
  801. (when (and (string-match nix-re-bracket-path str)
  802. (executable-find nix-instantiate-executable))
  803. (with-temp-buffer
  804. (when (eq (call-process nix-instantiate-executable nil (current-buffer)
  805. nil "--eval" "-E" str) 0)
  806. ;; Remove trailing newline
  807. (substring (buffer-string) 0 (- (buffer-size) 1))))))
  808. ;; Key maps
  809. (defvar nix-mode-menu (make-sparse-keymap "Nix")
  810. "Menu for Nix mode.")
  811. (defvar nix-mode-map (make-sparse-keymap)
  812. "Local keymap used for Nix mode.")
  813. (defun nix-create-keymap ()
  814. "Create the keymap associated with the Nix mode."
  815. )
  816. (defun nix-create-menu ()
  817. "Create the Nix menu as shown in the menu bar."
  818. (let ((m '("Nix"
  819. ["Format buffer" nix-format-buffer t])))
  820. (easy-menu-define nix-mode-menu nix-mode-map "Menu keymap for Nix mode" m)))
  821. (nix-create-keymap)
  822. (nix-create-menu)
  823. ;;;###autoload
  824. (define-derived-mode nix-mode prog-mode "Nix"
  825. "Major mode for editing Nix expressions.
  826. The following commands may be useful:
  827. '\\[newline-and-indent]'
  828. Insert a newline and move the cursor to align with the previous
  829. non-empty line.
  830. '\\[fill-paragraph]'
  831. Refill a paragraph so that all lines are at most `fill-column'
  832. lines long. This should do the right thing for comments beginning
  833. with `#'. However, this command doesn't work properly yet if the
  834. comment is adjacent to code (i.e., no intervening empty lines).
  835. In that case, select the text to be refilled and use
  836. `\\[fill-region]' instead.
  837. The hook `nix-mode-hook' is run when Nix mode is started.
  838. \\{nix-mode-map}
  839. "
  840. :group 'nix-mode
  841. :syntax-table nix-mode-syntax-table
  842. :abbrev-table nix-mode-abbrev-table
  843. ;; Disable hard tabs and set tab to 2 spaces
  844. ;; Recommended by nixpkgs manual: https://nixos.org/nixpkgs/manual/#sec-syntax
  845. (setq-local indent-tabs-mode nil)
  846. (setq-local tab-width 2)
  847. (setq-local electric-indent-chars '(?\n ?{ ?} ?\[ ?\] ?\( ?\)))
  848. ;; Font lock support.
  849. (setq-local font-lock-defaults '(nix-font-lock-keywords))
  850. ;; Special syntax properties for Nix
  851. (setq-local syntax-propertize-function 'nix-syntax-propertize)
  852. ;; Look at text properties when parsing
  853. (setq-local parse-sexp-lookup-properties t)
  854. ;; Setup SMIE integration
  855. (when nix-mode-use-smie
  856. (smie-setup nix-smie-grammar 'nix-smie-rules
  857. :forward-token 'nix-smie--forward-token
  858. :backward-token 'nix-smie--backward-token)
  859. (setq-local smie-indent-basic 2)
  860. (fset (make-local-variable 'smie-indent-exps)
  861. (symbol-function 'nix-smie--indent-exps))
  862. (fset (make-local-variable 'smie-indent-close)
  863. (symbol-function 'nix-smie--indent-close)))
  864. ;; Automatic indentation [C-j]
  865. (setq-local indent-line-function (lambda ()
  866. (if (and (not nix-mode-use-smie)
  867. (eq nix-indent-function 'smie-indent-line))
  868. (indent-relative)
  869. (funcall nix-indent-function))))
  870. ;; Indenting of comments
  871. (setq-local comment-start "# ")
  872. (setq-local comment-end "")
  873. (setq-local comment-start-skip "\\(^\\|\\s-\\);?#+ *")
  874. (setq-local comment-multi-line t)
  875. ;; Filling of comments
  876. (setq-local adaptive-fill-mode t)
  877. (setq-local paragraph-start "[ \t]*\\(#+[ \t]*\\)?$")
  878. (setq-local paragraph-separate paragraph-start)
  879. ;; Menu
  880. (easy-menu-add nix-mode-menu nix-mode-map)
  881. ;; Find file at point
  882. (push '(nix-mode . nix-mode-ffap-nixpkgs-path) ffap-alist)
  883. (push '(nix-mode "--:\\\\${}<>+@-Z_[:alpha:]~*?" "@" "@;.,!:")
  884. ffap-string-at-point-mode-alist))
  885. ;;;###autoload
  886. (add-to-list 'auto-mode-alist '("\\.nix\\'" . nix-mode))
  887. (provide 'nix-mode)
  888. ;;; nix-mode.el ends here