How I can I swap two arguments in a call to a Python function?

If I put point on the space between these two arguments:

self.assertEqual(json.loads(some.data), json_data)

and then M-t (transpose-words), I get:

self.assertEqual(json.loads(some.json), data_data)

On the other hand with C-M-t (transpose-sexps) I get:

self.assertEqual(json.loadsjson_data, (some.data))

What I want is:

self.assertEqual(json_data, json.loads(some.data))

Is there a command that will do that?

  • 2
    I haven't tried but Anchored Transpose can be used for this; it's, though, a 2-step process. – Kaushal Modi May 1 '15 at 10:58
  • There is a core function called transpose-subr which takes a forward function and translates it into a transpose function. So if we had c-forward-arglist (function to move from one function arg to the next - AFAICT this doesn't exist) we would have c-transpose-arglist. – Brendan Aug 8 '19 at 8:31

This is something I, too, wanted to have for a long time, and here I've found some motivation to work on it. It's probably not very robust, but at the first try it seems to cover the cases I tried:

(defun my/calculate-stops ()
    (let ((start
           (condition-case e
               (while t (backward-sexp))
             (error (point))))
      (push start stops)
      (condition-case e
          (while t
            (when (looking-at "\\s-*,")
              (push (point) stops)))
        (error (push (point) stops)))
      (nreverse stops))))

(defun my/transpose-args ()
  (when (looking-at "\\s-") (backward-sexp))
  (cl-loop with p = (point)
           with previous = nil
           for stop on (my/calculate-stops)
           for i upfrom 0
           when (<= p (car stop)) do
           (when previous
             (let* ((end (cadr stop))
                    (whole (buffer-substring previous end))
                    middle last)
               (delete-region previous end)
               (goto-char previous)
               (setf middle (if (> i 1) (- (car stop) previous)
                              (string-match "[^, \\t]" whole 
                                            (- (car stop) previous)))
                     last (if (> i 1) (substring whole 0 middle)
                            (concat (substring whole (- (car stop) previous) middle)
                                    (substring whole 0 (- (car stop) previous)))))
               (insert (substring whole middle) last)))
           end do (setf previous (car stop))))
| improve this answer | |
  • I get this when point is on the space (works when on the comma): let*: Wrong type argument: integer-or-marker-p, nil – Croad Langshan May 4 '15 at 20:24
  • @CroadLangshan the fix was relatively easy (see above). The funny thing I tried to follow Stefan's advise by writing an alternative forward-sexp-function, and that appeared to be too cumbersome because of the commas. I then tried to copycat traspose-sexp to do the same thing, and, again, having to account for commas makes this really difficult. I don't claim this always does the right thing when it comes to list delimiters, maybe only half of the time... – wvxvw May 5 '15 at 9:40
  • This fails for (aa, bb) when the cursor is on the first a. This also doesn't work for transposing FOO(aaa *, bbb, ccc) The * somehow messes up transposing. – ideasman42 Feb 19 '19 at 22:21
  • Also fails for FOO(&aaa, bbb) (& doesn't swap). – ideasman42 Feb 20 '19 at 2:09

I use a variation of transpose-sexps that looks for the case you describe and transposes things-separated-by-commas, or just does regular transpose-sexps. It also leaves the cursor in place instead of dragging it forward, which is a bit different but I personally like.

(defun my-transpose-sexps ()
  "If point is after certain chars transpose chunks around that.
Otherwise transpose sexps."
  (interactive "*")
  (if (not (looking-back "[,]\\s-*" (point-at-bol)))
      (progn (transpose-sexps 1) (forward-sexp -1))
    (let ((beg (point)) end rhs lhs)
      (while (and (not (eobp))
                  (not (looking-at "\\s-*\\([,]\\|\\s)\\)")))
        (forward-sexp 1))
      (setq rhs (buffer-substring beg (point)))
      (delete-region beg (point))
      (re-search-backward "[,]\\s-*" nil t)
      (setq beg (point))
      (while (and (not (bobp))
                  (not (looking-back "\\([,]\\|\\s(\\)\\s-*" (point-at-bol))))
        (forward-sexp -1))
      (setq lhs (buffer-substring beg (point)))
      (delete-region beg (point))
      (insert rhs)
      (re-search-forward "[,]\\s-*" nil t)
        (insert lhs)))))
| improve this answer | |
  • For me this leaves whitespace at the start of the assertEqual call (starting with point on the space). – Croad Langshan May 4 '15 at 20:27
  • 1
    This does not work for keyword arguments like: f(a=1, b=2) (it transposes around) – Att Righ Nov 26 '17 at 21:01
  • 1
    While the question is abount C code, this answer works for f(a=1, b=2) - emacs.stackexchange.com/a/47930/2418 – ideasman42 Feb 21 '19 at 0:19

In modes that use SMIE, transpose-sexp should work correctly for that case. They will still fail when the infix symbol (aka "separator") is not a , (or a ;) but is a word (e.g. and).

So, my opinion is that the command for that is transpose-sexp and when this doesn't work correctly, I consider it as a bug (but a bug that may be hard and/or take time to fix and have low-priority, so don't hold your breath). The way to fix it is by setting forward-sexp-function to a function that will know that "after I see a comma, I just jump around the whole argument".

| improve this answer | |
  • 1
    I guess java-mode (for example) doesn't use SMIE? How would one go about fixing that? – Samuel Edwin Ward Oct 21 '15 at 17:58
  • 1
    No, indeed, major modes for C-like languages don't use SMIE and not only for historical reasons: SMIE's parser is just too weak for those languages. This said, SMIE's parser would work just fine if you want to swap two arguments separated by a comma (this part of the grammar is simple enough), so I guess it would be possible to configure SMIE and use it for forward-sexp-function but you'd have to add some code to only use SMIE's forward-sexp-function in those cases where it works well enough, since in many other cases it would result in confusing behavior. – Stefan Oct 22 '15 at 0:36

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.