7

How to swap two arguments for C function calls? eg:

my_function(foo, bar, baz);
~~~~~~~~~~~~~^ (cursor location)

Should be transposed to the right to make:

my_function(bar, foo, baz);

The simple case works for this similar question.

But fails with:

my_function(&foo, *bar, baz[2]);
~~~~~~~~~~~~~ ^ (cursor location)

Giving:

my_function(&*bar, foo, baz[2]);

From what I can tell this is because these functions rely on backward-sexp which doesn't account for some characters used in C code.


For reference, this is the code: from https://emacs.stackexchange.com/a/11062/2418

(defun custom-calculate-stops ()
  (save-excursion
    (let
      (
        (start
          (condition-case e
            (while t (backward-sexp))
            (error (point))))
        stops)
      (push start stops)
      (condition-case e
        (while t
          (forward-sexp)
          (when (looking-at "\\s-*,")
            (push (point) stops)))
        (error (push (point) stops)))
      (nreverse stops))))
(defun custom-transpose-args ()
  (interactive)
  (when (looking-at "\\s-") (backward-sexp))
  (cl-loop with p = (point)
    with previous = nil
    for stop on (custom-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)))
    (cl-return)
    end do (setf previous (car stop))))
ideasman42
  • 8,375
  • 1
  • 28
  • 105

2 Answers2

5

The following transposes two arguments of c-functions. The first argument is the one with point in it. It is followed by the second arg and separated by a comma.

It looks for arguments by skipping over sexps and whitespaces-comments until it finds an argument separator, i.e., a comma or a parenthesis.

(defun c-forward-to-argsep ()
  "Move to the end of the current c function argument.
Returns point."
  (interactive)
  (while (progn (comment-forward most-positive-fixnum)
        (looking-at-p "[^,)]"))
    (forward-sexp))
  (point))

(defun c-backward-to-argsep ()
  "Move to the beginning of the current c function argument.
Returns point."
  (interactive)
  (let ((pt (point))
    cur)
    (up-list -1)
    (forward-char)
    (while (progn
         (setq cur (point))
         (> pt (c-forward-to-argsep)))
      (forward-char))
    (goto-char cur)))

(defun c-transpose-args ()
  "Transpose two arguments of a c-function.
The first arg is the one with point in it."
  (interactive)
  (let* ((pt (point))
     (b (c-backward-to-argsep))
     (sep (progn (goto-char pt)
             (c-forward-to-argsep)))
     (e (progn
          (unless (looking-at-p ",")
        (user-error "Argument separator not found"))
          (forward-char)
          (c-forward-to-argsep)))
     (ws-first (buffer-substring-no-properties
            (goto-char b)
            (progn (skip-chars-forward "[[:space:]\n]")
               (point))))
     (first (buffer-substring-no-properties (point) sep))
     (ws-second (buffer-substring-no-properties
             (goto-char (1+ sep))
             (progn (skip-chars-forward "[[:space:]\n]")
                (point))))
     (second (buffer-substring-no-properties (point) e)))
    (delete-region b e)
    (insert ws-first second "," ws-second first)))
ideasman42
  • 8,375
  • 1
  • 28
  • 105
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • When usnig this w/ `my_function(&foo, &bar, baz[2]);` on any of the I get the error `Scan error: "Containing expression ends prematurely"` – ideasman42 Feb 20 '19 at 11:07
  • Odd, this works when I'm in a text file, but not when I'm editing C source code (even if it's the only statement in the file). – ideasman42 Feb 20 '19 at 11:20
  • @ideasman42 The problem is that punctuation characters cause the following character to be joined to the sexp. That is a really strange behavior if one considers `(&` or `&)`! Maybe some GUI stuff could act in that way but definitively not `scan-sexps`. :-( Let us delete this answer until I find a fix. – Tobias Feb 20 '19 at 11:27
  • 1
    @ideasman42 Fixed by avoiding `backward-sexp`. – Tobias Feb 20 '19 at 11:49
  • Added updates to this answer, feel free to include changes and I'll delete mine. – ideasman42 Feb 20 '19 at 22:15
  • @ideasman42 I prefer to keep the answer as simple as possible. Your extensions are more suitable for a library based on this answer. But, that is a personal opinion. – Tobias Feb 20 '19 at 23:31
5

Based on @Tobias's excellent answer.

  • this extends it to have both forward and backward transpose.
  • correct cursor location.
  • don't move the cursor if there is nothing to do.

Note that this happens to work well for other languages that use comma separated arguments (C++, Lua & Python for e.g. ... probably others).


(defun my-c-transpose-args--forward-to-argsep ()
  "Move to the end of the current c function argument.
Returns point."
  (while (progn
           (comment-forward most-positive-fixnum)
           (looking-at-p "[^,)]"))
    (forward-sexp))
  (point))

(defun my-c-transpose-args--backward-to-argsep ()
  "Move to the beginning of the current c function argument.
Returns point."
  (let ((pt (point))
        (cur nil))
    (up-list -1)
    (forward-char)
    (while (progn
             (setq cur (point))
             (> pt (my-c-transpose-args--forward-to-argsep)))
      (forward-char))
    (goto-char cur)))

(defun my-c-transpose-args--direction (is_forward)
  "Transpose two arguments of a c-function.
The first arg is the one with point in it."
  (let* ((pt-original (point)) ;; only different to pt when not 'is_forward'
         (pt-offset
          (let ((pt-bounds (bounds-of-thing-at-point 'symbol)))
            (cond
             ;; Ensure the cursor is at the beginning of the symbol/bounds.
             ;; NOTE: this is needed when moving arguments that themselves
             ;; contain an S-expression, e.g.
             ;;   function(array[i], a, b);
             ;;             ^
             ;; In this case it's important for the cursor to be at the
             ;; argument start.
             (pt-bounds
              (goto-char (car pt-bounds))
              (- pt-original (car pt-bounds)))
             (t
              0))))
         (pt (progn
               (when (not is_forward)
                 (goto-char (- (my-c-transpose-args--backward-to-argsep) 1))
                 (unless (looking-at-p ",")
                   (goto-char pt-original)
                   (user-error "Argument separator not found")))
               (point)))
         (b (my-c-transpose-args--backward-to-argsep))
         (sep (progn
                (goto-char pt)
                (my-c-transpose-args--forward-to-argsep)))
         (e (progn
              (unless (looking-at-p ",")
                (goto-char pt-original)
                (user-error "Argument separator not found"))
              (forward-char)
              (my-c-transpose-args--forward-to-argsep)))
         (ws-first (buffer-substring-no-properties
                    (goto-char b)
                    (progn
                      (skip-chars-forward "[[:space:]\n]")
                      (point))))
         (first (buffer-substring-no-properties (point) sep))
         (ws-second (buffer-substring-no-properties
                     (goto-char (1+ sep))
                     (progn
                       (skip-chars-forward "[[:space:]\n]")
                       (point))))
         (second (buffer-substring-no-properties (point) e)))

    (delete-region b e)
    (insert ws-first second "," ws-second first)

    ;; Correct the cursor location to be on the same character.
    (goto-char
     (cond
      (is_forward
       (+
        ;; word start.
        (- (point) (length first))
        ;; Apply initial offset within the word.
        (- pt b (length ws-first))
        ;; Apply any offset.
        pt-offset))
      (t
       (+
        b (length ws-first)
        ;; Apply initial offset within the word.
        (- pt-original (+ pt 1 (length ws-second)))))))))

(defun my-c-transpose-args-forward ()
  (interactive)
  (my-c-transpose-args--direction t))

(defun my-c-transpose-args-backward ()
  (interactive)
  (my-c-transpose-args--direction nil))
ideasman42
  • 8,375
  • 1
  • 28
  • 105
  • Thanks, amazing! A small bug though: when `c-transpose-args-backward` is executed in between `<…>`, it errors with `Containing expression ends prematurely`. E.g. this text: `foo(monostate, const Slice rows)`. Although executing the function in any point that is not inside `<` and `>` works as expected. – Hi-Angel May 24 '19 at 07:47
  • 1
    I fixed it: please, apply [this patch](https://github.com/Hi-Angel/dotfiles/commit/5d48489cf80f0ceceb9f2b46944c457fd88f1de5#diff-1281e836e719f3cdc3750acfe4a4cf89) to your answer. – Hi-Angel May 27 '19 at 09:40
  • Could you adjust this so it only runs for C++ code? it's not needed in C code for eg. – ideasman42 May 27 '19 at 10:49
  • I don't think it is needed, because the code shouldn't change behavior with regard to C. The patch does 2 things: in `c-forward-to-argsep` it handles `scan-error` exception, which only happens on template braces. I just checked: if there's lone `>` brace in the way of `(forward-sexp)` call, exception doesn't appear. …And in `c-backward-to-argsep` it handles that `<…>` in C++ is a balanced expression. But it isn't in C, so problem is irrelevant there, and the change wouldn't break anything. – Hi-Angel May 27 '19 at 11:14
  • 2
    I should add: ultimately, "argument syntax" in C is a subset of the one in C++, so if this breaks anything for C, it gonna also regress C++. – Hi-Angel May 27 '19 at 14:31