branch: master commit c432e78ffd7f09b3b1868345ff80001a6bbe2ee6 Author: Noam Postavsky <npost...@users.sourceforge.net> Commit: Noam Postavsky <npost...@users.sourceforge.net>
Fix field adjustment on deletion For deletion, we need to check the bounds before the deletion happens, otherwise the overlay may already be moved to wrong place. * yasnippet.el (yas--before-change-modified-snippets): New variable. (yas--merge-and-drop-dups): New function. (yas--gather-active-snippets): New function. (yas--on-field-overlay-modification): Use it. * yasnippet.el --- yasnippet-tests.el | 21 ++++++++++++ yasnippet.el | 94 +++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/yasnippet-tests.el b/yasnippet-tests.el index db9c177..7791db8 100644 --- a/yasnippet-tests.el +++ b/yasnippet-tests.el @@ -1122,6 +1122,27 @@ hello ${1:$(when (stringp yas-text) (funcall func yas-text))} foo${1:$$(concat \ (ert-simulate-command '(yas-next-field-or-maybe-expand)) (should (string= (buffer-string) "<-<-abcdef\n"))))) +(ert-deftest nested-snippet-expansion-5-nested-delete () + "See Github #996." + (let ((yas-triggers-in-field t)) + (yas-with-snippet-dirs + '((".emacs.d/snippets" + ("text-mode" + ("sel" . "${1:ch}") + ("ch" . "<-${1:ch}")))) + (yas-reload-all) + (text-mode) + (yas-minor-mode +1) + (insert "sel") + (ert-simulate-command '(yas-expand)) + (ert-simulate-command '(forward-word 1)) + (ert-simulate-command '(yas-expand)) + (ert-simulate-command '(forward-word 1)) + ;; The (cl-assert (memq pfield (yas--snippet-fields psnippet))) + ;; in `yas--on-field-overlay-modification' failed here. + (ert-simulate-command '(delete-backward-char 1)) + (should (string= (buffer-string) "<-c\n"))))) + ;;; Loading ;;; diff --git a/yasnippet.el b/yasnippet.el index 2ec192a..27df486 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -3788,13 +3788,45 @@ BEG, END and LENGTH like overlay modification hooks." (defvar yas--todo-snippet-indent nil nil) (make-variable-buffer-local 'yas--todo-snippet-indent) +(defvar yas--before-change-modified-snippets nil) + +(defun yas--merge-and-drop-dups (list1 list2 cmp key) + ;; `delete-consecutive-dups' + `cl-merge'. + (funcall (if (fboundp 'delete-consecutive-dups) + #'delete-consecutive-dups ; 24.4 + #'delete-dups) + (cl-merge 'list list1 list2 cmp :key key))) + +(defun yas--gather-active-snippets (overlay beg end then-delete) + ;; Add active snippets in BEG..END into an OVERLAY keyed entry of + ;; `yas--before-change-modified-snippets'. Return accumulated list. + ;; If THEN-DELETE is non-nil, delete the entry. + (let ((new (yas-active-snippets beg end)) + (old (assq overlay yas--before-change-modified-snippets))) + (prog1 (cond ((and new old) + (setf (cdr old) + (yas--merge-and-drop-dups + (cdr old) new + ;; Sort like `yas-active-snippets'. + #'>= #'yas--snippet-id))) + (new (unless then-delete + ;; Don't add new entry if we're about to + ;; remove it anyway. + (push (cons overlay new) + yas--before-change-modified-snippets)) + new) + (old (cdr old)) + (t nil)) + (when then-delete + (cl-callf2 delq old yas--before-change-modified-snippets))))) + + (defun yas--on-field-overlay-modification (overlay after? beg end &optional length) "Clears the field and updates mirrors, conditionally. Only clears the field if it hasn't been modified and point is at field start. This hook does nothing if an undo is in progress." - (unless (or (not after?) - yas--inhibit-overlay-hooks + (unless (or yas--inhibit-overlay-hooks (not (overlayp yas--active-field-overlay)) ; Avoid Emacs bug #21824. ;; If a single change hits multiple overlays of the same ;; snippet, then we delete the snippet the first time, @@ -3807,33 +3839,37 @@ field start. This hook does nothing if an undo is in progress." (field (overlay-get overlay 'yas--field)) (snippet (overlay-get yas--active-field-overlay 'yas--snippet))) (if (yas--snippet-live-p snippet) - (save-match-data - (yas--letenv (yas--snippet-expand-env snippet) - (when (yas--skip-and-clear-field-p field beg end length) - ;; We delete text starting from the END of insertion. - (yas--skip-and-clear field end)) - (setf (yas--field-modified-p field) t) - ;; Adjust any pending active fields in case of stacked - ;; expansion. - (let ((pfield field) - (psnippets (yas-active-snippets beg end))) - (while (and pfield psnippets) - (let ((psnippet (pop psnippets))) - (cl-assert (memq pfield (yas--snippet-fields psnippet))) - (yas--advance-end-maybe pfield (overlay-end overlay)) - (setq pfield (yas--snippet-previous-active-field psnippet))))) - ;; 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)))) + (if after? + (save-match-data + (yas--letenv (yas--snippet-expand-env snippet) + (when (yas--skip-and-clear-field-p field beg end length) + ;; We delete text starting from the END of insertion. + (yas--skip-and-clear field end)) + (setf (yas--field-modified-p field) t) + ;; Adjust any pending active fields in case of stacked + ;; expansion. + (let ((pfield field) + (psnippets (yas--gather-active-snippets + overlay beg end t))) + (while (and pfield psnippets) + (let ((psnippet (pop psnippets))) + (cl-assert (memq pfield (yas--snippet-fields psnippet))) + (yas--advance-end-maybe pfield (overlay-end overlay)) + (setq pfield (yas--snippet-previous-active-field psnippet))))) + ;; 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)))) + ;; Remember active snippets to use for after the change. + (yas--gather-active-snippets overlay beg end nil)) (lwarn '(yasnippet zombie) :warning "Killing zombie snippet!") (delete-overlay overlay)))))