Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 lines
12 KiB

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