[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/nix-mode 8721f63650 310/500: Merge pull request #79 from j
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/nix-mode 8721f63650 310/500: Merge pull request #79 from j-piecuch/smie |
Date: |
Sat, 29 Jan 2022 08:27:16 -0500 (EST) |
branch: elpa/nix-mode
commit 8721f63650b49d79e26cd1145bfc96c4c3e1638d
Merge: 1e53bed4d4 1cda7dabbc
Author: Matthew Bauer <mjbauer95@gmail.com>
Commit: GitHub <noreply@github.com>
Merge pull request #79 from j-piecuch/smie
Use SMIE for indentation
---
nix-mode.el | 285 ++++++++++++++++++++++++++++++++++++++++-
tests/testcases/issue-60.3.nix | 2 +-
2 files changed, 283 insertions(+), 4 deletions(-)
diff --git a/nix-mode.el b/nix-mode.el
index 1f36ba105e..05f1d75503 100644
--- a/nix-mode.el
+++ b/nix-mode.el
@@ -20,6 +20,7 @@
(require 'nix-shebang)
(require 'nix-shell)
(require 'nix-repl)
+(require 'smie)
(require 'ffap)
(eval-when-compile (require 'subr-x))
@@ -33,7 +34,8 @@
Valid functions for this are:
- ‘indent-relative’
-- nix-indent-line (buggy)"
+- nix-indent-line (buggy)
+- smie-indent-line"
:group 'nix-mode
:type 'function)
@@ -142,6 +144,13 @@ Valid functions for this are:
(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
@@ -331,7 +340,269 @@ STRING-TYPE type of string based off of Emacs syntax
table types"
(0 (ignore (nix--double-quotes)))))
start end))
-;;; Indentation
+;; 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)
+ (pcase (cons kind token)
+ (`(:after . ,(guard (string-match-p nix-smie-indent-tokens-re
+ token)))
+ (nix-smie--indent-anchor))
+ (`(:after . "in")
+ (cond
+ ((bolp) '(column . 0))
+ ((<= (line-beginning-position)
+ (save-excursion
+ (forward-word)
+ (smie-backward-sexp t)
+ (point)))
+ (smie-rule-parent))))
+ (`(:after . "nonsep-;")
+ (forward-char)
+ (backward-sexp)
+ (if (smie-rule-bolp)
+ `(column . ,(current-column))
+ (nix-smie--indent-anchor)))
+ (`(:after . ":")
+ ;; Skip over the argument.
+ (smie-backward-sexp " -bseqskip- ")
+ (cond
+ ((smie-rule-bolp)
+ `(column . ,(current-column)))
+ ((= (current-indentation) 0)
+ '(column . 0))
+ (t
+ (nix-smie--indent-anchor))))
+ (`(:after . ",")
+ (smie-rule-parent tab-width))
+ (`(:before . "in")
+ (forward-word)
+ (smie-backward-sexp t)
+ (nix-smie--indent-anchor 0))
+ (`(: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."
+ (goto-char (+ (line-beginning-position) (current-indentation)))
+ (let ((eol (line-end-position))
+ (anchor (current-column))
+ tok)
+ (catch 'break
+ (while (and (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)))))
+
+(defconst nix-smie--path-chars "a-zA-Z0-9-+_.:/~")
+
+(defun nix-smie--skip-path (how)
+ (let ((start (point)))
+ (pcase how
+ ('forward (skip-chars-forward nix-smie--path-chars))
+ ('backward (skip-chars-backward nix-smie--path-chars))
+ (_ (error "expected 'forward or 'backward")))
+ (let ((sub (buffer-substring-no-properties start (point))))
+ (if (string-match-p "/" sub)
+ sub
+ (ignore (goto-char start))))))
+
+(defun nix-smie--forward-token-1 ()
+ (forward-comment (point-max))
+ (or (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 ()
+ (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 ()
+ (forward-comment (- (point)))
+ (or (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 ()
+ (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'."
+ (let (tok)
+ (save-excursion
+ ;; Skip over identifiers, balanced parens etc. as far back as we can.
+ (while (null (setq tok (nth 2 (smie-backward-sexp " -bexpskip- "))))))
+ (member tok '("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--indent-close ()
+ ;; Align close paren with opening paren.
+ (save-excursion
+ (when (looking-at "\\s)")
+ (forward-char 1)
+ (condition-case nil
+ (progn
+ (backward-sexp 1)
+ ;; Align to the first token on the line containing
+ ;; the opening paren.
+ (current-indentation))
+ (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."
@@ -542,7 +813,7 @@ END where to end the region."
(nix-is-comment-p)))))
;; Don't mess with strings.
(nix-is-string-p))
- (nix-indent-line)))
+ (smie-indent-line)))
(forward-line 1))))
;;;###autoload
@@ -622,7 +893,15 @@ The hook `nix-mode-hook' is run when Nix mode is started.
(setq-local parse-sexp-lookup-properties t)
;; Automatic indentation [C-j]
+ (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)
(setq-local indent-line-function nix-indent-function)
+ (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))
;; Indenting of comments
(setq-local comment-start "# ")
diff --git a/tests/testcases/issue-60.3.nix b/tests/testcases/issue-60.3.nix
index 1cf7350a78..c7c9eeb2e7 100644
--- a/tests/testcases/issue-60.3.nix
+++ b/tests/testcases/issue-60.3.nix
@@ -3,6 +3,6 @@ let
import
(
builtins.fetchTarball
- https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz
+ https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz
);
in mozilla-overlay
- [nongnu] elpa/nix-mode 1dd112e5cf 267/500: Fix function used as variable, (continued)
- [nongnu] elpa/nix-mode 1dd112e5cf 267/500: Fix function used as variable, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode f1973ceb4b 269/500: Add .nix to auto-mode-alist, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 1389a6b25a 275/500: Fix flycheck warnings, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 95ef285e74 277/500: Update .nix script, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 1ade7b76bd 282/500: ert/indent: Add testcase for contents of lists, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 02b59d9bcf 283/500: ert/indent: Add testcases for issues reported in #60, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode bb602e160f 286/500: Cleanups, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 7526a43ea0 292/500: tests: Rename test for consistency with the rest, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode aa47e0c5ae 302/500: Fix indentation of lambdas starting at bol., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode e744c602fd 304/500: Replace regex operations with faster alternatives in the lexer., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 8721f63650 310/500: Merge pull request #79 from j-piecuch/smie,
ELPA Syncer <=
- [nongnu] elpa/nix-mode 5fcdd667de 312/500: Make smie-indent-line the default indent function, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode b902e15c1f 003/500: Create nix-mode.el, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 1e74e5021a 002/500: Create LICENSE, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 616c368898 007/500: Add two spaces after period., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 7292d8971c 021/500: Refactor indenting, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 7f69c71f8d 019/500: Fixup mode map., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 32929d9aed 024/500: Indent relative for string, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 9ab56cd6c9 035/500: Refactor indenting, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 65d0b6471f 039/500: Add more font lock constants., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 5e61391ca1 047/500: Merge branch 'lets', ELPA Syncer, 2022/01/29