[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/nix-mode d5a53e2050 300/500: Indent using SMIE - initial w
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/nix-mode d5a53e2050 300/500: Indent using SMIE - initial working version. |
Date: |
Sat, 29 Jan 2022 08:27:15 -0500 (EST) |
branch: elpa/nix-mode
commit d5a53e2050520d1184aaff67f5061e5ae7e5ad77
Author: Jakub Piecuch <j.piecuch96@gmail.com>
Commit: Jakub Piecuch <j.piecuch96@gmail.com>
Indent using SMIE - initial working version.
---
nix-mode.el | 445 +++++++++++++++++++++++++++++++++++++-----------------------
1 file changed, 276 insertions(+), 169 deletions(-)
diff --git a/nix-mode.el b/nix-mode.el
index 1f36ba105e..c03aef08f5 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,7 @@
Valid functions for this are:
- ‘indent-relative’
-- nix-indent-line (buggy)"
+- smie-indent-line (buggy)"
:group 'nix-mode
:type 'function)
@@ -142,6 +143,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,122 +339,262 @@ STRING-TYPE type of string based off of Emacs syntax
table types"
(0 (ignore (nix--double-quotes)))))
start end))
-;;; Indentation
-
-(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)))
+;; 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--symbols-re
+ (regexp-opt '(":" "->" "||" "&&" "==" "!=" "<" "<=" ">" ">="
+ "//" "-" "+" "*" "/" "++" "?" "=" "," ";" "!")))
+
+(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- ")
+ (if (smie-rule-bolp)
+ `(column . ,(current-column))
+ (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."
+ (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_"))
+ (and (looking-at nix-smie--symbols-re)
+ (goto-char (match-end 0))))
+ (point)))))
+
+(defun nix-smie--forward-token ()
+ (let ((sym (nix-smie--forward-token-1)))
+ (cond
+ ((equal 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)))
+ (t 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_"))
+ (and (looking-back nix-smie--symbols-re (- (point) 2) t)
+ (goto-char (match-beginning 0))))
+ (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
- ;; 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."
+ ;; 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."
+ (memq
+ (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
- (beginning-of-line)
- (skip-chars-backward "\n[:space:]")
- (current-indentation)))
+ (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))))))))
;;;###autoload
(defun nix-mode-format ()
@@ -458,60 +606,9 @@ STRING-TYPE type of string based off of Emacs syntax table
types"
(while (not (equal (point) (point-max)))
(if (equal (string-match-p "^[\s-]*$" (thing-at-point 'line)) 0)
(delete-horizontal-space)
- (nix-indent-line))
+ (smie-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)))
@@ -542,7 +639,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 +719,17 @@ The hook `nix-mode-hook' is run when Nix mode is started.
(setq-local parse-sexp-lookup-properties t)
;; Automatic indentation [C-j]
- (setq-local indent-line-function nix-indent-function)
+ (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 'smie-indent-line)
+ (ignore-errors
+ (setf (car (memq 'smie-indent-exps smie-indent-functions))
+ 'nix-smie--indent-exps)
+ (setf (car (memq 'smie-indent-close smie-indent-functions))
+ 'nix-smie--indent-close))
+
;; Indenting of comments
(setq-local comment-start "# ")
- [nongnu] elpa/nix-mode e645310c88 458/500: Introduce `nix-store-show-path` command, (continued)
- [nongnu] elpa/nix-mode e645310c88 458/500: Introduce `nix-store-show-path` command, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode ef92acd96c 453/500: nix-format.el: Use replace-buffer-contents when available, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode ed00d8dff2 463/500: nix.el: Fix for Nix 2.5, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 327175e768 460/500: Merge pull request #138 from phikal/master, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode b1257d3ea6 461/500: Merge branch 'master' into store-path, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode ae1f5253be 478/500: nix-flake: Add documentation to README, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 9d8dc3c8d8 495/500: Merge pull request #143 from akirak/fix-flake-switching, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode e7bf2e4cc4 489/500: Merge pull request #140 from akirak/flake-transient, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 1f922d78eb 291/500: Fix regex regression that caused the indent to indent too much, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 9ebf7389eb 298/500: Merge branch 'etu-fix-issue-72', ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode d5a53e2050 300/500: Indent using SMIE - initial working version.,
ELPA Syncer <=
- [nongnu] elpa/nix-mode 8118355e81 306/500: Change smie indent functions in a cleaner (and correct) way., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 9d1d025cb7 313/500: Use "check" as test target, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 8812eec39a 314/500: Allow using nix-indent-region without arguments, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 29a93838bb 316/500: Add hello.nix to tests, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 0cb2b32485 323/500: Add a failing test that checks the indentation of function bodies., ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 79507ee193 327/500: Add more test cases to smie-lambdas.nix, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 37f641a913 434/500: Add menu to nix search mode, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode f38d4e9b37 459/500: Remove f dependency, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 3cca5b6527 452/500: Merge pull request #133 from nagy/small-fixes, ELPA Syncer, 2022/01/29
- [nongnu] elpa/nix-mode 89755c1e7e 475/500: nix-flake: Fix the default value, ELPA Syncer, 2022/01/29