2022-05-23 Copying code snippets

Two weeks ago I wrote about copying stuff from Emacs to the system clipboard, converting from Org-mode to markdown along the way. Even earlier, I wrote a snippet of code to convert double spaces to single ones when copying. Let’s continue the thread of transforming stuff while copying it from Emacs.

I often need to copy some snippet from the code I’m writing (or reading) to send it to a teammate. One issue I have with that is that those snippets are often taken from the middle of a function etc., and hence they are indented way too much. Take this classic piece of JavaScript:

function greet(name) {
    console.log(`Hello ${name}!`);
}

If I want to copy just the console.log, I don’t really need the tab in front of it. (And while at that, converting all these tabs to spaces would also be very useful – pasting text with tabs to various places like chat, Jira tasks etc. may or may not be a good idea, while spaces will work everywhere.) Wouldn’t it be great if Emacs had a feature to delete the unnecessary indentation and convert tabs to spaces? But wait, it’s Emacs – even if it doesn’t have such a feature, we can code it ourselves!

Writing a function to remove the common whitespace prefix from every line is probably a nice exercise, but it turns out there’s no need for it – Org-mode has the org-remove-indentation function which does exactly this!

(org-remove-indentation "  hello\n    world")

yields

hello
  world

One drawback is that it doesn’t work with tabs like I want it to do:

(insert (org-remove-indentation "  hello\n\t\tworld"))

gives

__hello
########______world

where _ denotes a space and ######## is a tab character.

Elisp has the untabify function which converts tabs to spaces, taking the tab-width variable into consideration. One minor complication with this is that it acts on a buffer, not a string. (Which is actually a good thing.) So, what we need to do is to copy the string we already have to a temporary buffer and run untabify. While at that, a glance at the source code of org-remove-indentation tells us that what we really need is org-do-remove-indentation, which does the same but on a buffer instead of on a string (and is called by org-remove-indentation after putting the string into a temp buffer).

(defun copy-snippet-deindented (begin end)
  "Copy region, untabifying and removing indentation."
  (interactive "r")
  (let ((region (buffer-substring-no-properties begin end)))
    (with-temp-buffer
      (insert region)
      (untabify (point-min) (point-max))
      (org-do-remove-indentation)
      (kill-new (buffer-string)))))

A small problem with this code is that untabify uses the tab-width setting from the temp buffer (i.e., the default one) instead of the one from the buffer the copy-snippet-deindented command was invoked. This is easy to fix using the following trick.

(defun copy-snippet-deindented (begin end)
  "Copy region, untabifying and removing indentation."
  (interactive "r")
  (let ((orig-tab-width tab-width)
	(region (buffer-substring-no-properties begin end)))
    (with-temp-buffer
      (setq tab-width orig-tab-width)
      (insert region)
      (untabify (point-min) (point-max))
      (org-do-remove-indentation)
      (kill-new (buffer-string)))))

Now it seems I have enough ways to copy text to the kill ring (or the system clipboard) that I should probably make a hydra to easily select the one I need!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode