I often find myself copying a region of code that is deep inside the indented structure. Since I don't want it to end up in a codeblock with superfluous indentation (given that the context is missing), I find myself remembering that and having to go back to select the region, then press < a couple times (I use evil) to shift the region left until leading indentation is removed, before I go ahead and copy again, then I have to undo the indentation by pressing u an equivalent number of times.

I was therefore thinking of automating this by creating a command for it and a bind to go with it. I tried searching to see if something like this already existed (I figured it surely would) but I guess I'm not having any luck with the way I'm formulating my queries.

What I'm curious about is if there's already a command or package for this.

Short of that, if there's a reliable, consistent way to remove the "leading" indentation, which I guess means removing the minimum indentation (of all lines, potentially 0 if already not indented) from each line in a region.

For example, this:

  (defun my-dots-search ()
    (let ((helm-ag--extra-options
           "--hidden --ignore-dir .git --ignore .gitignore --ignore .projectile"))
      (helm-do-ag my-dots-path)))

That has 2 spaces of leading indentation, determined by mapping each line to its leading indentation and taking the minimum of that. So removing that leading indentation would involve removing 2 spaces from each line, to arrive at this:

(defun my-dots-search ()
  (let ((helm-ag--extra-options
         "--hidden --ignore-dir .git --ignore .gitignore --ignore .projectile"))
    (helm-do-ag my-dots-path)))

If there isn't a function for this, I guess I could write it myself, so I would appreciate some pointers on things to look out for that I may be overlooking.

I imagine I would call such a function within a save-excursion to easily "undo" things once I'm done copying the region.

EDIT: This is my rough, first pass. I'm wondering if I'm making it unnecessarily complicated, doing something wrong, or if there's something I'm overlooking.

(defun my--copy-without-leading-indentation (region &optional indent-to)
  (let* ((lines (s-lines region))
         (indent-lengths (--map (or (cdar (s-matched-positions-all "^[[:space:]]+" it))
         (indent-lengths-and-lines (-zip indent-lengths lines))
         (non-empty-line-indents (--map (car it) (--filter (s-present? (cdr it)) indent-lengths-and-lines)))
         (min-indent (if non-empty-line-indents (-min non-empty-line-indents) 0))
         (unindented-lines (--map (if (or (= min-indent 0) (= (car it) 0))
                                      (cdr it)
                                    (substring (cdr it) min-indent))
         (indented-lines (when (and indent-to (> indent-to 0))
                           (--map (if (s-matches? "^[[:space:]]*$" it)
                                    (concat (s-repeat indent-to " ") it))
         (resulting-lines (or indented-lines unindented-lines))
         (joined (s-join "\n" resulting-lines)))
    (kill-new joined)))

(defun my-copy-without-leading-indentation (arg start end)
  "Copy the region without any leading indentation.

With argument, after removing leading indentation, indent to 4 spaces.
This is useful for Markdown codeblocks, for example."
  (interactive "P\nr")

  (let ((region (filter-buffer-substring start end)))
    (my--copy-without-leading-indentation region (when arg 4))))

When given the prefix argument it indents by 4 spaces, useful for unfenced markdown codeblocks. I'm gonna try to polish and clean it up, and maybe optimize it some more.


You basically just want to be calling indent-rigidly on a copy of the region. That will also deal nicely with indent-tabs-mode (which I think you'll find your version does not).

I'd suggest using a temporary buffer, and maintaining the original values for indent-tabs-mode and tab-width.

Something like this:

(defun my-copy-region-unindented (pad beginning end)
  "Copy the region, un-indented by the length of its minimum indent.

If numeric prefix argument PAD is supplied, indent the resulting
text by that amount."
  (interactive "P\nr")
  (let ((buf (current-buffer))
        (itm indent-tabs-mode)
        (tw tab-width)
        (st (syntax-table))
        (indent nil))
      (setq indent-tabs-mode itm
            tab-width tw)
      (set-syntax-table st)
      (insert-buffer-substring buf beginning end)
      ;; Establish the minimum level of indentation.
      (goto-char (point-min))
      (while (and (re-search-forward "^[[:space:]\n]*" nil :noerror)
                  (not (eobp)))
        (let ((length (current-column)))
          (when (or (not indent) (< length indent))
            (setq indent length)))
        (forward-line 1))
      (if (not indent)
          (error "Region is entirely whitespace")
        ;; Un-indent the buffer contents by the length of the minimum
        ;; indent level, and copy to the kill ring.
        (when pad
          (setq indent (- indent (prefix-numeric-value pad))))
        (indent-rigidly (point-min) (point-max) (- indent))
        (copy-region-as-kill (point-min) (point-max))))))
  • That works beautifully! The one thing I think I picked up on while working on mine was to use filter-buffer-substring instead to avoid copying characters that shouldn't be copied. Thanks for this, I hope to learn from it. – Jorge Israel Peña Aug 18 '17 at 20:34
  • Oh I see that copy-region-as-kill already calls filter-buffer-substring, so maybe it's not necessary to do it early on? Maybe it might even be incorrect to do so? – Jorge Israel Peña Aug 18 '17 at 20:38
  • I can't think of a particular reason to call that explicitly, but tbh I'd need to look up some scenarios in which any filtering actually takes place. – phils Aug 19 '17 at 1:50

Use rectangle commands:

  • C-x r M-w Save the text of the region-rectangle as the last killed rectangle (copy-rectangle-as-kill)
  • C-x r y Yank the last killed rectangle with its upper left corner at point (yank-rectangle)

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.