combobulate icon indicating copy to clipboard operation
combobulate copied to clipboard

Would it be possible to support a 'Smart indent on yank' feature (at least for Python)?

Open Dima-369 opened this issue 9 months ago • 4 comments

Thanks for this library! I really enjoy editing Python code with it. One 'killer' feature of PyCharm I am very used to, is its 'Smart Indent pasted lines' feature.

Here is a video demonstrating its behavior in PyCharm:

https://github.com/mickeynp/combobulate/assets/15002298/55ae3fe5-d2a5-45e7-a410-a807cc224b7f

Here is how Emacs does it by default on a yank (no fun, one usually has to manually reindent stuff):

https://github.com/mickeynp/combobulate/assets/15002298/9eea768b-10fe-49f2-a0b0-fadf09e03064

Using my code below, it behaves identical to PyCharm:

https://github.com/mickeynp/combobulate/assets/15002298/4cd0cad4-2f64-412f-a1cf-804565a0e54a


I thought a bit about it and came up with those functions to mimick its behavior (it's not perfect, but works fine for me so far).

@mickeynp since you are quite experienced with Python, how do you approach this issue? What do you think of this approach? Is there an easier solution to this? I don't know if this can even be implemented based on the treesit node structures and then indent the code based on that (but this would be a lot of work I think).

The code checks if the first line ends with a : then tries to best-guess apply indents based on that. dima-python-indent-for-current-line indents the entire pasted code block based on the (current-indentation).

(defun dima-count-prefix-spaces (s)
  "Count the common number of spaces in the prefix whitespace of each line in STRING."
  (-min
   (--keep
    (unless (s-blank-p it)
      (with-temp-buffer
        (insert it)
        (current-indentation)))
    (s-lines s))))

(defun dima-python-indent-adjust-left (text)
  "Return TEXT with spaces stripped at start if indent is the same.

Note that TEXT needs to be valid Python, just not indented correctly
at the left.

Take care that TEXT does not end with an empty line."
  (let ((n (dima-count-prefix-spaces text)))
    (if (= 0 n)
        text
      (thread-last (s-lines text)
                   (--map
                    (s-chop-prefix (s-repeat n " ") it))
                   (s-join "\n")))))

(defun dima-python-indent-fix-first-line (string)
  "Align STRING so it is correct Python, just not indented correctly at the left."
  (interactive)
  (let* ((lines (s-lines string))
         (ends-colon-p (s-ends-with-p ":" (cl-first lines))))

    (if ends-colon-p
        (let ((second-line-indent (with-temp-buffer
                                    (insert (cl-second lines))
                                    (max 0 (- (current-indentation) 4)))))
          (concat
           (s-repeat second-line-indent " ")
           (cl-first lines)
           "\n"
           (s-join "\n" (-drop 1 lines))))
      string)))

(defun dima-python-indent-for-current-line (string current-indent)
  "Return STRING with CURRENT-INDENT applied on all lines."
  (cond
   ((= 0 current-indent)
    string)

   (t
    (let ((lines (s-lines string)))
      ;; leave indentation for first line
      (concat
       (cl-first lines)
       "\n"
       (s-join "\n"
               (--map
                (concat (s-repeat current-indent " ") it)
                (cdr lines))))))))

(defun dima-python-ident-paste ()
  "Paste to current point with hopefully fixed indentation."
  (interactive)
  (when (region-active-p)
    (delete-region (region-beginning) (region-end)))
  (let* ((to (get-clipboard))
         (lines (s-lines to))
         (first-line-no-indent-p (= 0 (with-temp-buffer
                                        (insert (cl-first lines))
                                        (current-indentation)))))
    (insert
     (dima-python-indent-for-current-line
      (dima-python-indent-adjust-left
       (if first-line-no-indent-p
           (dima-python-indent-fix-first-line to)
         to))
      (current-indentation)))))

Dima-369 avatar Sep 24 '23 18:09 Dima-369

Nice work. python-mode already has code to determine the right indentation level. It should also work for things like indentation inside lists and so on. python-calculate-levels I think it is. You can look at how Combobulate does block indentation for examples.

The hard part is turning a region into a coherent one that has the correct indentation for the first line. There's a bunch of overly complicated code in combobulate that tries to do this: combobulate-indent-string-first-line, combobulate-indent-string, combobulate-extend-region-to-whole-lines, etc. Messy.

That might give you some ideas.

mickeynp avatar Sep 25 '23 20:09 mickeynp

Messy.

That sums it up, yup 😄

Thanks, I'll check out your suggestions. To me it would be amazing to see an interactive combobulate-python-yank-and-indent function implemented here, but just yesterday I also found a indentation bug with my code above.

Dima-369 avatar Sep 26 '23 06:09 Dima-369

I stumbled upon this problem too, while working on a similar package. I came up with a solution that works for text blocks, where the first line is defining indentation of all other lines It means that first line should have indentation level that is equal or less than other lines in the text block.

The basic idea is to preserve in kill ring indentation of the first line, since it is crucial for proper indentation of the whole block. When we have properly indented block of text in kill ring, all we need is to use indent-rigidly on this block and change its indentation so it matches current indentation at the insertion point.

I have some code in my config and evil-ts-obj-util--indent-text-according-to-point-pos is a function from my package that is similar to combobulate.

With this code you can copy/paste code blocks that not start with spaces preserving its indentations. For example, in

this_is = func(1, |temp={'1':1,
                        '2':2})

for temp argument first line indentation will be preserved.

dvzubarev avatar Feb 08 '24 17:02 dvzubarev

I've made a large number of simplifications to this in development, which I am due to finish merging some time next week. The problem is ensuring, as you say, the first line is properly indented relative to the other lines, and then adjusting indentation to match point when you yank.

mickeynp avatar Feb 09 '24 06:02 mickeynp