I'd like to be able delete the innermost brackets around the current point.

What's a good way to do this?

While the evil-surround package can do this, it needs to take a bracket type as input, so can't remove the inner-most brackets (without some extra code).


Use up-list or backward-up-list to move forward or backward to the first enclosing bracket, then forward-list or backward-list to locate the matching bracket. Delete the closing bracket then the opening bracket (in this order, because deleting the opening bracket moves the position of the closing bracket.

(defun delete-enclosing-parentheses (&optional arg)
  "Delete the innermost enclosing parentheses around point.

With a prefix argument N, delete the Nth level of enclosing parentheses,
where 1 is the innermost level."
  (interactive "*p")
    (backward-up-list arg)
    (let ((beg (point)))
      (delete-backward-char 1)
      (goto-char beg)
      (delete-char 1))))
  • Thanks, this mostly works well - however the behavior is asymmetrical - when the cursor is over the end bracket, it matches the first. When over the first bracket, it doesn't match the last. – ideasman42 Jan 4 '20 at 11:26
  • Added an answer which uses this method but works when the point is an opening brace too: emacs.stackexchange.com/a/54679/2418 – ideasman42 Jan 5 '20 at 2:20
  • @ideasman42 Emacs's idea of the cursor is that it's between characters, not on a character, although many terminal types show the cursor on a character. When the cursor is “on” the opening bracket, it's actually before the opening bracket, so it's outside. You can adopt a different convention, but it'll be inconsistent with how brackets work with some Emacs commands. – Gilles 'SO- stop being evil' Jan 6 '20 at 16:08
  • Good points, since I asked about "surrounding" bracket's, your answer is correct. Although I'll keep using my more verbose solution that handles the case when the point is over the bracket too. – ideasman42 Jan 6 '20 at 22:50
  • You can also use (delete-pair) in place of the whole form that begins with the let-binding. I don't know exactly what the differences are, though. Mine, with delete-pair, used to go crazy when called from within strings. Using (backward-up-list arg t t) tamed it. – Arch Stanton Mar 6 '20 at 18:48

Adding an answer based on @gilles-so-stop-being-evil 's current answer which works when the point is on the opening bracket as well as the last bracket, it also error checks and prints useful status upon completion.

(defun my-delete-surround-at-point--find-brackets (pos)
  "Return a pair of buffer positions for the opening & closing bracket positions.
Or nil when nothing is found."
    (goto-char pos)
         ;; Check if we're on the opening brace.
             ;; Note that the following check for opening brace
             ;; can be skipped, however it can cause the entire buffer
             ;; to be scanned for an opening brace causing noticeable lag.
              ;; Opening brace.
              (eq (syntax-class (syntax-after pos)) 4)
              ;; Not escaped.
              (= (logand (skip-syntax-backward "/\\") 1) 0))
           (forward-char 1)
           (if (and (ignore-errors (backward-up-list 1) t) (eq (point) pos))
             ;; Restore location and fall through to the next check.
             (goto-char pos)
         ;; Check if we're on the closing or final brace.
         (ignore-errors (backward-up-list 1) t))

      ;; Upon success, return the pair as a list.
      (list (point)
              (1- (point)))))))

(defun my-delete-surround-at-point ()
  (let ((range (my-delete-surround-at-point--find-brackets (point))))
    (unless range
      (user-error "No surrounding brackets"))
    (pcase-let ((`(,beg ,end) range))
      ;; For user message.
      (let ((lines (count-lines beg end))
            (beg-char (char-after beg))
            (end-char (char-after end)))

          (goto-char end)
          (delete-char 1)
          (goto-char beg)
          (delete-char 1))
         "Delete surrounding \"%c%c\"%s" beg-char end-char
         (if (> lines 1)
             (format " across %d lines" lines)

This is an answer I came up with, adding for completeness, however I'd rather not depend on evil-surround because it gives less control and is more likely to break.

(defun delete-surround-at-point (arg)
  (interactive "p")
      (best-range most-positive-fixnum)
      (best-inner nil)
      (best-outer nil)
      (chars (list ?\( ?\[ ?\{ ?\" ?\'))
      (char-found nil))
    (dolist (c chars)
          (overlay-inner (ignore-errors (evil-surround-inner-overlay c)))
              (<= (overlay-start overlay-inner) (point))
              (>= (overlay-end overlay-inner) (point))
              (ignore-errors (evil-surround-outer-overlay c)))))
        (if (and overlay-inner overlay-outer)
          (let ((test-range (- (overlay-end overlay-inner) (overlay-start overlay-inner))))
            (when (< test-range best-range)
              (setq best-range test-range)
              (when best-inner
                (delete-overlay best-inner)
                (delete-overlay best-outer))
              (setq best-inner overlay-inner)
              (setq best-outer overlay-outer)
              (setq char-found c)))
          ;; Else delete either overlay if it exists.
          (when overlay-inner
            (delete-overlay overlay-inner))
          (when overlay-outer
            (delete-overlay overlay-outer)))))
    (unless (null char-found)
      (evil-surround-delete char-found best-outer best-inner))
    (when best-inner
      (delete-overlay best-inner)
      (delete-overlay best-outer))))


Brackets can be detected using forward-sexp and backward-sexp, this examples uses these functions to implement context sensitive bracket removal.

(defun find-surrounding-brackets (pt &optional strict)
  "Find surrounding braces.

Argument PT is the point which is checked for brackets.

When argument STRICT is non-nil, only use braces at PT,
otherwise surrounding brackets are searched for."
  (catch 'match
        (re-begin "\\((\\|{\\|\\[\\)")
        (re-end "\\()\\|}\\|\\]\\)")
        (pt-begin nil)
        (pt-end nil))

      ;; First check if we're already on an ending bracket with a match,
      ;; handle this as a special case because there is no ambiguity in this instance.
      ;; Otherwise search for the previous bracket.
        (goto-char pt)
            ((looking-at re-begin)
              (when (setq pt-end (ignore-errors (forward-sexp) (1- (point))))
                (setq pt-begin pt)
                (throw 'match (list pt-begin pt-end))))
            ((looking-at re-end)
              (forward-char 1)
              (when (setq pt-begin (ignore-errors (backward-sexp) (point)))
                (setq pt-end pt)
                (throw 'match (list pt-begin pt-end)))))))

      ;; Now loop over preview brackets
      ;; until we find a match which includes `pt'.
      (unless strict
        (let ((pt-iter (1+ pt)))
            (setq pt-begin
                (goto-char pt-iter)
                  (when (search-backward-regexp re-begin (point-min) t 1)
                    (match-beginning 0)))))

            (setq pt-end
                (goto-char pt-begin)
                (ignore-errors (forward-sexp) (1- (point)))))

            (when (and pt-end (< pt pt-end))
              (throw 'match (list pt-begin pt-end)))
            (setq pt-iter pt-begin)))))
    (throw 'match nil)))

(defun delete-surround-at-point (arg)
  (interactive "p")
  (let ((range (find-surrounding-brackets (point))))
    (unless range
      (user-error "No surrounding brackets"))
    (pcase-let ((`(,start ,end) range))
        (goto-char end)
        (delete-char 1)
        (goto-char start)
        (delete-char 1)))))

  • 1
    Why are you searching for bracket characters rather than use context-sensitive built-in functions like backward-up-list? Using the built-in functions would be simpler and would handle cases like (foo -!- "(") correctly. – Gilles 'SO- stop being evil' Jan 4 '20 at 10:32
  • I wasn't aware of this function, adding new answer based on this. – ideasman42 Jan 5 '20 at 2:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.