6

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 ()
    (interactive)
    (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 ()
  (interactive)
  (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))
                                    0)
                                lines))
         (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))
                                  indent-lengths-and-lines))
         (indented-lines (when (and indent-to (> indent-to 0))
                           (--map (if (s-matches? "^[[:space:]]*$" it)
                                      it
                                    (concat (s-repeat indent-to " ") it))
                                  unindented-lines)))
         (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.

Jorge Israel Peña
  • 1,265
  • 9
  • 17

2 Answers2

5

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))
    (with-temp-buffer
      (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))))))

Personally I don't find the interactive prefix argument behaviour of kill-ring-save to be useful, so I've hijacked that as an interface for calling my-copy-region-unindented via the global M-w binding, which seems convenient to me.

By default this just calls copy-region-as-kill; but with a prefix arg it instead calls my-copy-region-unindented.

In the latter case, using C-u unindents the text completely, whilst a numeric arg is passed along as a PAD argument.

(defun my-copy-region-as-kill (pad beginning end)
  "Like `copy-region-as-kill' or, with prefix arg, `my-copy-region-unindented'."
  (interactive "P\nr")
  (if pad
      (my-copy-region-unindented (if (consp pad) nil pad)
                                 beginning end)
    (copy-region-as-kill beginning end)))

(global-set-key (kbd "M-w") 'my-copy-region-as-kill)
phils
  • 48,657
  • 3
  • 76
  • 115
  • 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 01:50
2

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)