Update handling of markers during indentation

* yasnippet.el (yas--snapshot-marker-location): New function, save a
regexp and whitespace count determining a marker's location in a line.
(yas--restore-marker-location): New function, restores a marker's
location based on info from `yas--snapshot-marker-location'.
(yas--indent-region): Use them to fix marker locations after indentation.
* yasnippet-tests.el (indent-org-property, indent-cc-mode):
(indent-snippet-mode): New tests.
This commit is contained in:
Noam Postavsky 2016-12-03 15:14:21 -05:00
parent 28d5496144
commit e878afb883
2 changed files with 123 additions and 23 deletions

View File

@ -243,6 +243,68 @@ $1 ------------------------")
XXXXX ---------------- XXXXX ---- XXXXX ---------------- XXXXX ----
XXXXX ------------------------")))) XXXXX ------------------------"))))
(ert-deftest indent-org-property ()
"Handling of `org-mode' property indentation, see `org-property-format'."
;; This is an interesting case because `org-indent-line' calls
;; `replace-match' for properties.
(with-temp-buffer
(org-mode)
(yas-minor-mode +1)
(yas-expand-snippet "* Test ${1:test}\n:PROPERTIES:\n:ID: $1-after\n:END:")
(yas-mock-insert "foo bar")
(ert-simulate-command '(yas-next-field))
(goto-char (point-min))
(let ((expected (with-temp-buffer
(insert (format (concat "* Test foo bar\n"
" " org-property-format "\n"
" " org-property-format "\n"
" " org-property-format)
":PROPERTIES:" ""
":ID:" "foo bar-after"
":END:" ""))
(delete-trailing-whitespace)
(buffer-string))))
;; Some org-mode versions leave trailing whitespace, some don't.
(delete-trailing-whitespace)
(should (equal expected (buffer-string))))))
(ert-deftest indent-cc-mode ()
"Handling of cc-mode's indentation."
;; This is an interesting case because cc-mode deletes all the
;; indentation before recreating it.
(with-temp-buffer
(c++-mode)
(yas-minor-mode +1)
(yas-expand-snippet "\
int foo()
{
if ($1) {
delete $1;
$1 = 0;
}
}")
(yas-mock-insert "var")
(should (string= "\
int foo()
{
if (var) {
delete var;
var = 0;
}
}" (buffer-string)))))
(ert-deftest indent-snippet-mode ()
"Handling of snippet-mode indentation."
;; This is an interesting case because newlines match [[:space:]] in
;; snippet-mode.
(with-temp-buffer
(snippet-mode)
(yas-minor-mode +1)
(yas-expand-snippet "# -*- mode: snippet -*-\n# name: $1\n# key: $1\n# --\n")
(yas-mock-insert "foo")
(should (string= "# -*- mode: snippet -*-\n# name: foo\n# key: foo\n# --\n"
(buffer-string)))))
(ert-deftest indent-mirrors-on-update () (ert-deftest indent-mirrors-on-update ()
"Check that mirrors are always kept indented." "Check that mirrors are always kept indented."
(with-temp-buffer (with-temp-buffer

View File

@ -3917,40 +3917,78 @@ Meant to be called in a narrowed buffer, does various passes"
(goto-char parse-start) (goto-char parse-start)
(yas--indent snippet))) (yas--indent snippet)))
;; HACK: Some implementations of `indent-line-function' (called via
;; `indent-according-to-mode') delete text before they insert (like
;; cc-mode), some make complicated regexp replacements (looking at
;; you, org-mode). To find place where the marker "should" go after
;; indentation, we create a regexp based on what the line looks like
;; before, putting a capture group where the marker is. The regexp
;; matches any whitespace with [[:space:]]* to allow for the
;; indentation changing whitespace. Additionally, we try to preserve
;; the amount of whitespace *following* the marker, because
;; indentation generally affects whitespace at the beginning, not the
;; end.
;;
;; This is all best-effort heuristic stuff, but it should cover 99% of
;; use-cases.
(defun yas--snapshot-marker-location (marker)
"Returns info for restoring MARKER's location after indent.
The returned value is a list of the form (REGEXP MARKER WS-COUNT)."
(when (and (<= (line-beginning-position) marker)
(<= marker (line-end-position)))
(let ((before
(split-string (buffer-substring-no-properties
(line-beginning-position) marker) "[[:space:]]+" t))
(after
(split-string (buffer-substring-no-properties
marker (line-end-position)) "[[:space:]]+" t)))
(list (concat "[[:space:]]*"
(mapconcat (lambda (s)
(if (eq s marker) "\\(\\)"
(regexp-quote s)))
(nconc before (list marker) after)
"[[:space:]]*"))
marker
(progn (goto-char marker)
(skip-syntax-forward " " (line-end-position))
(- (point) marker))))))
(defun yas--restore-marker-location (re-marker)
"Restores marker based on info from `yas--snapshot-marker-location'."
(let ((regexp (nth 0 re-marker))
(marker (nth 1 re-marker))
(ws-count (nth 2 re-marker)))
(beginning-of-line)
(save-restriction
;; Narrowing is the only way to limit `looking-at'.
(narrow-to-region (point) (line-end-position))
(if (not (looking-at regexp))
(lwarn '(yasnippet re-marker) :warning
"Couldn't find: %S" regexp)
(goto-char (match-beginning 1))
(skip-syntax-forward " ")
(skip-syntax-backward " " (- (point) ws-count))
(set-marker marker (point))))))
(defun yas--indent-region (from to snippet) (defun yas--indent-region (from to snippet)
"Indent the lines between FROM and TO with `indent-according-to-mode'. "Indent the lines between FROM and TO with `indent-according-to-mode'.
The SNIPPET's markers are preserved." The SNIPPET's markers are preserved."
;;; Apropos indenting problems....
;;
;; `indent-according-to-mode' uses whatever `indent-line-function'
;; is available. Some implementations of these functions delete text
;; before they insert. If there happens to be a marker just after
;; the text being deleted, the insertion actually happens after the
;; marker, which misplaces it.
;;
;; This would also happen if we had used overlays with the
;; `front-advance' property set to nil.
;;
;; This is why I have these `trouble-markers', they are the ones at
;; the first non-whitespace char at the line. After indentation
;; takes place we should be at the correct to restore them. All
;; other non-trouble-markers should have been *pushed* and don't
;; need special attention.
(let* ((snippet-markers (yas--collect-snippet-markers snippet)) (let* ((snippet-markers (yas--collect-snippet-markers snippet))
(to (set-marker (make-marker) to))) (to (set-marker (make-marker) to)))
(save-excursion (save-excursion
(goto-char from) (goto-char from)
(save-restriction (save-restriction
(widen) (widen)
;; Indent each non-empty line.
(cl-loop if (/= (line-beginning-position) (line-end-position)) do (cl-loop if (/= (line-beginning-position) (line-end-position)) do
(back-to-indentation) ;; Indent each non-empty line.
(let ((trouble-markers ; The markers at (point). (let ((remarkers
(cl-remove (point) snippet-markers :test #'/=))) (delq nil (mapcar #'yas--snapshot-marker-location
snippet-markers))))
(unwind-protect (unwind-protect
(indent-according-to-mode) (progn (back-to-indentation)
(dolist (marker trouble-markers) (indent-according-to-mode))
(set-marker marker (point))))) (mapc #'yas--restore-marker-location remarkers)))
while (and (zerop (forward-line 1)) while (and (zerop (forward-line 1))
(< (point) to))))))) (< (point) to)))))))