branch: master commit ffce2362681d0bdaf096ed950e84e5f38a1149b4 Author: Noam Postavsky <npost...@users.sourceforge.net> Commit: Noam Postavsky <npost...@users.sourceforge.net>
Do auto indentation only in post command hook * yasnippet.el (yas--todo-snippet-indent): New variable. (yas--on-field-overlay-modification): Save snippet to it, and don't indent after mirror update. (yas--do-todo-field-updates): New function. (yas--post-command-handler): Call it. (yas--snippet-field-mirrors, yas--indent-mirrors-of-snippet): New functions, split out from... (yas--snippet-field-mirrors): ...here. * yasnippet-tests.el (yas-test-delete-and-insert-command) (indent-mirrors-on-complex-update): New test and helper function. --- yasnippet-tests.el | 22 +++++++++++ yasnippet.el | 111 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 94 insertions(+), 39 deletions(-) diff --git a/yasnippet-tests.el b/yasnippet-tests.el index c555a9e..db9c177 100644 --- a/yasnippet-tests.el +++ b/yasnippet-tests.el @@ -608,6 +608,28 @@ int foo() ;; Assuming 2 space indent. (should (string= "def xxx\n xxx\nend" (buffer-string))))) +(defun yas-test-delete-and-insert-command (beg end new) + "Simulate a completion command (similar to company-mode)." + (interactive "r\ns") + ;; Simulate a completion command (like what company-mode does) + ;; which deletes the "xxx" and then replaces it with something + ;; else. + (delete-region beg end) + (insert new)) + +(ert-deftest indent-mirrors-on-complex-update () + "Don't get messed up by command that deletes and then inserts." + (with-temp-buffer + (ruby-mode) + (yas-minor-mode 1) + (yas-expand-snippet "def foo\n ${1:slice} = append($1)\nend") + (yas-mock-insert "xxx") + (ert-simulate-command `(yas-test-delete-and-insert-command + ,(- (point) 3) ,(point) ,"yyy")) + ;; Assuming 2 space indent. + (should (string= "def foo\n yyy = append(yyy)\nend" (buffer-string))))) + + (ert-deftest snippet-with-multiline-mirrors-issue-665 () "In issue 665, a multi-line mirror is attempted." diff --git a/yasnippet.el b/yasnippet.el index dcf593e..2ec192a 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -3785,6 +3785,9 @@ BEG, END and LENGTH like overlay modification hooks." (= beg (yas--field-start field)) ; Insertion at field start? (not (yas--field-modified-p field)))) +(defvar yas--todo-snippet-indent nil nil) +(make-variable-buffer-local 'yas--todo-snippet-indent) + (defun yas--on-field-overlay-modification (overlay after? beg end &optional length) "Clears the field and updates mirrors, conditionally. @@ -3819,12 +3822,29 @@ field start. This hook does nothing if an undo is in progress." (cl-assert (memq pfield (yas--snippet-fields psnippet))) (yas--advance-end-maybe pfield (overlay-end overlay)) (setq pfield (yas--snippet-previous-active-field psnippet))))) - (save-excursion - (yas--field-update-display field)) - (yas--update-mirrors snippet))) + ;; Update fields now, but delay auto indentation until + ;; post-command. We don't want to run indentation on + ;; the intermediate state where field text might be + ;; removed (and hence the field could be deleted along + ;; with leading indentation). + (let ((yas-indent-line nil)) + (save-excursion + (yas--field-update-display field)) + (yas--update-mirrors snippet)) + (unless (or (not (eq yas-indent-line 'auto)) + (memq snippet yas--todo-snippet-indent)) + (push snippet yas--todo-snippet-indent)))) (lwarn '(yasnippet zombie) :warning "Killing zombie snippet!") (delete-overlay overlay))))) +(defun yas--do-todo-field-updates () + (when yas--todo-snippet-indent + (save-excursion + (cl-loop for snippet in yas--todo-snippet-indent + do (yas--indent-mirrors-of-snippet + snippet (yas--snippet-field-mirrors snippet))) + (setq yas--todo-snippet-indent nil)))) + (defun yas--auto-fill () (let* ((orig-point (point)) (end (progn (forward-paragraph) (point))) @@ -4800,46 +4820,58 @@ When multiple expressions are found, only the last one counts." (parent 1) (t 0)))))) +(defun yas--snippet-field-mirrors (snippet) + ;; Make a list of (FIELD . MIRROR). + (cl-sort + (cl-mapcan (lambda (field) + (mapcar (lambda (mirror) + (cons field mirror)) + (yas--field-mirrors field))) + (yas--snippet-fields snippet)) + ;; Then sort this list so that entries with mirrors with + ;; parent fields appear before. This was important for + ;; fixing #290, and also handles the case where a mirror in + ;; a field causes another mirror to need reupdating. + #'> :key (lambda (fm) (yas--calculate-mirror-depth (cdr fm))))) + +(defun yas--indent-mirrors-of-snippet (snippet &optional f-ms) + ;; Indent mirrors of SNIPPET. F-MS is the return value of + ;; (yas--snippet-field-mirrors SNIPPET). + (when (eq yas-indent-line 'auto) + (let ((yas--inhibit-overlay-hooks t)) + (cl-loop for (beg . end) in + (cl-sort (mapcar (lambda (f-m) + (let ((mirror (cdr f-m))) + (cons (yas--mirror-start mirror) + (yas--mirror-end mirror)))) + (or f-ms + (yas--snippet-field-mirrors snippet))) + #'< :key #'car) + do (yas--indent-region beg end snippet))))) + (defun yas--update-mirrors (snippet) "Update all the mirrors of SNIPPET." (yas--save-restriction-and-widen (save-excursion - (cl-loop - for (field . mirror) - in (cl-sort - ;; Make a list of (FIELD . MIRROR). - (cl-mapcan (lambda (field) - (mapcar (lambda (mirror) - (cons field mirror)) - (yas--field-mirrors field))) - (yas--snippet-fields snippet)) - ;; Then sort this list so that entries with mirrors with - ;; parent fields appear before. This was important for - ;; fixing #290, and also handles the case where a mirror in - ;; a field causes another mirror to need reupdating. - #'> :key (lambda (fm) (yas--calculate-mirror-depth (cdr fm)))) - ;; Before updating a mirror with a parent-field, maybe advance - ;; its start (#290). - do (let ((parent-field (yas--mirror-parent-field mirror))) - (when parent-field - (yas--advance-start-maybe mirror (yas--fom-start parent-field)))) - ;; Update this mirror. - do (yas--mirror-update-display mirror field) - ;; Delay indenting until we're done all mirrors. We must do - ;; this to avoid losing whitespace between fields that are - ;; still empty (i.e., they will be non-empty after updating). - when (eq yas-indent-line 'auto) - collect (cons (yas--mirror-start mirror) (yas--mirror-end mirror)) - into indent-regions - ;; `yas--place-overlays' is needed since the active field and - ;; protected overlays might have been changed because of insertions - ;; in `yas--mirror-update-display'. - do (let ((active-field (yas--snippet-active-field snippet))) - (when active-field (yas--place-overlays snippet active-field))) - finally do - (let ((yas--inhibit-overlay-hooks t)) - (cl-loop for (beg . end) in (cl-sort indent-regions #'< :key #'car) - do (yas--indent-region beg end snippet))))))) + (let ((f-ms (yas--snippet-field-mirrors snippet))) + (cl-loop + for (field . mirror) in f-ms + ;; Before updating a mirror with a parent-field, maybe advance + ;; its start (#290). + do (let ((parent-field (yas--mirror-parent-field mirror))) + (when parent-field + (yas--advance-start-maybe mirror (yas--fom-start parent-field)))) + ;; Update this mirror. + do (yas--mirror-update-display mirror field) + ;; `yas--place-overlays' is needed since the active field and + ;; protected overlays might have been changed because of insertions + ;; in `yas--mirror-update-display'. + do (let ((active-field (yas--snippet-active-field snippet))) + (when active-field (yas--place-overlays snippet active-field)))) + ;; Delay indenting until we're done all mirrors. We must do + ;; this to avoid losing whitespace between fields that are + ;; still empty (i.e., they will be non-empty after updating). + (yas--indent-mirrors-of-snippet snippet f-ms))))) (defun yas--mirror-update-display (mirror field) "Update MIRROR according to FIELD (and mirror transform)." @@ -4897,6 +4929,7 @@ When multiple expressions are found, only the last one counts." ;; Don't pop up more than once in a session (still log though). (defvar warning-suppress-types) ; `warnings' is autoloaded by `lwarn'. (add-to-list 'warning-suppress-types '(yasnippet auto-fill bug))) + (yas--do-todo-field-updates) (condition-case err (progn (yas--finish-moving-snippets) (cond ((eq 'undo this-command)