emacs-elpa-diffs
[Top][All Lists]
Advanced

[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 "# ")



reply via email to

[Prev in Thread] Current Thread [Next in Thread]