;;; elpy-refactor.el --- Refactoring mode for Elpy
|
|
|
|
;; Copyright (C) 2013-2016 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))))
|
|
(when (not (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]."
|
|
(when (not 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)
|
|
(when (not 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
|