From ac260248371437ba930b15fcad23fd6c2ada9f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 18 May 2016 06:32:18 -0400 Subject: [PATCH 1/6] Rework error handling * yasnippet.el (yas-good-grace): Add support for `inline' and `hooks' options. (yas--eval-for-string): Renamed from `yas--eval-lisp'. Errors are passed straight through (no rethrow) when `yas-good-grace' is nil. (yas--handle-error): Removed. (yas--eval-for-effect): Renamed from `yas--eval-lisp-no-saves'. Use `yas--safely-run-hook'. (yas-define-snippets): Update docstring. (yas-throw): Don't throw, just signal. (yas--save-backquotes, yas--apply-transform): Use `yas--eval-for-string'. (yas--safely-run-hook): Renamed from `yas--safely-run-hooks'. Takes 1 hook function, instead of a hook var; check `yas-good-grace'. (yas-expand-snippet): Use `yas--eval-for-effect'. --- yasnippet.el | 89 ++++++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/yasnippet.el b/yasnippet.el index 644aa90..44545c0 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -340,9 +340,16 @@ per-snippet basis. A value of `cua' is considered equivalent to (const cua))) ; backwards compat (defcustom yas-good-grace t - "If non-nil, don't raise errors in inline elisp evaluation. + "If non-nil, don't raise errors in elisp evaluation. -An error string \"[yas] error\" is returned instead." +This affects both the inline elisp in snippets and the hook +variables such as `yas-after-exit-snippet-hook'. + +If this variable's value is `inline', an error string \"[yas] +error\" is returned instead of raising the error. If this +variable's value is `hooks', a message is output to according to +`yas-verbosity-level'. If this variable's value is t, both are +active." :type 'boolean) (defcustom yas-visit-from-menu nil @@ -1323,33 +1330,27 @@ Returns (TEMPLATES START END). This function respects ;;; Internal functions and macros: -(defun yas--handle-error (err) - "Handle error depending on value of `yas-good-grace'." - (let ((msg (yas--format "elisp error: %s" (error-message-string err)))) - (if yas-good-grace msg - (error "%s" msg)))) - -(defun yas--eval-lisp (form) +(defun yas--eval-for-string (form) "Evaluate FORM and convert the result to string." - (let ((retval (catch 'yas--exception - (condition-case err - (save-excursion - (save-restriction - (save-match-data - (widen) - (let ((result (eval form))) - (when result - (format "%s" result)))))) - (error (yas--handle-error err)))))) - (when (and (consp retval) - (eq 'yas--exception (car retval))) - (error (cdr retval))) - retval)) + (let ((eval-saving-stuff + (lambda (form) + (save-excursion + (save-restriction + (save-match-data + (widen) + (let ((result (eval form))) + (when result + (format "%s" result))))))))) + (if (memq yas-good-grace '(t inline)) + (condition-case oops + (funcall eval-saving-stuff form) + (yas--exception (signal 'yas-exception (cdr oops))) + (error (cdr oops))) + (funcall eval-saving-stuff form)))) -(defun yas--eval-lisp-no-saves (form) - (condition-case err - (eval form) - (error (message "%s" (yas--handle-error err))))) +(defun yas--eval-for-effect (form) + ;; FIXME: simulating lexical-binding. + (yas--safely-run-hook `(lambda () ,form))) (defun yas--read-lisp (string &optional nil-on-error) "Read STRING as a elisp expression and return it. @@ -1665,7 +1666,7 @@ this is a snippet or a snippet-command. CONDITION, EXPAND-ENV and KEYBINDING are Lisp forms, they have been `yas--read-lisp'-ed and will eventually be -`yas--eval-lisp'-ed. +`yas--eval-for-string'-ed. The remaining elements are strings. @@ -2855,7 +2856,8 @@ The last element of POSSIBILITIES may be a list of strings." (defun yas-throw (text) "Throw a yas--exception with TEXT as the reason." - (throw 'yas--exception (cons 'yas--exception text))) + (signal 'yas--exception text)) +(put 'yas--exception 'error-conditions '(error yas--exception)) (defun yas-verify-value (possibilities) "Verify that the current field value is in POSSIBILITIES. @@ -3020,7 +3022,7 @@ string iff EMPTY-ON-NIL-P is true." (transformed (and transform (save-excursion (goto-char start-point) - (let ((ret (yas--eval-lisp transform))) + (let ((ret (yas--eval-for-string transform))) (or ret (and empty-on-nil-p ""))))))) transformed)) @@ -3333,11 +3335,16 @@ This renders the snippet as ordinary text." (yas--maybe-move-to-active-field snippet)) (setq yas--snippets-to-move nil)) -(defun yas--safely-run-hooks (hook-var) - (condition-case error - (run-hooks hook-var) - (error - (yas--message 2 "%s error: %s" hook-var (error-message-string error))))) +(defun yas--safely-run-hook (hook) + (let ((run-the-hook (lambda (hook) (funcall hook)))) + (if (memq yas-good-grace '(t hooks)) + (funcall run-the-hook hook) + (condition-case error + (funcall run-the-hook hook) + (error + (yas--message 2 "Error running %s: %s" + (if (symbolp hook) hook "a hook") + (error-message-string error))))))) (defun yas--check-commit-snippet () @@ -3371,8 +3378,8 @@ If so cleans up the whole snippet up." nil))))) (unless (or (null snippets) snippets-left) (if snippet-exit-transform - (yas--eval-lisp-no-saves snippet-exit-transform)) - (yas--safely-run-hooks 'yas-after-exit-snippet-hook)))) + (yas--eval-for-effect snippet-exit-transform)) + (mapcar #'yas--safely-run-hook yas-after-exit-snippet-hook)))) ;; Apropos markers-to-points: ;; @@ -3648,7 +3655,7 @@ considered when expanding the snippet." (cond ((listp content) ;; x) This is a snippet-command ;; - (yas--eval-lisp-no-saves content)) + (yas--eval-for-effect content)) (t ;; x) This is a snippet-snippet :-) ;; @@ -4169,9 +4176,9 @@ with their evaluated value into `yas--backquote-markers-and-strings'." (delete-region (match-beginning 0) (match-end 0))) (let ((before-change-functions (cons detect-change before-change-functions))) - (setq transformed (yas--eval-lisp (yas--read-lisp - (yas--restore-escapes - current-string '(?`)))))) + (setq transformed (yas--eval-for-string (yas--read-lisp + (yas--restore-escapes + current-string '(?`)))))) (goto-char (match-beginning 0)) (when transformed (let ((marker (make-marker)) From 9c9547a6281ecdac0a27cef843befa4271a5eefa Mon Sep 17 00:00:00 2001 From: Noam Postavsky Date: Wed, 18 May 2016 07:33:15 -0400 Subject: [PATCH 2/6] Don't catch and rethrow yas-{-}exception * yasnippet.el (yas--eval-for-string): Don't catch yas--exception. (yas-throw): Signal `yas-exception', not `yas--exception'; DATA must be a list. (yas-exception): Add `error-message' property. (yas-verify-value): Just use plain format since the new `error-message' property for `yas-exception' will add the right prefix. --- yasnippet.el | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/yasnippet.el b/yasnippet.el index 44545c0..4258d14 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -1344,7 +1344,6 @@ Returns (TEMPLATES START END). This function respects (if (memq yas-good-grace '(t inline)) (condition-case oops (funcall eval-saving-stuff form) - (yas--exception (signal 'yas-exception (cdr oops))) (error (cdr oops))) (funcall eval-saving-stuff form)))) @@ -2855,17 +2854,16 @@ The last element of POSSIBILITIES may be a list of strings." key))))) (defun yas-throw (text) - "Throw a yas--exception with TEXT as the reason." - (signal 'yas--exception text)) -(put 'yas--exception 'error-conditions '(error yas--exception)) + "Signal `yas-exception' with TEXT as the reason." + (signal 'yas-exception (list text))) +(put 'yas-exception 'error-conditions '(error yas-exception)) +(put 'yas-exception 'error-message "[yas] Exception") (defun yas-verify-value (possibilities) "Verify that the current field value is in POSSIBILITIES. - -Otherwise throw exception." - (when (and yas-moving-away-p - (cl-notany (lambda (pos) (string= pos yas-text)) possibilities)) - (yas-throw (yas--format "Field only allows %s" possibilities)))) +Otherwise signal `yas-exception'." + (when (and yas-moving-away-p (cl-notany (lambda (pos) (string= pos yas-text)) possibilities)) + (yas-throw (format "Field only allows %s" possibilities)))) (defun yas-field-value (number) "Get the string for field with NUMBER. From 203df22e26ac6fd5b1ad8aee3c2b4e5c0b57071d Mon Sep 17 00:00:00 2001 From: Noam Postavsky Date: Wed, 18 May 2016 07:41:53 -0400 Subject: [PATCH 3/6] Use debug-on-error to simplify error handling * yasnippet.el (yas--eval-for-string, yas--safely-run-hook: Let-bind `debug-on-error' according to `yas-good-grace' and add `debug' to condition handler to deduplicate evaluation. --- yasnippet.el | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/yasnippet.el b/yasnippet.el index 4258d14..7645c19 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -1332,20 +1332,17 @@ Returns (TEMPLATES START END). This function respects (defun yas--eval-for-string (form) "Evaluate FORM and convert the result to string." - (let ((eval-saving-stuff - (lambda (form) - (save-excursion - (save-restriction - (save-match-data - (widen) - (let ((result (eval form))) - (when result - (format "%s" result))))))))) - (if (memq yas-good-grace '(t inline)) - (condition-case oops - (funcall eval-saving-stuff form) - (error (cdr oops))) - (funcall eval-saving-stuff form)))) + (let ((debug-on-error (and (not (memq yas-good-grace '(t inline))) + debug-on-error))) + (condition-case oops + (save-excursion + (save-restriction + (save-match-data + (widen) + (let ((result (eval form))) + (when result + (format "%s" result)))))) + ((debug error) (cdr oops))))) (defun yas--eval-for-effect (form) ;; FIXME: simulating lexical-binding. @@ -3334,15 +3331,14 @@ This renders the snippet as ordinary text." (setq yas--snippets-to-move nil)) (defun yas--safely-run-hook (hook) - (let ((run-the-hook (lambda (hook) (funcall hook)))) - (if (memq yas-good-grace '(t hooks)) - (funcall run-the-hook hook) - (condition-case error - (funcall run-the-hook hook) - (error - (yas--message 2 "Error running %s: %s" - (if (symbolp hook) hook "a hook") - (error-message-string error))))))) + (let ((debug-on-error (and (not (memq yas-good-grace '(t hooks))) + debug-on-error))) + (condition-case error + (funcall hook) + ((debug error) + (yas--message 2 "Error running %s: %s" + (if (symbolp hook) hook "a hook") + (error-message-string error)))))) (defun yas--check-commit-snippet () From b62cf52f3517d9ba62635304df8b8cac877c53e2 Mon Sep 17 00:00:00 2001 From: Noam Postavsky Date: Wed, 18 May 2016 07:48:22 -0400 Subject: [PATCH 4/6] Remove lambda list building hack * yasnippet.el (yas-load-directory, yas--eval-for-effect): Use `apply-partially' instead of building lambda lists. --- yasnippet.el | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/yasnippet.el b/yasnippet.el index 7645c19..f6036da 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -1345,8 +1345,7 @@ Returns (TEMPLATES START END). This function respects ((debug error) (cdr oops))))) (defun yas--eval-for-effect (form) - ;; FIXME: simulating lexical-binding. - (yas--safely-run-hook `(lambda () ,form))) + (yas--safely-run-hook (apply-partially #'eval form))) (defun yas--read-lisp (string &optional nil-on-error) "Read STRING as a elisp expression and return it. @@ -1755,8 +1754,7 @@ With prefix argument USE-JIT do jit-loading of snippets." ;; (yas--define-parents mode-sym parents) (yas--menu-keymap-get-create mode-sym) - (let ((fun `(lambda () ;; FIXME: Simulating lexical-binding. - (yas--load-directory-1 ',dir ',mode-sym)))) + (let ((fun (apply-partially #'yas--load-directory-1 dir mode-sym))) (if use-jit (yas--schedule-jit mode-sym fun) (funcall fun))) From 6c4fbb25b3cd50407b1b458e807922979995a684 Mon Sep 17 00:00:00 2001 From: Noam Postavsky Date: Wed, 18 May 2016 08:03:30 -0400 Subject: [PATCH 5/6] Fix snippet local exit hook * yasnippet.el (yas--check-commit-snippet): Use `yas-after-exit-snippet-hook' value of last exited snippet. * yasnippet.el (yas--safely-run-hook): Also accept a list of functions. * yasnippet.el (yas--safely-call-fun): New function, extracted from yas--safely-run-hook. --- yasnippet.el | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/yasnippet.el b/yasnippet.el index f6036da..715dce6 100644 --- a/yasnippet.el +++ b/yasnippet.el @@ -3328,16 +3328,19 @@ This renders the snippet as ordinary text." (yas--maybe-move-to-active-field snippet)) (setq yas--snippets-to-move nil)) +(defun yas--safely-call-fun (fun) + (condition-case error + (funcall fun) + ((debug error) + (yas--message 2 "Error running %s: %s" + (if (symbolp fun) fun "a hook") + (error-message-string error))))) + (defun yas--safely-run-hook (hook) (let ((debug-on-error (and (not (memq yas-good-grace '(t hooks))) debug-on-error))) - (condition-case error - (funcall hook) - ((debug error) - (yas--message 2 "Error running %s: %s" - (if (symbolp hook) hook "a hook") - (error-message-string error)))))) - + (if (functionp hook) (yas--safely-call-fun hook) + (mapc #'yas--safely-call-fun hook)))) (defun yas--check-commit-snippet () "Check if point exited the currently active field of the snippet. @@ -3345,15 +3348,19 @@ This renders the snippet as ordinary text." If so cleans up the whole snippet up." (let* ((snippets (yas-active-snippets 'all)) (snippets-left snippets) - (snippet-exit-transform)) + (snippet-exit-transform nil) + (snippet-exit-hook yas-after-exit-snippet-hook)) (dolist (snippet snippets) (let ((active-field (yas--snippet-active-field snippet))) (yas--letenv (yas--snippet-expand-env snippet) + ;; Note: the `force-exit' field could be a transform in case of + ;; ${0: ...}, see `yas--move-to-field'. (setq snippet-exit-transform (yas--snippet-force-exit snippet)) (cond ((or snippet-exit-transform (not (and active-field (yas--field-contains-point-p active-field)))) (setq snippets-left (delete snippet snippets-left)) (setf (yas--snippet-force-exit snippet) nil) + (setq snippet-exit-hook yas-after-exit-snippet-hook) (yas--commit-snippet snippet)) ((and active-field (or (not yas--active-field-overlay) @@ -3371,7 +3378,7 @@ If so cleans up the whole snippet up." (unless (or (null snippets) snippets-left) (if snippet-exit-transform (yas--eval-for-effect snippet-exit-transform)) - (mapcar #'yas--safely-run-hook yas-after-exit-snippet-hook)))) + (yas--safely-run-hook snippet-exit-hook)))) ;; Apropos markers-to-points: ;; From f3d0e03a05d3422bbeec1ed57514cc1628181251 Mon Sep 17 00:00:00 2001 From: Noam Postavsky Date: Sat, 17 Dec 2016 15:07:35 -0500 Subject: [PATCH 6/6] * yasnippet-tests.el (snippet-exit-hooks): New test. --- yasnippet-tests.el | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/yasnippet-tests.el b/yasnippet-tests.el index a6abcb7..dc0c43c 100644 --- a/yasnippet-tests.el +++ b/yasnippet-tests.el @@ -553,6 +553,36 @@ TODO: correct this bug!" "brother from another mother") ;; no newline should be here! ))) +(ert-deftest snippet-exit-hooks () + (defvar yas--ran-exit-hook) + (with-temp-buffer + (yas-saving-variables + (let ((yas--ran-exit-hook nil) + (yas-triggers-in-field t)) + (yas-with-snippet-dirs + '((".emacs.d/snippets" + ("emacs-lisp-mode" + ("foo" . "\ +# expand-env: ((yas-after-exit-snippet-hook (lambda () (setq yas--ran-exit-hook t)))) +# -- +FOO ${1:f1} ${2:f2}") + ("sub" . "\ +# expand-env: ((yas-after-exit-snippet-hook (lambda () (setq yas--ran-exit-hook 'sub)))) +# -- +SUB")))) + (yas-reload-all) + (emacs-lisp-mode) + (yas-minor-mode +1) + (insert "foo") + (ert-simulate-command '(yas-expand)) + (should-not yas--ran-exit-hook) + (yas-mock-insert "sub") + (ert-simulate-command '(yas-expand)) + (ert-simulate-command '(yas-next-field)) + (should-not yas--ran-exit-hook) + (ert-simulate-command '(yas-next-field)) + (should (eq yas--ran-exit-hook t))))))) + (defvar yas--barbaz) (defvar yas--foobarbaz)