File: elpy-refactor.el

package info (click to toggle)
elpy 1.34.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, sid
  • size: 2,512 kB
  • sloc: lisp: 11,667; python: 3,012; makefile: 167; sh: 60
file content (297 lines) | stat: -rw-r--r-- 11,553 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
;;; elpy-refactor.el --- Refactoring mode for Elpy

;; Copyright (C) 2013-2019  Jorgen Schaefer

;; Author: Jorgen Schaefer <contact@jorgenschaefer.de>
;; URL: https://github.com/jorgenschaefer/elpy

;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This file provides an interface, including a major mode, to use
;; refactoring options provided by the Rope library.

;;; Code:

;; We require elpy, but elpy loads us, so we shouldn't load it back.
;; (require 'elpy)

(defvar elpy-refactor-changes nil
  "Changes that will be commited on \\[elpy-refactor-commit].")
(make-variable-buffer-local 'elpy-refactor-current-changes)

(defvar elpy-refactor-window-configuration nil
  "The old window configuration. Will be restored after commit.")
(make-variable-buffer-local 'elpy-refactor-window-configuration)

(make-obsolete
 'elpy-refactor
 "Refactoring has been unstable and flakey, support will be dropped in the future."
 "elpy 1.5.0")
(defun elpy-refactor ()
  "Run the Elpy refactoring interface for Python code."
  (interactive)
  (save-some-buffers)
  (let* ((selection (elpy-refactor-select
                     (elpy-refactor-rpc-get-options)))
         (method (car selection))
         (args (cdr selection)))
    (when method
      (elpy-refactor-create-change-buffer
       (elpy-refactor-rpc-get-changes method args)))))

(defun elpy-refactor-select (options)
  "Show the user the refactoring options and let her choose one.

Depending on the chosen option, ask the user for further
arguments and build the argument.

Return a cons cell of the name of the option and the arg list
created."
  (let ((buf (get-buffer-create "*Elpy Refactor*"))
        (pos (vector (1- (point))
                     (ignore-errors
                       (1- (region-beginning)))
                     (ignore-errors
                       (1- (region-end)))))
        (inhibit-read-only t)
        (options (sort options
                       (lambda (a b)
                         (let ((cata (cdr (assq 'category a)))
                               (catb (cdr (assq 'category b))))
                           (if (equal cata catb)
                               (string< (cdr (assq 'description a))
                                        (cdr (assq 'description b)))
                             (string< cata catb))))))
        (key ?a)
        last-category
        option-alist)
    (with-current-buffer buf
      (erase-buffer)
      (dolist (option options)
        (let ((category (cdr (assq 'category option)))
              (description (cdr (assq 'description option)))
              (name (cdr (assq 'name option)))
              (args (cdr (assq 'args option))))
          (unless (equal category last-category)
            (when last-category
              (insert "\n"))
            (insert (propertize category 'face 'bold) "\n")
            (setq last-category category))
          (insert " (" key ") " description "\n")
          (setq option-alist (cons (list key name args)
                                   option-alist))
          (setq key (1+ key))))
      (let ((window-conf (current-window-configuration)))
        (unwind-protect
            (progn
              (with-selected-window (display-buffer buf)
                (goto-char (point-min)))
              (fit-window-to-buffer (get-buffer-window buf))
              (let* ((key (read-key "Refactoring action? "))
                     (entry (cdr (assoc key option-alist))))
                (kill-buffer buf)
                (cons (car entry)       ; name
                      (elpy-refactor-build-arguments (cadr entry)
                                                     pos))))
          (set-window-configuration window-conf))))))

(defun elpy-refactor-build-arguments (args pos)
  "Translate an argument list specification to an argument list.

POS is a vector of three elements, the current offset, the offset
of the beginning of the region, and the offset of the end of the
region.

ARGS is a list of triples, each triple containing the name of an
argument (ignored), the type of the argument, and a possible
prompt string.

Available types:

  offset       - The offset in the buffer, (1- (point))
  start_offset - Offset of the beginning of the region
  end_offset   - Offset of the end of the region
  string       - A free-form string
  filename     - A non-existing file name
  directory    - An existing directory name
  boolean      - A boolean question"
  (mapcar (lambda (arg)
            (let ((type (cadr arg))
                  (prompt (cl-caddr arg)))
              (cond
               ((equal type "offset")
                (aref pos 0))
               ((equal type "start_offset")
                (aref pos 1))
               ((equal type "end_offset")
                (aref pos 2))
               ((equal type "string")
                (read-from-minibuffer prompt))
               ((equal type "filename")
                (expand-file-name
                 (read-file-name prompt)))
               ((equal type "directory")
                (expand-file-name
                 (read-directory-name prompt)))
               ((equal type "boolean")
                (y-or-n-p prompt)))))
          args))

(defun elpy-refactor-create-change-buffer (changes)
  "Show the user a buffer of changes.

The user can review the changes and confirm them with
\\[elpy-refactor-commit]."
  (unless changes
    (error "No changes for this refactoring action."))
  (with-current-buffer (get-buffer-create "*Elpy Refactor*")
    (elpy-refactor-mode)
    (setq elpy-refactor-changes changes
          elpy-refactor-window-configuration (current-window-configuration))
    (let ((inhibit-read-only t))
      (erase-buffer)
      (elpy-refactor-insert-changes changes))
    (select-window (display-buffer (current-buffer)))
    (goto-char (point-min))))

(defun elpy-refactor-insert-changes (changes)
  "Format and display the changes described in CHANGES."
  (insert (propertize "Use C-c C-c to apply the following changes."
                      'face 'bold)
          "\n\n")
  (dolist (change changes)
    (let ((action (cdr (assq 'action change))))
      (cond
       ((equal action "change")
        (insert (cdr (assq 'diff change))
                "\n"))
       ((equal action "create")
        (let ((type (cdr (assq 'type change))))
          (if (equal type "file")
              (insert "+++ " (cdr (assq 'file change)) "\n"
                      "Create file " (cdr (assq 'file change)) "\n"
                      "\n")
            (insert "+++ " (cdr (assq 'path change)) "\n"
                    "Create directory " (cdr (assq 'path change)) "\n"
                    "\n"))))
       ((equal action "move")
        (insert "--- " (cdr (assq 'source change)) "\n"
                "+++ " (cdr (assq 'destination change)) "\n"
                "Rename " (cdr (assq 'type change)) "\n"
                "\n"))
       ((equal action "delete")
        (let ((type (cdr (assq 'type change))))
          (if (equal type "file")
              (insert "--- " (cdr (assq 'file change)) "\n"
                      "Delete file " (cdr (assq 'file change)) "\n"
                      "\n")
            (insert "--- " (cdr (assq 'path change)) "\n"
                    "Delete directory " (cdr (assq 'path change)) "\n"
                    "\n"))))))))

(defvar elpy-refactor-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") 'elpy-refactor-commit)
    (define-key map (kbd "q") 'bury-buffer)
    (define-key map (kbd "h") 'describe-mode)
    (define-key map (kbd "?") 'describe-mode)
    map)
  "The key map for `elpy-refactor-mode'.")

(define-derived-mode elpy-refactor-mode diff-mode "Elpy Refactor"
  "Mode to display refactoring actions and ask confirmation from the user.

\\{elpy-refactor-mode-map}"
  :group 'elpy
  (view-mode 1))

(defun elpy-refactor-commit ()
  "Commit the changes in the current buffer."
  (interactive)
  (unless elpy-refactor-changes
    (error "No changes to commit."))
  ;; Restore the window configuration as the first thing so that
  ;; changes below are visible to the user. Especially the point
  ;; change in possible buffer changes.
  (set-window-configuration elpy-refactor-window-configuration)
  (dolist (change elpy-refactor-changes)
    (let ((action (cdr (assq 'action change))))
      (cond
       ((equal action "change")
        (with-current-buffer (find-file-noselect (cdr (assq 'file change)))
          ;; This would break for save-excursion as the buffer is
          ;; truncated, so all markets now point to position 1.
          (let ((old-point (point)))
            (undo-boundary)
            (erase-buffer)
            (insert (cdr (assq 'contents change)))
            (undo-boundary)
            (goto-char old-point))))
       ((equal action "create")
        (if (equal (cdr (assq 'type change))
                   "file")
            (find-file-noselect (cdr (assq 'file change)))
          (make-directory (cdr (assq 'path change)))))
       ((equal action "move")
        (let* ((source (cdr (assq 'source change)))
               (dest (cdr (assq 'destination change)))
               (buf (get-file-buffer source)))
          (when buf
            (with-current-buffer buf
              (setq buffer-file-name dest)
              (rename-buffer (file-name-nondirectory dest) t)))
          (rename-file source dest)))
       ((equal action "delete")
        (if (equal (cdr (assq 'type change)) "file")
            (let ((name (cdr (assq 'file change))))
              (when (y-or-n-p (format "Really delete %s? " name))
                (delete-file name t)))
          (let ((name (cdr (assq 'directory change))))
            (when (y-or-n-p (format "Really delete %s? " name))
              (delete-directory name nil t))))))))
  (kill-buffer (current-buffer)))

(defun elpy-refactor-rpc-get-options ()
  "Get a list of refactoring options from the Elpy RPC."
  (if (use-region-p)
      (elpy-rpc "get_refactor_options"
                (list (buffer-file-name)
                      (1- (region-beginning))
                      (1- (region-end))))
    (elpy-rpc "get_refactor_options"
              (list (buffer-file-name)
                    (1- (point))))))

(defun elpy-refactor-rpc-get-changes (method args)
  "Get a list of changes from the Elpy RPC after applying METHOD with ARGS."
  (elpy-rpc "refactor"
            (list (buffer-file-name)
                  method args)))

(defun elpy-refactor-options (option)
  "Show available refactor options and let user choose one."
  (interactive "c[i]: importmagic-fixup [p]: autopep8-fix-code [r]: refactor")
  (let ((choice (char-to-string option)))
    (cond
     ((string-equal choice "i")
      (elpy-importmagic-fixup))
     ((string-equal choice "p")
      (elpy-autopep8-fix-code))
     ((string-equal choice "r")
      (elpy-refactor)))))

(provide 'elpy-refactor)
;;; elpy-refactor.el ends here