├── .gitignore ├── Cask ├── README.md ├── features ├── convert_post_condition.feature ├── example.rb ├── extract_constant.feature ├── extract_local_variable.feature ├── extract_method.feature ├── extract_to_let.feature ├── step-definitions │ └── ruby-refactor-steps.el └── support │ └── env.el └── ruby-refactor.el /.gitignore: -------------------------------------------------------------------------------- 1 | .cask 2 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package "ruby-refactor" "0.1" "Refactoring ruby") 5 | 6 | (development 7 | (depends-on "f") 8 | (depends-on "ecukes")) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ruby-refactor.el 2 | 3 | Ruby refactor is inspired by the Vim plugin vim-refactoring-ruby, currently found at https://github.com/ecomba/vim-ruby-refactoring. 4 | 5 | These are the refactorings available 6 | - Extract to Method (C-c C-r e) 7 | - Extract Local Variable (C-c C-r v) 8 | - Extract Constant (C-c C-r c) 9 | - Add Parameter (C-c C-r p) 10 | - Extract to Let (C-c C-r l) 11 | - Convert Post Conditional (C-c C-r o) 12 | 13 | # Install 14 | The recommended way to install `ruby-refactor` is from Marmalade or [MELPA](https://melpa.org/). 15 | 16 | ``` 17 | M-x package-install RET ruby-refactor 18 | ``` 19 | 20 | To install manually, add ruby-refactor.el to your load path, then: 21 | 22 | ```lisp 23 | (require 'ruby-refactor) 24 | ``` 25 | 26 | In both cases, you must enable `ruby-refactor-minor-mode` in `ruby-mode`: 27 | 28 | ```lisp 29 | (add-hook 'ruby-mode-hook 'ruby-refactor-mode-launch) 30 | ``` 31 | 32 | # Usage 33 | 34 | ## Extract to Method: 35 | Select a region of text and invoke `ruby-refactor-extract-to-method`. 36 | You'll be prompted for a method name and a new argument list. If your 37 | extracted method does not take parameters, leave it empty. The method 38 | will be created above the method you are in with the method contents 39 | being the selected region. The region will be replaced with a call to 40 | the method. 41 | 42 | ## Extract Local Variable: 43 | Select a region of text and invoke `ruby-refactor-extract-local-variable`. 44 | You'll be prompted for a variable name. The new variable will 45 | be created directly above the selected region and the region 46 | will be replaced with the variable. 47 | 48 | ## Extract Constant: 49 | Select a region of text and invoke `ruby-refactor-extract-constant`. 50 | You'll be prompted for a constant name. The new constant will 51 | be created at the top of the enclosing class or module directly 52 | after any include or extend statements and the regions will be 53 | replaced with the constant. 54 | 55 | ## Add Parameter: 56 | `ruby-refactor-add-parameter` 57 | This simply prompts you for a parameter to add to the current 58 | method definition. If you are on a text, you can just hit enter 59 | as it will use it by default. You can set `ruby-refactor-add-parens` 60 | if you like parens on your params list. Default values and the 61 | like shouldn't confuse it. 62 | 63 | ## Extract to Let: 64 | This is really for use with RSpec 65 | 66 | `ruby-refactor-extract-to-let` 67 | There is a variable for where the 'let' gets placed. It can be 68 | "top" which is top-most in the file, or "closest" which just 69 | walks up to the first describe/context it finds. 70 | You can also specify a different regex, so that you can just 71 | use "describe" if you want. 72 | If you are on a line: 73 | 74 | ```ruby 75 | a = Something.else.doing 76 | ``` 77 | 78 | becomes 79 | 80 | ```ruby 81 | let(:a){ Something.else.doing } 82 | ``` 83 | 84 | If you are selecting a region: 85 | 86 | ```ruby 87 | a = Something.else 88 | a.stub(:blah) 89 | ``` 90 | 91 | becomes 92 | 93 | ```ruby 94 | let :a do 95 | _a = Something.else 96 | _a.stub(:blah) 97 | _a 98 | end 99 | ``` 100 | 101 | In both cases, you need the line, first line to have an ` = ` in it, 102 | as that drives conversion. 103 | 104 | There is also the bonus that the let will be placed *after* any other 105 | let statements. It appends it to bottom of the list. 106 | 107 | Oh, if you invoke with a prefix arg (`C-u`, etc.), it'll swap the placement 108 | of the let. If you have location as top, a prefix argument will place 109 | it closest. I kinda got nutty with this one. 110 | 111 | ## Convert Post Conditional: 112 | Select a region of text and invoke `ruby-refactor-convert-post-conditional`. 113 | This simply moves the expression inside of an 'if' or 'unless' block. 114 | 115 | So this: 116 | 117 | ```ruby 118 | do_some_stuff('blah') if condition 119 | ``` 120 | 121 | becomes 122 | 123 | ```ruby 124 | if condition 125 | do_some_stuff('blah') 126 | end 127 | ``` 128 | 129 | 130 | ## TODO 131 | From the vim plugin, these remain to be done (I don't plan to do them all.) 132 | - remove inline temp (sexy!) 133 | 134 | ## How to contribute 135 | The first thing you'll need to do is to get your tests passing. The tests depend on [Cask](http://cask.github.io/index.html), [ecukes](https://github.com/ecukes/ecukes), and [espuds](https://github.com/ecukes/espuds). Cask is sort of like bundler for emacs and ecukes is basically cucumber for emacs. 136 | 137 | To get started, install the necessary components: 138 | 139 | ~$ brew install cask 140 | ~$ cask 141 | 142 | And run your tests (which should be green): 143 | 144 | ~$ cask exec ecukes 145 | 146 | Tests are live in the `features/` directory. 147 | 148 | # License 149 | Copyright (C) 2013 Andrew J Vargo 150 | 151 | Authors: Andrew J Vargo , Jeff Morgan 152 | 153 | This program is free software; you can redistribute it and/or modify 154 | it under the terms of the GNU General Public License as published by 155 | the Free Software Foundation, either version 3 of the License, or 156 | (at your option) any later version. 157 | 158 | This program is distributed in the hope that it will be useful, 159 | but WITHOUT ANY WARRANTY; without even the implied warranty of 160 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 161 | GNU General Public License for more details. 162 | 163 | You should have received a copy of the GNU General Public License 164 | along with this program. If not, see . 165 | -------------------------------------------------------------------------------- /features/convert_post_condition.feature: -------------------------------------------------------------------------------- 1 | Feature: The ruby-refactor-convert-post-conditional function 2 | 3 | Background: 4 | Given I have loaded my example Ruby file 5 | And I turn on ruby-mode 6 | And I turn on ruby-refactor-mode 7 | 8 | Scenario: Should convert post conditional 9 | When I select "some are greater" 10 | And I start an action chain 11 | And I press "C-c C-r o" 12 | And I execute the action chain 13 | Then I should see: 14 | """ 15 | unless long_method.empty? 16 | puts "some are greater than 3" 17 | end 18 | """ -------------------------------------------------------------------------------- /features/example.rb: -------------------------------------------------------------------------------- 1 | module Outer 2 | module Inner 3 | class MyClass 4 | attr_reader :first, :last 5 | 6 | def initialize(something) 7 | @something = something 8 | end 9 | 10 | def long_method 11 | [1, 2, 3, 4, 5].each do |value| 12 | puts value 13 | end 14 | 15 | greater_than_three = [1, 2, 3, 4, 5].find_all do |value| 16 | value > 3 17 | end 18 | 19 | greater_than_three.each do |value| 20 | puts "#{value} is greater than three" 21 | end 22 | end 23 | 24 | def has_great_than_three? 25 | puts "some are greater than 3" unless long_method.empty? 26 | end 27 | end 28 | end 29 | end 30 | 31 | 32 | describe "TestSubject" do 33 | 34 | context "the first context" do 35 | before(:each) do 36 | mock_advertiser_one = mock_model(Advertiser) 37 | mock_advertiser_one.stub(:uid).and_return("UID1") 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /features/extract_constant.feature: -------------------------------------------------------------------------------- 1 | Feature: The ruby-refactor-extract-constant function 2 | 3 | Background: 4 | Given I have loaded my example Ruby file 5 | And I turn on ruby-mode 6 | And I turn on ruby-refactor-mode 7 | 8 | Scenario: Should extract a constant 9 | When I select "\"#{value} is greater than three\"" 10 | And I start an action chain 11 | And I press "C-c C-r c" 12 | And I type "FOO_CONST" 13 | And I execute the action chain 14 | Then I should see: 15 | """ 16 | module Inner 17 | class MyClass 18 | 19 | FOO_CONST = "#{value} is greater than three" 20 | 21 | attr_reader 22 | """ 23 | -------------------------------------------------------------------------------- /features/extract_local_variable.feature: -------------------------------------------------------------------------------- 1 | Feature: The ruby-refactor-extract-local-variable function 2 | 3 | Background: 4 | Given I have loaded my example Ruby file 5 | And I turn on ruby-mode 6 | And I turn on ruby-refactor-mode 7 | 8 | Scenario: Should extract a local variable 9 | When I select "\"#{value} is greater than three\"" 10 | And I start an action chain 11 | And I press "C-c C-r v" 12 | And I type "foo_var" 13 | And I execute the action chain 14 | Then I should see: 15 | """ 16 | greater_than_three.each do |value| 17 | foo_var = "#{value} is greater than three" 18 | puts foo_var 19 | end 20 | """ 21 | -------------------------------------------------------------------------------- /features/extract_method.feature: -------------------------------------------------------------------------------- 1 | Feature: The ruby-refactor-extract-to_method function 2 | 3 | Background: 4 | Given I have loaded my example Ruby file 5 | And I turn on ruby-mode 6 | And I turn on ruby-refactor-mode 7 | 8 | Scenario: Extracted method should have proper indentation 9 | When I select "puts \"#{value} is greater than three\"" 10 | And I start an action chain 11 | And I press "C-c C-r e" 12 | And I type "the_new_method" 13 | And I execute the action chain 14 | Then I should see: 15 | """ 16 | def the_new_method 17 | puts "#{value} is greater than three" 18 | end 19 | """ 20 | 21 | 22 | Scenario: Extracted method should wrap next line when the regions ends with newline 23 | When I select: 24 | """ 25 | greater_than_three.each do |value| 26 | puts "#{value} is greater than three" 27 | end 28 | 29 | """ 30 | And I start an action chain 31 | And I press "C-c C-r e" 32 | And I type "the_new_method" 33 | And I execute the action chain 34 | Then I should see: 35 | """ 36 | 37 | the_new_method 38 | end 39 | """ 40 | -------------------------------------------------------------------------------- /features/extract_to_let.feature: -------------------------------------------------------------------------------- 1 | Feature: The ruby-refactor-extract-to_let function 2 | 3 | Background: 4 | Given I have loaded my example Ruby file 5 | And I turn on ruby-mode 6 | And I turn on ruby-refactor-mode 7 | 8 | Scenario: Should not strip trailing parenthesis 9 | When I select: 10 | """ 11 | mock_advertiser_one = mock_model(Advertiser) 12 | mock_advertiser_one.stub(:uid).and_return("UID1") 13 | """ 14 | And I press "C-c C-r l" 15 | Then I should see: 16 | """ 17 | let :mock_advertiser_one do 18 | mock_advertiser_one = mock_model(Advertiser) 19 | mock_advertiser_one.stub(:uid).and_return("UID1") 20 | mock_advertiser_one 21 | end 22 | """ 23 | 24 | Scenario: Should have newline after let with extracting line 25 | When I go to the front of the word "mock_model" 26 | And I press "C-c C-r l" 27 | Then I should see: 28 | """ 29 | let(:mock_advertiser_one){ mock_model(Advertiser) } 30 | 31 | context "the first context" do 32 | """ 33 | -------------------------------------------------------------------------------- /features/step-definitions/ruby-refactor-steps.el: -------------------------------------------------------------------------------- 1 | 2 | (When "^I go to character \"\\(.+\\)\"$" 3 | (lambda (char) 4 | (goto-char (point-min)) 5 | (let ((search (re-search-forward (format "%s" char) nil t)) 6 | (message "Can not go to character '%s' since it does not exist in the current buffer: %s")) 7 | (cl-assert search nil message char (espuds-buffer-contents))))) 8 | 9 | (When "^I go to the \\(front\\|end\\) of the word \"\\(.+\\)\"$" 10 | (lambda (pos word) 11 | (goto-char (point-min)) 12 | (let ((search (re-search-forward (format "%s" word) nil t)) 13 | (message "Can not go to character '%s' since it does not exist in the current buffer: %s")) 14 | (cl-assert search nil message word (espuds-buffer-contents)) 15 | (if (string-equal "front" pos) (backward-word))))) 16 | 17 | 18 | (Given "^I have loaded my example Ruby file$" 19 | (lambda () 20 | (insert-file-contents "features/example.rb") 21 | )) 22 | 23 | (When "^I select:$" 24 | (lambda (text) 25 | (goto-char (point-min)) 26 | (let ((search (re-search-forward text nil t))) 27 | (cl-assert search nil "The text '%s' was not found in the current buffer." text)) 28 | (set-mark (point)) 29 | (re-search-backward text))) 30 | -------------------------------------------------------------------------------- /features/support/env.el: -------------------------------------------------------------------------------- 1 | (let* ((current-directory (file-name-directory load-file-name)) 2 | (features-directory (expand-file-name ".." current-directory)) 3 | (project-directory (expand-file-name ".." features-directory))) 4 | (setq ruby-refactor-root-path project-directory)) 5 | 6 | (add-to-list 'load-path ruby-refactor-root-path) 7 | 8 | (require 'ruby-refactor) 9 | (require 'espuds) 10 | (require 'ert) 11 | 12 | (Before 13 | (global-set-key (kbd "C-c C-r e") 'ruby-refactor-extract-to-method) 14 | (global-set-key (kbd "C-c C-r p") 'ruby-refactor-add-parameter) 15 | (global-set-key (kbd "C-c C-r l") 'ruby-refactor-extract-to-let) 16 | (global-set-key (kbd "C-c C-r v") 'ruby-refactor-extract-local-variable) 17 | (global-set-key (kbd "C-c C-r c") 'ruby-refactor-extract-constant) 18 | (switch-to-buffer 19 | (get-buffer-create "*ruby-refactor*")) 20 | (erase-buffer) 21 | (fundamental-mode) 22 | (transient-mark-mode 1) 23 | (cua-mode 0) 24 | (setq set-mark-default-inactive nil) 25 | (deactivate-mark)) 26 | 27 | (After) 28 | -------------------------------------------------------------------------------- /ruby-refactor.el: -------------------------------------------------------------------------------- 1 | ;;; ruby-refactor.el --- A minor mode which presents various Ruby refactoring helpers. 2 | 3 | ;; Copyright (C) 2013 Andrew J Vargo 4 | 5 | ;; Authors: Andrew J Vargo , Jeff Morgan 6 | ;; Keywords: refactor ruby 7 | ;; Version: 0.1 8 | ;; URL: https://github.com/ajvargo/ruby-refactor 9 | ;; Package-Requires: ((ruby-mode "1.2")) 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; Ruby refactor is inspired by the Vim plugin vim-refactoring-ruby, 27 | ;; currently found at https://github.com/ecomba/vim-ruby-refactoring. 28 | 29 | ;; I've implemented 5 refactorings 30 | ;; - Extract to Method 31 | ;; - Extract Local Variable 32 | ;; - Extract Constant 33 | ;; - Add Parameter 34 | ;; - Extract to Let 35 | 36 | ; ## Install 37 | ;; Add this file to your load path. 38 | ;; (require 'ruby-refactor) ; if not installed from a package 39 | ;; (add-hook 'ruby-mode-hook 'ruby-refactor-mode-launch) 40 | 41 | ;; ## Extract to Method: 42 | ;; Select a region of text and invoke 'ruby-refactor-extract-to-method'. 43 | ;; You'll be prompted for a method name. The method will be created 44 | ;; above the method you are in with the method contents being the 45 | ;; selected region. The region will be replaced w/ a call to method. 46 | 47 | ;; ## Extract Local Variable: 48 | ;; Select a region of text and invoke `ruby-refactor-extract-local-variable`. 49 | ;; You'll be prompted for a variable name. The new variable will 50 | ;; be created directly above the selected region and the region 51 | ;; will be replaced with the variable. 52 | 53 | ;; ## Extract Constant: 54 | ;; Select a region of text and invoke `ruby-refactor-extract-constant`. 55 | ;; You'll be prompted for a constant name. The new constant will 56 | ;; be created at the top of the enclosing class or module directly 57 | ;; after any include or extend statements and the regions will be 58 | ;; replaced with the constant. 59 | 60 | ;; ## Add Parameter: 61 | ;; 'ruby-refactor-add-parameter' 62 | ;; This simply prompts you for a parameter to add to the current 63 | ;; method definition. If you are on a text, you can just hit enter 64 | ;; as it will use it by default. There is a custom variable to set 65 | ;; if you like parens on your params list. Default values and the 66 | ;; like shouldn't confuse it. 67 | 68 | ;; ## Extract to Let: 69 | ;; This is really for use with RSpec 70 | 71 | ;; 'ruby-refactor-extract-to-let' 72 | ;; There is a variable for where the 'let' gets placed. It can be 73 | ;; "top" which is top-most in the file, or "closest" which just 74 | ;; walks up to the first describe/context it finds. 75 | ;; You can also specify a different regex, so that you can just 76 | ;; use "describe" if you want. 77 | ;; If you are on a line: 78 | ;; a = Something.else.doing 79 | ;; becomes 80 | ;; let(:a){ Something.else.doing } 81 | 82 | ;; If you are selecting a region: 83 | ;; a = Something.else 84 | ;; a.stub(:blah) 85 | ;; becomes 86 | ;; let :a do 87 | ;; _a = Something.else 88 | ;; _a.stub(:blah) 89 | ;; _a 90 | ;; end 91 | 92 | ;; In both cases, you need the line, first line to have an ' = ' in it, 93 | ;; as that drives conversion. 94 | 95 | ;; There is also the bonus that the let will be placed *after* any other 96 | ;; let statements. It appends it to bottom of the list. 97 | 98 | ;; Oh, if you invoke with a prefix arg (C-u, etc.), it'll swap the placement 99 | ;; of the let. If you have location as top, a prefix argument will place 100 | ;; it closest. I kinda got nutty with this one. 101 | 102 | 103 | ;; ## TODO 104 | ;; From the vim plugin, these remain to be done (I don't plan to do them all.) 105 | ;; - remove inline temp (sexy!) 106 | 107 | ;;; Code: 108 | 109 | (require 'ruby-mode) 110 | 111 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 112 | ;;; Customizations 113 | (defgroup ruby-refactor nil 114 | "Refactoring helpers for Ruby." 115 | :version "0.1" 116 | :group 'files) 117 | 118 | (defcustom ruby-refactor-let-prefix "" 119 | "Prefix to use when extracting a region to let." 120 | :group 'ruby-refactor 121 | :type 'string) 122 | 123 | (defcustom ruby-refactor-add-parens nil 124 | "Add parens when adding a parameters to a function. 125 | Will be converted if params already exist." 126 | :group 'ruby-refactor 127 | :type 'boolean) 128 | 129 | (defcustom ruby-refactor-trim-re "[ \t\n]*" 130 | "Regex to use for trim functions. 131 | Will be applied to both front and back of string." 132 | :group 'ruby-refactor 133 | :type 'string) 134 | 135 | (defcustom ruby-refactor-let-placement-re "^[ \t]*\\(describe\\|context\\)" 136 | "Regex searched for to determine where to put let statemement. 137 | See `ruby-refactor-let-position' to specify proximity to assignment 138 | being altered." 139 | :group 'ruby-refactor 140 | :type 'string) 141 | 142 | (defcustom ruby-refactor-let-position 'top 143 | "Where to place the 'let' statement. 144 | 'closest places it after the most recent context or describe. 145 | 'top (default) places it after opening describe" 146 | :type '(choice (const :tag "place top-most" top) 147 | (const :tag "place closest" closest))) 148 | 149 | (defcustom ruby-refactor-keymap-prefix (kbd "C-c C-r") 150 | "ruby-refactor keymap prefix." 151 | :group 'ruby-refactor 152 | :type 'sexp) 153 | 154 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 155 | ;;; Vars 156 | 157 | (defvar ruby-refactor-mode-map 158 | (let ((map (make-sparse-keymap))) 159 | (let ((prefix-map (make-sparse-keymap))) 160 | (define-key prefix-map (kbd "e") 'ruby-refactor-extract-to-method) 161 | (define-key prefix-map (kbd "p") 'ruby-refactor-add-parameter) 162 | (define-key prefix-map (kbd "l") 'ruby-refactor-extract-to-let) 163 | (define-key prefix-map (kbd "v") 'ruby-refactor-extract-local-variable) 164 | (define-key prefix-map (kbd "c") 'ruby-refactor-extract-constant) 165 | (define-key prefix-map (kbd "o") 'ruby-refactor-convert-post-conditional) 166 | (define-key map ruby-refactor-keymap-prefix prefix-map)) 167 | map) 168 | "Keymap to use in ruby refactor minor mode.") 169 | 170 | (defvar ruby-refactor-mode-hook nil 171 | "Hooks run during mode start.") 172 | 173 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 174 | ;;; Helper functions 175 | (defun ruby-refactor-line-contains-equal-p (line) 176 | "Return if line contains an '='." 177 | (string-match "=" line)) 178 | 179 | (defun ruby-refactor-line-has-let-p () 180 | "Return if line contains 'let('." 181 | (string-match "let(" (thing-at-point 'line))) 182 | 183 | (defun ruby-refactor-ends-with-newline-p (region-start region-end) 184 | "Return if the last character is a newline ignoring trailing spaces." 185 | (let ((text (replace-regexp-in-string " *$" "" (buffer-substring-no-properties region-start region-end)))) 186 | (string-match "\n" (substring text -1)))) 187 | 188 | (defun ruby-refactor-trim-string (string) 189 | "Trims text from both front and back of STRING." 190 | (replace-regexp-in-string (concat ruby-refactor-trim-re "$") "" 191 | (replace-regexp-in-string (concat "^" ruby-refactor-trim-re) "" string))) 192 | 193 | (defun ruby-refactor-trim-newline-endings (string) 194 | "Trims newline off front and back of STRING" 195 | (replace-regexp-in-string "\\(^\n\\|\n$\\)" "" string)) 196 | 197 | (defun ruby-refactor-trim-list (list) 198 | "Apply `ruby-refactor-trim-string' to each item in LIST, and returns newly trimmed list." 199 | (mapcar #'ruby-refactor-trim-string list)) 200 | 201 | (defun ruby-refactor-goto-def-start () 202 | "Move point to start of first def to appear previously." 203 | (search-backward-regexp "^\\s *def")) 204 | 205 | (defun ruby-refactor-goto-first-non-let-line () 206 | "Place point at beginning of first non let( containing line." 207 | (while (ruby-refactor-line-has-let-p) 208 | (forward-line 1))) 209 | 210 | (defun ruby-refactor-goto-constant-insertion-point () 211 | "Move point to the proper location to insert a constant at the top of a class or module." 212 | (search-backward-regexp "^ *\\") 213 | (forward-line 1) 214 | (while (or (string-match "include" (thing-at-point 'line)) 215 | (string-match "extend" (thing-at-point 'line))) 216 | (forward-line 1))) 217 | 218 | (defun ruby-refactor-jump-to-let-insert-point (flip-location) 219 | "Position point at the proper place for inserting let. 220 | This depends the value of `ruby-refactor-let-position'." 221 | (let ((position-test (if (null flip-location) 222 | #'(lambda(left right)(eq left right)) 223 | #'(lambda(left right)(not (eq left right)))))) 224 | (cond ((funcall position-test 'top ruby-refactor-let-position) 225 | (goto-char 0) 226 | (search-forward-regexp ruby-refactor-let-placement-re)) 227 | ((funcall position-test 'closest ruby-refactor-let-position) 228 | (search-backward-regexp ruby-refactor-let-placement-re))))) 229 | 230 | (defun ruby-refactor-get-input-with-default (prompt default-value) 231 | "Get user input with a default value." 232 | (list (read-string (format "%s (%s): " prompt default-value) nil nil default-value))) 233 | 234 | (defun ruby-refactor-new-params (existing-params new-variable) 235 | "Append or create parameter list, doing the right thing for parens." 236 | (let ((param-list (mapconcat 'identity 237 | (ruby-refactor-trim-list (remove "" (append (split-string existing-params ",") (list new-variable)))) 238 | ", " ))) 239 | (if ruby-refactor-add-parens 240 | (format "(%s)" param-list) 241 | (format " %s" param-list)))) 242 | 243 | (defun ruby-refactor-assignment-error-message () 244 | "Message user with error if the (first) line of a let extraction is missing." 245 | (message "First line needs to have an assigment")) 246 | 247 | (defun ruby-refactor-extract-line-to-let (flip-location) 248 | "Extract current line to let." 249 | (let* ((line-bounds (bounds-of-thing-at-point 'line)) 250 | (text-begin (car line-bounds)) 251 | (text-end (cdr line-bounds)) 252 | (text (ruby-refactor-trim-newline-endings (buffer-substring-no-properties text-begin text-end))) 253 | (line-components (ruby-refactor-trim-list (split-string text " = ")))) 254 | (if (ruby-refactor-line-contains-equal-p text) 255 | (progn 256 | (delete-region text-begin text-end) 257 | (ruby-refactor-jump-to-let-insert-point flip-location) 258 | (forward-line 1) 259 | (ruby-refactor-goto-first-non-let-line) 260 | (ruby-indent-line) 261 | (insert (format "let(:%s){ %s }\n" (car line-components) (cadr line-components))) 262 | (newline-and-indent) 263 | (beginning-of-line) 264 | (unless (looking-at "^[ \t]*$") (newline-and-indent)) 265 | (delete-blank-lines)) 266 | (ruby-refactor-assignment-error-message)))) 267 | 268 | (defun ruby-refactor-extract-region-to-let (flip-location) 269 | "Extract current region to let." 270 | (let* ((text-begin (region-beginning)) 271 | (text-end (region-end)) 272 | (text (ruby-refactor-trim-newline-endings (buffer-substring-no-properties text-begin text-end))) 273 | (text-lines (ruby-refactor-trim-list (split-string text "\n")))) 274 | (if (ruby-refactor-line-contains-equal-p (car text-lines)) 275 | (let* ((variable-name (car (ruby-refactor-trim-list (split-string (car text-lines) " = ")))) 276 | (faux-variable-name (concat ruby-refactor-let-prefix variable-name)) 277 | (case-fold-search nil)) 278 | (delete-region text-begin text-end) 279 | (ruby-refactor-jump-to-let-insert-point flip-location) 280 | (forward-line 1) 281 | (ruby-refactor-goto-first-non-let-line) 282 | (ruby-indent-line) 283 | (insert (format "let :%s do" variable-name)) 284 | (mapc #'(lambda(line) 285 | (newline) 286 | (insert (replace-regexp-in-string variable-name faux-variable-name line))) 287 | text-lines) 288 | (insert "\n" faux-variable-name "\n" "end") 289 | (newline-and-indent) 290 | (search-backward "let") 291 | (ruby-indent-exp) 292 | (search-forward "end") 293 | (newline-and-indent) 294 | (beginning-of-line) 295 | (unless (looking-at "^[ \t]*$") (newline-and-indent)) 296 | (delete-blank-lines)) 297 | (ruby-refactor-assignment-error-message)))) 298 | 299 | (defun ruby-refactor-define-extracted-method (function-name argument-list function-guts) 300 | (concat "def " function-name 301 | (if (string= "" (ruby-refactor-trim-string argument-list)) 302 | "" 303 | (ruby-refactor-new-params "" argument-list)) 304 | "\n" function-guts "\nend\n\n")) 305 | 306 | (defun ruby-refactor-generate-function-call (function-name argument-list) 307 | (if (string= "" (ruby-refactor-trim-string argument-list)) 308 | function-name 309 | (format "%s(%s)" function-name argument-list))) 310 | 311 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 312 | ;;; API 313 | 314 | ;;;###autoload 315 | (defun ruby-refactor-extract-to-method (region-start region-end) 316 | "Extract region to method" 317 | (interactive "r") 318 | (save-restriction 319 | (save-match-data 320 | (widen) 321 | (let ((ends-with-newline (ruby-refactor-ends-with-newline-p region-start region-end)) 322 | (function-guts (ruby-refactor-trim-newline-endings (buffer-substring-no-properties region-start region-end))) 323 | (function-name (read-from-minibuffer "Method name: ")) 324 | (argument-list (read-from-minibuffer "Argument list (empty if none): "))) 325 | (delete-region region-start region-end) 326 | (ruby-indent-line) 327 | (insert (ruby-refactor-generate-function-call function-name argument-list)) 328 | (if ends-with-newline 329 | (progn 330 | (ruby-indent-line) 331 | (insert "\n") 332 | (ruby-indent-line))) 333 | (ruby-refactor-goto-def-start) 334 | (insert (ruby-refactor-define-extracted-method function-name argument-list function-guts)) 335 | (ruby-refactor-goto-def-start) 336 | (indent-region (point) 337 | (progn 338 | (forward-paragraph) 339 | (point))) 340 | (search-forward function-name) 341 | (backward-sexp) 342 | )))) 343 | 344 | ;;;###autoload 345 | (defun ruby-refactor-add-parameter (variable-name) 346 | "Add a parameter to the method point is in." 347 | (interactive (ruby-refactor-get-input-with-default "Variable name" (thing-at-point 'symbol))) 348 | (save-excursion 349 | (save-restriction 350 | (save-match-data 351 | (widen) 352 | (ruby-refactor-goto-def-start) 353 | (search-forward "def") 354 | (let* ((params-start-point (search-forward-regexp (concat ruby-symbol-re "+"))) 355 | (params-end-point (line-end-position)) 356 | (params-string (buffer-substring-no-properties params-start-point params-end-point))) 357 | (delete-region params-start-point params-end-point) 358 | (goto-char params-start-point) 359 | (insert (ruby-refactor-new-params params-string variable-name)) 360 | ))))) 361 | 362 | ;;;###autoload 363 | (defun ruby-refactor-extract-to-let(&optional flip-location) 364 | "Converts initialization on current line to 'let', ala RSpec 365 | When called with a prefix argument, flips the default location 366 | for placement. 367 | If a region is selected, the first line needs to have an assigment. 368 | The let style is then a do block containing the region. 369 | If a region is not selected, the transformation uses the current line." 370 | (interactive "P") 371 | (save-excursion 372 | (save-restriction 373 | (save-match-data 374 | (widen) 375 | (if (region-active-p) 376 | (ruby-refactor-extract-region-to-let flip-location) 377 | (ruby-refactor-extract-line-to-let flip-location)))))) 378 | 379 | ;;;###autoload 380 | (defun ruby-refactor-extract-local-variable() 381 | "Extracts selected text to local variable" 382 | (interactive) 383 | (save-restriction 384 | (save-match-data 385 | (widen) 386 | (let* ((text-begin (region-beginning)) 387 | (text-end (region-end)) 388 | (text (ruby-refactor-trim-newline-endings (buffer-substring-no-properties text-begin text-end))) 389 | (variable-name (read-from-minibuffer "Variable name? "))) 390 | (delete-region text-begin text-end) 391 | (insert variable-name) 392 | (beginning-of-line) 393 | (open-line 1) 394 | (ruby-indent-line) 395 | (insert variable-name " = " text) 396 | (search-forward variable-name) 397 | (backward-sexp))))) 398 | 399 | ;;;###autoload 400 | (defun ruby-refactor-extract-constant() 401 | "Extracts selected text to a constant at the top of the current class or module" 402 | (interactive) 403 | (save-restriction 404 | (save-match-data 405 | (widen) 406 | (let* ((text-begin (region-beginning)) 407 | (text-end (region-end)) 408 | (text (ruby-refactor-trim-newline-endings (buffer-substring-no-properties text-begin text-end))) 409 | (constant-name (read-from-minibuffer "Constant name? "))) 410 | (delete-region text-begin text-end) 411 | (insert constant-name) 412 | (ruby-refactor-goto-constant-insertion-point) 413 | (beginning-of-line) 414 | (open-line 2) 415 | (forward-line 1) 416 | (ruby-indent-line) 417 | (insert constant-name " = " text "\n") 418 | (search-forward constant-name) 419 | (backward-sexp))))) 420 | 421 | ;;;###autoload 422 | (defun ruby-refactor-remove-inline-temp() 423 | "Replaces temporary variable with direct call to method" 424 | (interactive) 425 | (error "Not Yet Implemented")) 426 | 427 | ;;;###autoload 428 | (defun ruby-refactor-convert-post-conditional() 429 | "Convert post conditional expression to conditional expression" 430 | (interactive) 431 | (save-restriction 432 | (save-match-data 433 | (widen) 434 | (let* ((text-begin (line-beginning-position)) 435 | (text-end (line-end-position)) 436 | (text (ruby-refactor-trim-newline-endings (buffer-substring-no-properties text-begin text-end))) 437 | (conditional 438 | (cond ((string-match-p "if" text) "if") 439 | ((string-match-p "unless" text) "unless") 440 | (t (error "You need an `if' or `unless' on the target line")))) 441 | (line-components (ruby-refactor-trim-list (split-string text (format " %s " conditional))))) 442 | (delete-region text-begin text-end) 443 | (insert (format "%s %s" conditional (cadr line-components))) 444 | (newline-and-indent) 445 | (insert (format "%s" (car line-components))) 446 | (newline-and-indent) 447 | (insert "end") 448 | (ruby-indent-line) 449 | (search-backward conditional))))) 450 | 451 | ;;;###autoload 452 | (define-minor-mode ruby-refactor-mode 453 | "Ruby Refactor minor mode" 454 | :global nil 455 | :group 'ruby-refactor 456 | :keymap ruby-refactor-mode-map 457 | :lighter " RubyRef") 458 | 459 | ;;;###autoload 460 | (defun ruby-refactor-mode-launch () 461 | "Turn on `ruby-refactor-mode'." 462 | (ruby-refactor-mode 1)) 463 | 464 | (provide 'ruby-refactor) 465 | 466 | ;;; ruby-refactor.el ends here 467 | --------------------------------------------------------------------------------