]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | ;;; elpy-refactor.el --- Refactoring mode for Elpy |
2 | ||
3 | ;; Copyright (C) 2020 Gaby Launay | |
4 | ||
5 | ;; Author: Gaby Launay <gaby.launay@protonmail.com> | |
6 | ;; URL: https://github.com/jorgenschaefer/elpy | |
7 | ||
8 | ;; This program is free software; you can redistribute it and/or | |
9 | ;; modify it under the terms of the GNU General Public License | |
10 | ;; as published by the Free Software Foundation; either version 3 | |
11 | ;; of the License, or (at your option) any later version. | |
12 | ||
13 | ;; This program is distributed in the hope that it will be useful, | |
14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | ;; GNU General Public License for more details. | |
17 | ||
18 | ;; You should have received a copy of the GNU General Public License | |
19 | ;; along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 | ||
21 | ;;; Commentary: | |
22 | ||
23 | ;; This file provides an interface, including a major mode, to use | |
24 | ;; refactoring options provided by the Jedi library. | |
25 | ||
26 | ;;; Code: | |
27 | ||
28 | ;; We require elpy, but elpy loads us, so we shouldn't load it back. | |
29 | ;; (require 'elpy) | |
30 | (require 'diff-mode) | |
31 | ||
32 | ||
33 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
34 | ;;; Refactor mode (for applying diffs) | |
35 | ||
36 | (defvar elpy-refactor--saved-window-configuration nil | |
37 | "Saved windows configuration, so that we can restore it after `elpy-refactor' has done its thing.") | |
38 | ||
39 | (defvar elpy-refactor--saved-pos nil | |
40 | "Line and column number of the position we were at before starting refactoring.") | |
41 | ||
42 | (defvar elpy-refactor--modified-buffers '() | |
43 | "Keep track of the buffers modified by the current refactoring sessions.") | |
44 | ||
45 | (defun elpy-refactor--apply-diff (proj-path diff) | |
46 | "Apply DIFF, looking for the files in PROJ-PATH." | |
47 | (let ((current-line (line-number-at-pos (point))) | |
48 | (current-col (- (point) (line-beginning-position)))) | |
49 | (with-current-buffer (get-buffer-create " *Elpy Refactor*") | |
50 | (elpy-refactor-mode) | |
51 | (let ((inhibit-read-only t)) | |
52 | (erase-buffer) | |
53 | (insert diff)) | |
54 | (setq default-directory proj-path) | |
55 | (goto-char (point-min)) | |
56 | (elpy-refactor--apply-whole-diff)) | |
57 | (condition-case nil | |
58 | (progn | |
59 | (goto-char (point-min)) | |
60 | (forward-line (- current-line 1)) | |
61 | (beginning-of-line) | |
62 | (forward-char current-col)) | |
63 | (error)) | |
64 | )) | |
65 | ||
66 | (defun elpy-refactor--display-diff (proj-path diff) | |
67 | "Display DIFF in a `diff-mode' window. | |
68 | ||
69 | DIFF files should be relative to PROJ-PATH." | |
70 | (setq elpy-refactor--saved-window-configuration (current-window-configuration) | |
71 | elpy-refactor--saved-pos (list (line-number-at-pos (point) t) | |
72 | (- (point) (line-beginning-position))) | |
73 | elpy-refactor--modified-buffers '()) | |
74 | (with-current-buffer (get-buffer-create "*Elpy Refactor*") | |
75 | (elpy-refactor-mode) | |
76 | (let ((inhibit-read-only t)) | |
77 | (erase-buffer) | |
78 | (insert (propertize | |
79 | (substitute-command-keys | |
80 | (concat | |
81 | "\\[diff-file-next] and \\[diff-file-prev] -- Move between files\n" | |
82 | "\\[diff-hunk-next] and \\[diff-hunk-prev] -- Move between hunks\n" | |
83 | "\\[diff-split-hunk] -- Split the current hunk at point\n" | |
84 | "\\[elpy-refactor--apply-hunk] -- Apply the current hunk\n" | |
85 | "\\[diff-kill-hunk] -- Kill the current hunk\n" | |
86 | "\\[elpy-refactor--apply-whole-diff] -- Apply the whole diff\n" | |
87 | "\\[elpy-refactor--quit] -- Quit\n")) | |
88 | 'face 'bold) | |
89 | "\n\n") | |
90 | (align-regexp (point-min) (point-max) "\\(\\s-*\\) -- ") | |
91 | (goto-char (point-min)) | |
92 | (while (search-forward " -- " nil t) | |
93 | (replace-match " " nil t)) | |
94 | (goto-char (point-max)) | |
95 | (insert diff)) | |
96 | (setq default-directory proj-path) | |
97 | (goto-char (point-min)) | |
98 | (if (diff--some-hunks-p) | |
99 | (progn | |
100 | (select-window (display-buffer (current-buffer))) | |
101 | (diff-hunk-next)) | |
102 | ;; quit if not diff at all... | |
103 | (message "No differences to validate") | |
104 | (kill-buffer (current-buffer))))) | |
105 | ||
106 | (defvar elpy-refactor-mode-map | |
107 | (let ((map (make-sparse-keymap))) | |
108 | (define-key map (kbd "C-c C-c") 'elpy-refactor--apply-hunk) | |
109 | (define-key map (kbd "C-c C-a") 'elpy-refactor--apply-whole-diff) | |
110 | (define-key map (kbd "C-c C-x") 'diff-kill-hunk) | |
111 | (define-key map (kbd "q") 'elpy-refactor--quit) | |
112 | (define-key map (kbd "C-c C-k") 'elpy-refactor--quit) | |
113 | (define-key map (kbd "h") 'describe-mode) | |
114 | (define-key map (kbd "?") 'describe-mode) | |
115 | map) | |
116 | "The key map for `elpy-refactor-mode'.") | |
117 | ||
118 | (define-derived-mode elpy-refactor-mode diff-mode "Elpy Refactor" | |
119 | "Mode to display refactoring actions and ask confirmation from the user. | |
120 | ||
121 | \\{elpy-refactor-mode-map}" | |
122 | :group 'elpy | |
123 | (view-mode 1)) | |
124 | ||
125 | (defun elpy-refactor--apply-hunk () | |
126 | "Apply the current hunk." | |
127 | (interactive) | |
128 | (save-excursion | |
129 | (diff-apply-hunk)) | |
130 | ;; keep track of modified buffers | |
131 | (let ((buf (find-buffer-visiting (diff-find-file-name)))) | |
132 | (when buf | |
133 | (add-to-list 'elpy-refactor--modified-buffers buf))) | |
134 | ;; | |
135 | (diff-hunk-kill) | |
136 | (unless (diff--some-hunks-p) | |
137 | (elpy-refactor--quit))) | |
138 | ||
139 | (defun elpy-refactor--apply-whole-diff () | |
140 | "Apply the whole diff and quit." | |
141 | (interactive) | |
142 | (goto-char (point-min)) | |
143 | (diff-hunk-next) | |
144 | (while (diff--some-hunks-p) | |
145 | (let ((buf (find-buffer-visiting (diff-find-file-name)))) | |
146 | (when buf | |
147 | (add-to-list 'elpy-refactor--modified-buffers buf))) | |
148 | (condition-case nil | |
149 | (progn | |
150 | (save-excursion | |
151 | (diff-apply-hunk)) | |
152 | (diff-hunk-kill)) | |
153 | (error (diff-hunk-next)))) ;; if a hunk fail, switch to the next one | |
154 | ;; quit | |
155 | (elpy-refactor--quit)) | |
156 | ||
157 | (defun elpy-refactor--quit () | |
158 | "Quit the refactoring session." | |
159 | (interactive) | |
160 | ;; save modified buffers | |
161 | (dolist (buf elpy-refactor--modified-buffers) | |
162 | (with-current-buffer buf | |
163 | (basic-save-buffer))) | |
164 | (setq elpy-refactor--modified-buffers '()) | |
165 | ;; kill refactoring buffer | |
166 | (kill-buffer (current-buffer)) | |
167 | ;; Restore window configuration | |
168 | (when elpy-refactor--saved-window-configuration | |
169 | (set-window-configuration elpy-refactor--saved-window-configuration) | |
170 | (setq elpy-refactor--saved-window-configuration nil)) | |
171 | ;; Restore cursor position | |
172 | (when elpy-refactor--saved-pos | |
173 | (goto-char (point-min)) | |
174 | (forward-line (- (car elpy-refactor--saved-pos) 1)) | |
175 | (forward-char (car (cdr elpy-refactor--saved-pos))) | |
176 | (setq elpy-refactor--saved-pos nil))) | |
177 | ||
178 | ||
179 | ||
180 | ;;;;;;;;;;;;;;;;; | |
181 | ;; User functions | |
182 | ||
183 | (defun elpy-refactor-rename (new-name &optional dontask) | |
184 | "Rename the symbol at point to NEW-NAME. | |
185 | ||
186 | With a prefix argument (or if DONTASK is non-nil), | |
187 | do not display the diff before applying." | |
188 | (interactive (list | |
189 | (let ((old-name (thing-at-point 'symbol))) | |
190 | (if (or (not old-name) | |
191 | (not (elpy-refactor--is-valid-symbol-p old-name))) | |
192 | (error "No symbol at point") | |
193 | (read-string | |
194 | (format "New name for '%s': " | |
195 | (thing-at-point 'symbol)) | |
196 | (thing-at-point 'symbol)))))) | |
197 | (unless (and new-name | |
198 | (elpy-refactor--is-valid-symbol-p new-name)) | |
199 | (error "'%s' is not a valid python symbol")) | |
200 | (message "Gathering occurences of '%s'..." | |
201 | (thing-at-point 'symbol)) | |
202 | (let* ((elpy-rpc-timeout 10) ;; refactoring can be long... | |
203 | (diff (elpy-rpc-get-rename-diff new-name)) | |
204 | (proj-path (alist-get 'project_path diff)) | |
205 | (success (alist-get 'success diff)) | |
206 | (diff (alist-get 'diff diff))) | |
207 | (cond ((not success) | |
208 | (error "Refactoring failed for some reason")) | |
209 | ((string= success "Not available") | |
210 | (error "This functionnality needs jedi > 0.17.0, please update")) | |
211 | ((or dontask current-prefix-arg) | |
212 | (message "Replacing '%s' with '%s'..." | |
213 | (thing-at-point 'symbol) | |
214 | new-name) | |
215 | (elpy-refactor--apply-diff proj-path diff) | |
216 | (message "Done")) | |
217 | (t | |
218 | (elpy-refactor--display-diff proj-path diff))))) | |
219 | ||
220 | (defun elpy-refactor-extract-variable (new-name) | |
221 | "Extract the current region to a new variable NEW-NAME." | |
222 | (interactive "sNew name: ") | |
223 | (let ((beg (if (region-active-p) | |
224 | (region-beginning) | |
225 | (car (or (bounds-of-thing-at-point 'symbol) | |
226 | (error "No symbol at point"))))) | |
227 | (end (if (region-active-p) | |
228 | (region-end) | |
229 | (cdr (bounds-of-thing-at-point 'symbol))))) | |
230 | (when (or (elpy-refactor--is-valid-symbol-p new-name) | |
231 | (y-or-n-p "'%s' does not appear to be a valid python symbol. Are you sure you want to use it? ")) | |
232 | (let* ((line-beg (save-excursion | |
233 | (goto-char beg) | |
234 | (line-number-at-pos))) | |
235 | (line-end (save-excursion | |
236 | (goto-char end) | |
237 | (line-number-at-pos))) | |
238 | (col-beg (save-excursion | |
239 | (goto-char beg) | |
240 | (- (point) (line-beginning-position)))) | |
241 | (col-end (save-excursion | |
242 | (goto-char end) | |
243 | (- (point) (line-beginning-position)))) | |
244 | (diff (elpy-rpc-get-extract-variable-diff | |
245 | new-name line-beg line-end col-beg col-end)) | |
246 | (proj-path (alist-get 'project_path diff)) | |
247 | (success (alist-get 'success diff)) | |
248 | (diff (alist-get 'diff diff))) | |
249 | (cond ((not success) | |
250 | (error "We could not extract the selection as a variable")) | |
251 | ((string= success "Not available") | |
252 | (error "This functionnality needs jedi > 0.17.0, please update")) | |
253 | (t | |
254 | (deactivate-mark) | |
255 | (elpy-refactor--apply-diff proj-path diff))))))) | |
256 | ||
257 | (defun elpy-refactor-extract-function (new-name) | |
258 | "Extract the current region to a new function NEW-NAME." | |
259 | (interactive "sNew function name: ") | |
260 | (unless (region-active-p) | |
261 | (error "No selection")) | |
262 | (when (or (elpy-refactor--is-valid-symbol-p new-name) | |
263 | (y-or-n-p "'%s' does not appear to be a valid python symbol. Are you sure you want to use it? ")) | |
264 | (let* ((line-beg (save-excursion | |
265 | (goto-char (region-beginning)) | |
266 | (line-number-at-pos))) | |
267 | (line-end (save-excursion | |
268 | (goto-char (region-end)) | |
269 | (line-number-at-pos))) | |
270 | (col-beg (save-excursion | |
271 | (goto-char (region-beginning)) | |
272 | (- (point) (line-beginning-position)))) | |
273 | (col-end (save-excursion | |
274 | (goto-char (region-end)) | |
275 | (- (point) (line-beginning-position)))) | |
276 | (diff (elpy-rpc-get-extract-function-diff | |
277 | new-name line-beg line-end col-beg col-end)) | |
278 | (proj-path (alist-get 'project_path diff)) | |
279 | (success (alist-get 'success diff)) | |
280 | (diff (alist-get 'diff diff))) | |
281 | (cond ((not success) | |
282 | (error "We could not extract the selection as a function")) | |
283 | ((string= success "Not available") | |
284 | (error "This functionnality needs jedi > 0.17.0, please update")) | |
285 | (t | |
286 | (deactivate-mark) | |
287 | (elpy-refactor--apply-diff proj-path diff)))))) | |
288 | ||
289 | (defun elpy-refactor-inline () | |
290 | "Inline the variable at point." | |
291 | (interactive) | |
292 | (let* ((diff (elpy-rpc-get-inline-diff)) | |
293 | (proj-path (alist-get 'project_path diff)) | |
294 | (success (alist-get 'success diff)) | |
295 | (diff (alist-get 'diff diff))) | |
296 | (cond ((not success) | |
297 | (error "We could not inline the variable '%s'" | |
298 | (thing-at-point 'symbol))) | |
299 | ((string= success "Not available") | |
300 | (error "This functionnality needs jedi > 0.17.0, please update")) | |
301 | (t | |
302 | (elpy-refactor--apply-diff proj-path diff))))) | |
303 | ||
304 | ||
305 | ;;;;;;;;;;;; | |
306 | ;; Utilities | |
307 | ||
308 | (defun elpy-refactor--is-valid-symbol-p (symbol) | |
309 | "Return t if SYMBOL is a valid python symbol." | |
310 | (eq 0 (string-match "^[a-zA-Z_][a-zA-Z0-9_]*$" symbol))) | |
311 | ||
312 | ;;;;;;;;;;;; | |
313 | ;; Compatibility | |
314 | (unless (fboundp 'diff--some-hunks-p) | |
315 | (defun diff--some-hunks-p () | |
316 | (save-excursion | |
317 | (goto-char (point-min)) | |
318 | (re-search-forward diff-hunk-header-re nil t)))) | |
319 | ||
320 | (provide 'elpy-refactor) | |
321 | ;;; elpy-refactor.el ends here |