diff options
| author | Noam Postavsky | 2017-04-23 10:43:05 -0400 |
|---|---|---|
| committer | Noam Postavsky | 2017-05-09 20:50:19 -0400 |
| commit | e7b6751c0a74f24c14cd207d57a4e1a95f409256 (patch) | |
| tree | 14f60933c9d78aabf42ba79abd911f9ef3e3d373 | |
| parent | 17e540aa428c5392f7a9b4c1f7495bac8a8fe5da (diff) | |
| download | emacs-e7b6751c0a74f24c14cd207d57a4e1a95f409256.tar.gz emacs-e7b6751c0a74f24c14cd207d57a4e1a95f409256.zip | |
Fix lisp-indent-region and indent-sexp (Bug#26619)
The new lisp-indent-region introduced in 2017-04-22 "Add new
`lisp-indent-region' that doesn't reparse the code." is broken because
it doesn't save the calculated indent amounts for already seen sexp
depths. Fix this by unifying the indent-sexp and lisp-indent-region
code. Furthermore, only preserve position 2 of the running parse
when the depth doesn't change.
* lisp/emacs-lisp/lisp-mode.el (lisp-ppss): Use an OLDSTATE that
corresponds with the start point when calling parse-partial-sexp.
(lisp-indent-state): New struct.
(lisp-indent-calc-next): New function, extracted from indent-sexp.
(indent-sexp, lisp-indent-region): Use it.
(lisp-indent-line): Take indentation, instead of parse state.
* test/lisp/emacs-lisp/lisp-mode-tests.el
(lisp-mode-tests--correctly-indented-sexp): New constant.
(lisp-indent-region, lisp-indent-region-defun-with-docstring):
(lisp-indent-region-open-paren, lisp-indent-region-in-sexp): New
tests.
| -rw-r--r-- | lisp/emacs-lisp/lisp-mode.el | 176 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/lisp-mode-tests.el | 85 |
2 files changed, 171 insertions, 90 deletions
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el index 7448864ff99..6287f27b139 100644 --- a/lisp/emacs-lisp/lisp-mode.el +++ b/lisp/emacs-lisp/lisp-mode.el | |||
| @@ -754,49 +754,108 @@ function is `common-lisp-indent-function'." | |||
| 754 | 754 | ||
| 755 | (defun lisp-ppss (&optional pos) | 755 | (defun lisp-ppss (&optional pos) |
| 756 | "Return Parse-Partial-Sexp State at POS, defaulting to point. | 756 | "Return Parse-Partial-Sexp State at POS, defaulting to point. |
| 757 | Like to `syntax-ppss' but includes the character address of the | 757 | Like `syntax-ppss' but includes the character address of the last |
| 758 | last complete sexp in the innermost containing list at position | 758 | complete sexp in the innermost containing list at position |
| 759 | 2 (counting from 0). This is important for lisp indentation." | 759 | 2 (counting from 0). This is important for lisp indentation." |
| 760 | (unless pos (setq pos (point))) | 760 | (unless pos (setq pos (point))) |
| 761 | (let ((pss (syntax-ppss pos))) | 761 | (let ((pss (syntax-ppss pos))) |
| 762 | (if (nth 9 pss) | 762 | (if (nth 9 pss) |
| 763 | (parse-partial-sexp (car (last (nth 9 pss))) pos) | 763 | (let ((sexp-start (car (last (nth 9 pss))))) |
| 764 | (parse-partial-sexp sexp-start pos nil nil (syntax-ppss sexp-start))) | ||
| 764 | pss))) | 765 | pss))) |
| 765 | 766 | ||
| 767 | (cl-defstruct (lisp-indent-state | ||
| 768 | (:constructor nil) | ||
| 769 | (:constructor lisp-indent-initial-state | ||
| 770 | (&aux (ppss (lisp-ppss)) | ||
| 771 | (ppss-point (point)) | ||
| 772 | (depth (car ppss)) | ||
| 773 | (stack (make-list (1+ depth) nil))))) | ||
| 774 | stack ;; Cached indentation, per depth. | ||
| 775 | ppss | ||
| 776 | depth | ||
| 777 | ppss-point) | ||
| 778 | |||
| 779 | (defun lisp-indent-calc-next (state) | ||
| 780 | "Move to next line and return calculated indent for it. | ||
| 781 | STATE is updated by side effect, the first state should be | ||
| 782 | created by `lisp-indent-initial-state'. This function may move | ||
| 783 | by more than one line to cross a string literal." | ||
| 784 | (pcase-let (((cl-struct lisp-indent-state | ||
| 785 | (stack indent-stack) ppss depth ppss-point) | ||
| 786 | state)) | ||
| 787 | ;; Parse this line so we can learn the state to indent the | ||
| 788 | ;; next line. | ||
| 789 | (while (let ((last-sexp (nth 2 ppss))) | ||
| 790 | (setq ppss (parse-partial-sexp | ||
| 791 | ppss-point (progn (end-of-line) (point)) | ||
| 792 | nil nil ppss)) | ||
| 793 | ;; Preserve last sexp of state (position 2) for | ||
| 794 | ;; `calculate-lisp-indent', if we're at the same depth. | ||
| 795 | (if (and (not (nth 2 ppss)) (= depth (car ppss))) | ||
| 796 | (setf (nth 2 ppss) last-sexp) | ||
| 797 | (setq last-sexp (nth 2 ppss))) | ||
| 798 | ;; Skip over newlines within strings. | ||
| 799 | (nth 3 ppss)) | ||
| 800 | (let ((string-start (nth 8 ppss))) | ||
| 801 | (setq ppss (parse-partial-sexp (point) (point-max) | ||
| 802 | nil nil ppss 'syntax-table)) | ||
| 803 | (setf (nth 2 ppss) string-start)) ; Finished a complete string. | ||
| 804 | (setq ppss-point (point))) | ||
| 805 | (setq ppss-point (point)) | ||
| 806 | (let* ((next-depth (car ppss)) | ||
| 807 | (depth-delta (- next-depth depth))) | ||
| 808 | (cond ((< depth-delta 0) | ||
| 809 | (setq indent-stack (nthcdr (- depth-delta) indent-stack))) | ||
| 810 | ((> depth-delta 0) | ||
| 811 | (setq indent-stack (nconc (make-list depth-delta nil) | ||
| 812 | indent-stack)))) | ||
| 813 | (setq depth next-depth)) | ||
| 814 | (prog1 | ||
| 815 | (let (indent) | ||
| 816 | (cond ((= (forward-line 1) 1) nil) | ||
| 817 | ((car indent-stack)) | ||
| 818 | ((integerp (setq indent (calculate-lisp-indent ppss))) | ||
| 819 | (setf (car indent-stack) indent)) | ||
| 820 | ((consp indent) ; (COLUMN CONTAINING-SEXP-START) | ||
| 821 | (car indent)) | ||
| 822 | ;; This only happens if we're in a string. | ||
| 823 | (t (error "This shouldn't happen")))) | ||
| 824 | (setf (lisp-indent-state-stack state) indent-stack) | ||
| 825 | (setf (lisp-indent-state-depth state) depth) | ||
| 826 | (setf (lisp-indent-state-ppss-point state) ppss-point) | ||
| 827 | (setf (lisp-indent-state-ppss state) ppss)))) | ||
| 828 | |||
| 766 | (defun lisp-indent-region (start end) | 829 | (defun lisp-indent-region (start end) |
| 767 | "Indent region as Lisp code, efficiently." | 830 | "Indent region as Lisp code, efficiently." |
| 768 | (save-excursion | 831 | (save-excursion |
| 769 | (setq end (copy-marker end)) | 832 | (setq end (copy-marker end)) |
| 770 | (goto-char start) | 833 | (goto-char start) |
| 834 | (beginning-of-line) | ||
| 771 | ;; The default `indent-region-line-by-line' doesn't hold a running | 835 | ;; The default `indent-region-line-by-line' doesn't hold a running |
| 772 | ;; parse state, which forces each indent call to reparse from the | 836 | ;; parse state, which forces each indent call to reparse from the |
| 773 | ;; beginning. That has O(n^2) complexity. | 837 | ;; beginning. That has O(n^2) complexity. |
| 774 | (let* ((parse-state (lisp-ppss start)) | 838 | (let* ((parse-state (lisp-indent-initial-state)) |
| 775 | (last-syntax-point start) | ||
| 776 | (pr (unless (minibufferp) | 839 | (pr (unless (minibufferp) |
| 777 | (make-progress-reporter "Indenting region..." (point) end)))) | 840 | (make-progress-reporter "Indenting region..." (point) end)))) |
| 778 | (while (< (point) end) | 841 | (let ((ppss (lisp-indent-state-ppss parse-state))) |
| 779 | (unless (and (bolp) (eolp)) | 842 | (unless (or (and (bolp) (eolp)) (nth 3 ppss)) |
| 780 | (lisp-indent-line parse-state)) | 843 | (lisp-indent-line (calculate-lisp-indent ppss)))) |
| 781 | (forward-line 1) | 844 | (let ((indent nil)) |
| 782 | (let ((last-sexp (nth 2 parse-state))) | 845 | (while (progn (setq indent (lisp-indent-calc-next parse-state)) |
| 783 | (setq parse-state (parse-partial-sexp last-syntax-point (point) | 846 | (< (point) end)) |
| 784 | nil nil parse-state)) | 847 | (unless (or (and (bolp) (eolp)) (not indent)) |
| 785 | ;; It's important to preserve last sexp location for | 848 | (lisp-indent-line indent)) |
| 786 | ;; `calculate-lisp-indent'. | 849 | (and pr (progress-reporter-update pr (point))))) |
| 787 | (unless (nth 2 parse-state) | ||
| 788 | (setf (nth 2 parse-state) last-sexp)) | ||
| 789 | (setq last-syntax-point (point))) | ||
| 790 | (and pr (progress-reporter-update pr (point)))) | ||
| 791 | (and pr (progress-reporter-done pr)) | 850 | (and pr (progress-reporter-done pr)) |
| 792 | (move-marker end nil)))) | 851 | (move-marker end nil)))) |
| 793 | 852 | ||
| 794 | (defun lisp-indent-line (&optional parse-state) | 853 | (defun lisp-indent-line (&optional indent) |
| 795 | "Indent current line as Lisp code." | 854 | "Indent current line as Lisp code." |
| 796 | (interactive) | 855 | (interactive) |
| 797 | (let ((pos (- (point-max) (point))) | 856 | (let ((pos (- (point-max) (point))) |
| 798 | (indent (progn (beginning-of-line) | 857 | (indent (progn (beginning-of-line) |
| 799 | (calculate-lisp-indent (or parse-state (lisp-ppss)))))) | 858 | (or indent (calculate-lisp-indent (lisp-ppss)))))) |
| 800 | (skip-chars-forward " \t") | 859 | (skip-chars-forward " \t") |
| 801 | (if (or (null indent) (looking-at "\\s<\\s<\\s<")) | 860 | (if (or (null indent) (looking-at "\\s<\\s<\\s<")) |
| 802 | ;; Don't alter indentation of a ;;; comment line | 861 | ;; Don't alter indentation of a ;;; comment line |
| @@ -1116,16 +1175,7 @@ Lisp function does not specify a special indentation." | |||
| 1116 | If optional arg ENDPOS is given, indent each line, stopping when | 1175 | If optional arg ENDPOS is given, indent each line, stopping when |
| 1117 | ENDPOS is encountered." | 1176 | ENDPOS is encountered." |
| 1118 | (interactive) | 1177 | (interactive) |
| 1119 | (let* ((indent-stack (list nil)) | 1178 | (let* ((parse-state (lisp-indent-initial-state))) |
| 1120 | ;; Use `syntax-ppss' to get initial state so we don't get | ||
| 1121 | ;; confused by starting inside a string. We don't use | ||
| 1122 | ;; `syntax-ppss' in the loop, because this is measurably | ||
| 1123 | ;; slower when we're called on a long list. | ||
| 1124 | (state (syntax-ppss)) | ||
| 1125 | (init-depth (car state)) | ||
| 1126 | (next-depth init-depth) | ||
| 1127 | (last-depth init-depth) | ||
| 1128 | (last-syntax-point (point))) | ||
| 1129 | ;; We need a marker because we modify the buffer | 1179 | ;; We need a marker because we modify the buffer |
| 1130 | ;; text preceding endpos. | 1180 | ;; text preceding endpos. |
| 1131 | (setq endpos (copy-marker | 1181 | (setq endpos (copy-marker |
| @@ -1135,64 +1185,20 @@ ENDPOS is encountered." | |||
| 1135 | (save-excursion (forward-sexp 1) (point))))) | 1185 | (save-excursion (forward-sexp 1) (point))))) |
| 1136 | (save-excursion | 1186 | (save-excursion |
| 1137 | (while (< (point) endpos) | 1187 | (while (< (point) endpos) |
| 1138 | ;; Parse this line so we can learn the state to indent the | 1188 | (let ((indent (lisp-indent-calc-next parse-state))) |
| 1139 | ;; next line. Preserve element 2 of the state (last sexp) for | 1189 | ;; If the line contains a comment indent it now with |
| 1140 | ;; `calculate-lisp-indent'. | 1190 | ;; `indent-for-comment'. |
| 1141 | (let ((last-sexp (nth 2 state))) | 1191 | (when (nth 4 (lisp-indent-state-ppss parse-state)) |
| 1142 | (while (progn | 1192 | (save-excursion |
| 1143 | (setq state (parse-partial-sexp | 1193 | (goto-char (lisp-indent-state-ppss-point parse-state)) |
| 1144 | last-syntax-point (progn (end-of-line) (point)) | 1194 | (indent-for-comment) |
| 1145 | nil nil state)) | 1195 | (setf (lisp-indent-state-ppss-point parse-state) |
| 1146 | (setq last-sexp (or (nth 2 state) last-sexp)) | 1196 | (line-end-position)))) |
| 1147 | ;; Skip over newlines within strings. | ||
| 1148 | (nth 3 state)) | ||
| 1149 | (setq state (parse-partial-sexp (point) (point-max) | ||
| 1150 | nil nil state 'syntax-table)) | ||
| 1151 | (setq last-sexp (or (nth 2 state) last-sexp)) | ||
| 1152 | (setq last-syntax-point (point))) | ||
| 1153 | (setf (nth 2 state) last-sexp)) | ||
| 1154 | (setq next-depth (car state)) | ||
| 1155 | ;; If the line contains a comment indent it now with | ||
| 1156 | ;; `indent-for-comment'. | ||
| 1157 | (when (nth 4 state) | ||
| 1158 | (indent-for-comment) | ||
| 1159 | (end-of-line)) | ||
| 1160 | (setq last-syntax-point (point)) | ||
| 1161 | (when (< next-depth init-depth) | ||
| 1162 | (setq indent-stack (nconc indent-stack | ||
| 1163 | (make-list (- init-depth next-depth) nil)) | ||
| 1164 | last-depth (- last-depth next-depth) | ||
| 1165 | next-depth init-depth)) | ||
| 1166 | ;; Now indent the next line according to what we learned from | ||
| 1167 | ;; parsing the previous one. | ||
| 1168 | (forward-line 1) | ||
| 1169 | (when (< (point) endpos) | ||
| 1170 | (let ((depth-delta (- next-depth last-depth))) | ||
| 1171 | (cond ((< depth-delta 0) | ||
| 1172 | (setq indent-stack (nthcdr (- depth-delta) indent-stack))) | ||
| 1173 | ((> depth-delta 0) | ||
| 1174 | (setq indent-stack (nconc (make-list depth-delta nil) | ||
| 1175 | indent-stack)))) | ||
| 1176 | (setq last-depth next-depth)) | ||
| 1177 | ;; But not if the line is blank, or just a comment (we | 1197 | ;; But not if the line is blank, or just a comment (we |
| 1178 | ;; already called `indent-for-comment' above). | 1198 | ;; already called `indent-for-comment' above). |
| 1179 | (skip-chars-forward " \t") | 1199 | (skip-chars-forward " \t") |
| 1180 | (unless (or (eolp) (eq (char-syntax (char-after)) ?<)) | 1200 | (unless (or (eolp) (eq (char-syntax (char-after)) ?<) (not indent)) |
| 1181 | (indent-line-to | 1201 | (indent-line-to indent))))) |
| 1182 | (or (car indent-stack) | ||
| 1183 | ;; The state here is actually to the end of the | ||
| 1184 | ;; previous line, but that's fine for our purposes. | ||
| 1185 | ;; And parsing over the newline would only destroy | ||
| 1186 | ;; element 2 (last sexp position). | ||
| 1187 | (let ((val (calculate-lisp-indent state))) | ||
| 1188 | (cond ((integerp val) | ||
| 1189 | (setf (car indent-stack) val)) | ||
| 1190 | ((consp val) ; (COLUMN CONTAINING-SEXP-START) | ||
| 1191 | (car val)) | ||
| 1192 | ;; `calculate-lisp-indent' only returns nil | ||
| 1193 | ;; when we're in a string, but this won't | ||
| 1194 | ;; happen because we skip strings above. | ||
| 1195 | (t (error "This shouldn't happen!")))))))))) | ||
| 1196 | (move-marker endpos nil))) | 1202 | (move-marker endpos nil))) |
| 1197 | 1203 | ||
| 1198 | (defun indent-pp-sexp (&optional arg) | 1204 | (defun indent-pp-sexp (&optional arg) |
diff --git a/test/lisp/emacs-lisp/lisp-mode-tests.el b/test/lisp/emacs-lisp/lisp-mode-tests.el index 27f0bb5ec13..1f78eb30105 100644 --- a/test/lisp/emacs-lisp/lisp-mode-tests.el +++ b/test/lisp/emacs-lisp/lisp-mode-tests.el | |||
| @@ -21,10 +21,7 @@ | |||
| 21 | (require 'cl-lib) | 21 | (require 'cl-lib) |
| 22 | (require 'lisp-mode) | 22 | (require 'lisp-mode) |
| 23 | 23 | ||
| 24 | (ert-deftest indent-sexp () | 24 | (defconst lisp-mode-tests--correctly-indented-sexp "\ |
| 25 | "Test basics of \\[indent-sexp]." | ||
| 26 | (with-temp-buffer | ||
| 27 | (insert "\ | ||
| 28 | \(a | 25 | \(a |
| 29 | (prog1 | 26 | (prog1 |
| 30 | (prog1 | 27 | (prog1 |
| @@ -42,9 +39,14 @@ noindent\" 3 | |||
| 42 | 2) ; comment | 39 | 2) ; comment |
| 43 | ;; comment | 40 | ;; comment |
| 44 | b)") | 41 | b)") |
| 42 | |||
| 43 | (ert-deftest indent-sexp () | ||
| 44 | "Test basics of \\[indent-sexp]." | ||
| 45 | (with-temp-buffer | ||
| 46 | (insert lisp-mode-tests--correctly-indented-sexp) | ||
| 45 | (goto-char (point-min)) | 47 | (goto-char (point-min)) |
| 46 | (let ((indent-tabs-mode nil) | 48 | (let ((indent-tabs-mode nil) |
| 47 | (correct (buffer-string))) | 49 | (correct lisp-mode-tests--correctly-indented-sexp)) |
| 48 | (dolist (mode '(fundamental-mode emacs-lisp-mode)) | 50 | (dolist (mode '(fundamental-mode emacs-lisp-mode)) |
| 49 | (funcall mode) | 51 | (funcall mode) |
| 50 | (indent-sexp) | 52 | (indent-sexp) |
| @@ -97,5 +99,78 @@ noindent\" 3 | |||
| 97 | (indent-sexp) | 99 | (indent-sexp) |
| 98 | (should (equal (buffer-string) correct))))) | 100 | (should (equal (buffer-string) correct))))) |
| 99 | 101 | ||
| 102 | (ert-deftest lisp-indent-region () | ||
| 103 | "Test basics of `lisp-indent-region'." | ||
| 104 | (with-temp-buffer | ||
| 105 | (insert lisp-mode-tests--correctly-indented-sexp) | ||
| 106 | (goto-char (point-min)) | ||
| 107 | (let ((indent-tabs-mode nil) | ||
| 108 | (correct lisp-mode-tests--correctly-indented-sexp)) | ||
| 109 | (emacs-lisp-mode) | ||
| 110 | (indent-region (point-min) (point-max)) | ||
| 111 | ;; Don't mess up correctly indented code. | ||
| 112 | (should (string= (buffer-string) correct)) | ||
| 113 | ;; Correctly add indentation. | ||
| 114 | (save-excursion | ||
| 115 | (while (not (eobp)) | ||
| 116 | (delete-horizontal-space) | ||
| 117 | (forward-line))) | ||
| 118 | (indent-region (point-min) (point-max)) | ||
| 119 | (should (equal (buffer-string) correct)) | ||
| 120 | ;; Correctly remove indentation. | ||
| 121 | (save-excursion | ||
| 122 | (let ((n 0)) | ||
| 123 | (while (not (eobp)) | ||
| 124 | (unless (looking-at "noindent\\|^[[:blank:]]*$") | ||
| 125 | (insert (make-string n ?\s))) | ||
| 126 | (cl-incf n) | ||
| 127 | (forward-line)))) | ||
| 128 | (indent-region (point-min) (point-max)) | ||
| 129 | (should (equal (buffer-string) correct))))) | ||
| 130 | |||
| 131 | |||
| 132 | (ert-deftest lisp-indent-region-defun-with-docstring () | ||
| 133 | "Test Bug#26619." | ||
| 134 | (with-temp-buffer | ||
| 135 | (insert "\ | ||
| 136 | \(defun test () | ||
| 137 | \"This is a test. | ||
| 138 | Test indentation in emacs-lisp-mode\" | ||
| 139 | (message \"Hi!\"))") | ||
| 140 | (let ((indent-tabs-mode nil) | ||
| 141 | (correct (buffer-string))) | ||
| 142 | (emacs-lisp-mode) | ||
| 143 | (indent-region (point-min) (point-max)) | ||
| 144 | (should (equal (buffer-string) correct))))) | ||
| 145 | |||
| 146 | (ert-deftest lisp-indent-region-open-paren () | ||
| 147 | (with-temp-buffer | ||
| 148 | (insert "\ | ||
| 149 | \(with-eval-after-load 'foo | ||
| 150 | (setq bar `( | ||
| 151 | baz)))") | ||
| 152 | (let ((indent-tabs-mode nil) | ||
| 153 | (correct (buffer-string))) | ||
| 154 | (emacs-lisp-mode) | ||
| 155 | (indent-region (point-min) (point-max)) | ||
| 156 | (should (equal (buffer-string) correct))))) | ||
| 157 | |||
| 158 | (ert-deftest lisp-indent-region-in-sexp () | ||
| 159 | (with-temp-buffer | ||
| 160 | (insert "\ | ||
| 161 | \(when t | ||
| 162 | (when t | ||
| 163 | (list 1 2 3) | ||
| 164 | 'etc) | ||
| 165 | (quote etc) | ||
| 166 | (quote etc))") | ||
| 167 | (let ((indent-tabs-mode nil) | ||
| 168 | (correct (buffer-string))) | ||
| 169 | (emacs-lisp-mode) | ||
| 170 | (search-backward "1") | ||
| 171 | (indent-region (point) (point-max)) | ||
| 172 | (should (equal (buffer-string) correct))))) | ||
| 173 | |||
| 174 | |||
| 100 | (provide 'lisp-mode-tests) | 175 | (provide 'lisp-mode-tests) |
| 101 | ;;; lisp-mode-tests.el ends here | 176 | ;;; lisp-mode-tests.el ends here |