-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathevil-ruby-text-objects.el
More file actions
196 lines (172 loc) · 8.66 KB
/
evil-ruby-text-objects.el
File metadata and controls
196 lines (172 loc) · 8.66 KB
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
;;; evil-ruby-text-objects.el --- Evil text objects for Ruby code -*- lexical-binding: t -*-
;; Copyright (C) 2019 Sergio Gil
;; Author: Sergio Gil <sgilperez@gmail.com>
;; Version: 0.1
;; Keywords: languages
;; URL: https://github.com/porras/evil-ruby-text-objects
;; Package-Requires: ((emacs "25.1") (evil "1.2.0"))
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Adds text objects and keybindings to work with Ruby code with Evil.
;;
;; See documentation at https://github.com/porras/evil-ruby-text-objects
;;; Code:
(require 'evil)
(require 'eieio)
(require 'cl-lib)
;; these are the external functions, from the bundled ruby-mode, and from
;; enh-ruby-mode (only in case it's installed) which are used from this package.
(declare-function ruby-beginning-of-block "ruby-mode.el")
(declare-function ruby-end-of-block "ruby-mode.el")
(declare-function enh-ruby-beginning-of-block "ext:enh-ruby-mode.el")
(declare-function enh-ruby-end-of-block "ext:enh-ruby-mode.el")
(declare-function enh-ruby-up-sexp "ext:enh-ruby-mode.el")
;; These classes abstract away the differences between ruby-mode and
;; enh-ruby-mode. They implement four methods: up, beginning, end, and
;; mark-special (to handle specific cases without resorting to the mode tools).
;; The enh-ruby-mode variant just delegate to enh-ruby-mode's
;; enh-ruby-beginning-of-block, enh-ruby-end-of-block and enh-ruby-up-sexp, and
;; returns nil from mark-special to signal that no special case needs to be
;; handled. The ruby-mode variants delegates to ruby-mode's
;; ruby-beginning-of-block, ruby-end-of-block, and backward-up-list, but
;; performing additional movements so that they behave exactly like the
;; enh-ruby-mode's counterparts. In mark-special, it checks if we're currently
;; in a oneline method/class/etc (a case that ruby-mode doesn't handle), and in
;; that case it selects the line and return t to signal that the special case
;; was handled.
(defclass evil-ruby-text-objects--enh-ruby-mode-navigator () ())
(defclass evil-ruby-text-objects--ruby-mode-navigator () ())
(cl-defmethod evil-ruby-text-objects--beginning ((_ evil-ruby-text-objects--ruby-mode-navigator))
"Emulate `enh-ruby-beginning-of-block' using `ruby-beginning-of-block'.
`ruby-beginning-of-block' moves us to the beginning of the line
where the block begins, we need to move forward to the `do'
keyword to complete the emulation."
(ruby-beginning-of-block)
(re-search-forward "do" (line-end-position) t))
(cl-defmethod evil-ruby-text-objects--end ((_ evil-ruby-text-objects--ruby-mode-navigator))
"Emulate `enh-ruby-end-of-block' using `ruby-end-of-block'.
We move to the end of the block, and then move one word forward
if we are at the `end' keyword."
(ruby-end-of-block)
(when (looking-at "end") (evil-forward-word-begin)))
(cl-defmethod evil-ruby-text-objects--up ((_ evil-ruby-text-objects--ruby-mode-navigator))
"Delegate directly to `ruby-mode'."
(backward-up-list))
(cl-defmethod evil-ruby-text-objects--mark-special ((_ evil-ruby-text-objects--ruby-mode-navigator) keyword)
"Manage oneline definition.
Searches for a oneline KEYWORD (def or class) with a regular
expression (`enh-ruby-mode' manages this correctly but
`ruby-mode' doesn't)."
(when (string-match-p (concat "^\s*" keyword ".*;\s*end\s*$") (thing-at-point 'line))
(beginning-of-line-text)
(set-mark (point))
(end-of-line)
t))
(cl-defmethod evil-ruby-text-objects--beginning ((_ evil-ruby-text-objects--enh-ruby-mode-navigator))
"Delegate directly to `enh-ruby-mode'."
(enh-ruby-beginning-of-block))
(cl-defmethod evil-ruby-text-objects--end ((_ evil-ruby-text-objects--enh-ruby-mode-navigator))
"Delegate directly to `enh-ruby-mode'."
(enh-ruby-end-of-block))
(cl-defmethod evil-ruby-text-objects--up ((_ evil-ruby-text-objects--enh-ruby-mode-navigator))
"Delegate directly to `enh-ruby-mode'."
(enh-ruby-up-sexp))
(cl-defmethod evil-ruby-text-objects--mark-special ((_ evil-ruby-text-objects--enh-ruby-mode-navigator) _keyword)
"Do nothing.
\(we don't need to manage any special case in `enh-ruby-mode')."
nil)
(defun evil-ruby-text-objects--make-navigator ()
"Instantiate a navigator object suitable for the current ruby mode.
It defaults to the builtin `ruby-mode', so that this mode can be used with
languages similar to Ruby, such as Crystal."
(cond ((eq major-mode 'enh-ruby-mode) (evil-ruby-text-objects--enh-ruby-mode-navigator))
(t (evil-ruby-text-objects--ruby-mode-navigator))))
;; the rest of the functions are used always, and use a navigator instance,
;; which implements the two supported ruby modes.
(defun evil-ruby-text-objects--evil-range (count type keyword &optional inner)
"Define a linewise ‘evil-range’ selecting the specified Ruby expression.
COUNT: number of times it should go up the tree searching for the target
expression (for nested expressions)
TYPE: managed by ‘evil-range’ and passed as is
KEYWORD: string or regexp with the keyword that marks the beginning of the
target expression
INNER: When t, then only the content of the expression is selected but not its
opening or closing"
(save-excursion
(let ((navigator (evil-ruby-text-objects--make-navigator)))
(unless (evil-ruby-text-objects--mark-special navigator keyword)
(skip-syntax-forward " ")
(unless (looking-at keyword)
(evil-ruby-text-objects--beginning navigator))
(dotimes (i count)
(while (not (looking-at keyword))
(when (bobp) (user-error "Can't find current %s opening" keyword))
(evil-ruby-text-objects--up navigator))
(unless (= i (1- count)) ; if it's not the last one
(evil-ruby-text-objects--up navigator)))
(set-mark (point))
(evil-ruby-text-objects--end navigator)))
(when inner
(search-backward "end")
(exchange-point-and-mark)
(skip-chars-forward "^;\n")
(forward-char)
(skip-syntax-forward " "))
(evil-text-object-make-linewise (evil-range (region-beginning) (region-end) type :expanded t))))
(defmacro evil-ruby-text-objects--define-object (object &optional keyword)
"Define an inner and an outer object.
Accepted parameters are the OBJECT name (a string) and optionally a KEYWORD
\(string or regexp, defaults to the object name). It defines two evil text
objects, evil-a-ruby-<OBJECT> (outer), and evil-inner-ruby-<OBJECT> (inner)."
(let ((keyword (or keyword object))
(outer-object (intern (concat "evil-a-ruby-" object)))
(inner-object (intern (concat "evil-inner-ruby-" object))))
`(progn
(evil-define-text-object ,outer-object (count &rest _)
,(format "Select a Ruby %s." object)
(evil-ruby-text-objects--evil-range count type ,keyword))
(evil-define-text-object ,inner-object (count &rest _)
,(format "Select the inner content of a Ruby %s." object)
(evil-ruby-text-objects--evil-range count type ,keyword t)))))
(evil-ruby-text-objects--define-object "method" "def")
(evil-ruby-text-objects--define-object "class")
(evil-ruby-text-objects--define-object "module")
(evil-ruby-text-objects--define-object "namespace" (regexp-opt '("class" "module")))
(evil-ruby-text-objects--define-object "block" "do")
(evil-ruby-text-objects--define-object "conditional" (regexp-opt '("if" "unless" "case")))
(evil-ruby-text-objects--define-object "begin")
;;;###autoload
(define-minor-mode evil-ruby-text-objects-mode
"Enable Evil keybindings for ruby text objects."
:keymap (make-sparse-keymap)
(evil-define-key '(operator visual) evil-ruby-text-objects-mode-map
"am" 'evil-a-ruby-method
"am" 'evil-a-ruby-method
"arm" 'evil-a-ruby-method
"arc" 'evil-a-ruby-class
"arM" 'evil-a-ruby-module
"arn" 'evil-a-ruby-namespace
"arb" 'evil-a-ruby-block
"ari" 'evil-a-ruby-conditional
"arg" 'evil-a-ruby-begin
"im" 'evil-inner-ruby-method
"irm" 'evil-inner-ruby-method
"irc" 'evil-inner-ruby-class
"irM" 'evil-inner-ruby-module
"irn" 'evil-inner-ruby-namespace
"irb" 'evil-inner-ruby-block
"iri" 'evil-inner-ruby-conditional
"irg" 'evil-inner-ruby-begin))
(provide 'evil-ruby-text-objects)
;;; evil-ruby-text-objects.el ends here