Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Dima-369 opened this issue Sep 24, 2023 · 4 comments

Comments

@Dima-369
Copy link

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:

CleanShot.2023-09-24.at.20.15.24.mp4

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

CleanShot.2023-09-24.at.20.16.40.mp4

Using my code below, it behaves identical to PyCharm:

CleanShot.2023-09-24.at.20.19.04.mp4

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)))))
@mickeynp
Copy link
Owner

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.

@Dima-369
Copy link
Author

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.

@dvzubarev
Copy link

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.

@mickeynp
Copy link
Owner

mickeynp commented Feb 9, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants