├── .gitignore ├── Cask ├── Makefile ├── README.md ├── README.org ├── ess-smart-equals.el ├── features ├── ess-smart-equals.feature ├── ess-smart-ops.feature ├── step-definitions │ └── ess-smart-equals-steps.el └── support │ └── env.el └── test ├── ess-smart-equals-test.el ├── manual-init.el └── test-helper.el /.gitignore: -------------------------------------------------------------------------------- 1 | .*~ 2 | .bonz* 3 | .DS_Store 4 | *.tgz 5 | *.elc 6 | .cask/* 7 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source melpa) 2 | 3 | (package "ess-smart-equals" "0.2.1" "A flexible, context-sensitive assignment key for R and S") 4 | 5 | (depends-on "ess") 6 | 7 | (development 8 | (depends-on "s") 9 | (depends-on "f") 10 | (depends-on "ecukes") 11 | (depends-on "espuds") 12 | (depends-on "ert-runner")) 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CASK ?= cask 2 | EMACS ?= emacs 3 | 4 | all: test 5 | 6 | test: unit integration 7 | 8 | unit: 9 | ${CASK} exec ert-runner 10 | 11 | integration: 12 | ${CASK} exec ecukes 13 | 14 | install-deps: 15 | ${CASK} install 16 | 17 | .PHONY: all test unit integration install-deps 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ess-smart-equals.el 3 | 4 | > Current Version: 0.3.1. This is a major update – indeed a 5 | > complete rewrite – relative to version 0.2.2, with significant 6 | > improvements in functionality, flexibility, and performance. 7 | > This has also been brought up to date with recent versions of 8 | > ESS. 9 | 10 | Assignment in R is syntactically complicated by a few features: 11 | 12 | 1. the historical role of '\_' (underscore) as an assignment 13 | 14 | character in the S language; 2. the somewhat 15 | inconvenient-to-type, if conceptually pure, '<-' operator as the 16 | preferred assignment operator; 3. the ability to use either an 17 | '=', '<-', and a variety of other operators for assignment; and 18 | 19 | 1. the multiple roles that '=' can play, including for setting 20 | 21 | named arguments in a function call. 22 | 23 | This package offers a flexible, context-sensitive assignment key 24 | for R and S that is, by default, tied to the '=' key. This key 25 | inserts or completes relevant, properly spaced operators 26 | (assignment, comparison, etc.) based on the syntactic context in 27 | the code. It allows very easy cycling through the possible 28 | operators in that context. The contexts, the operators, and 29 | their cycling order in each context are customizable. 30 | 31 | The package defines a buffer-local minor mode 32 | `ess-smart-equals-mode`, intended for S-language modes (e.g., 33 | ess-r-mode, inferior-ess-r-mode, and ess-r-transcript-mode), that 34 | when enabled in a buffer activates the '=' key to to handle 35 | context-sensitive completion and cycling of relevant operators. When 36 | the mode is active and an '=' is pressed: 37 | 38 | 1. With a prefix argument or in specified contexts (which for 39 | most major modes means in strings or comments), just 40 | insert '='. 41 | 42 | 2. If an operator relevant to the context lies before point 43 | (with optional whitespace), it is replaced, cyclically, by the 44 | next operator in the configured list for that context. 45 | 46 | 3. Otherwise, if a prefix of an operator relevant to the 47 | context lies before point, that operator is completed. 48 | 49 | 4. Otherwise, the highest priority relevant operator is inserted 50 | with surrounding whitespace (see `ess-smart-equals-no-spaces`). 51 | 52 | Consecutive presses of '=' cycle through the relevant operators. 53 | After an '=', a backspace (or other configurable keys) removes 54 | the last operator and tab offers a choice of operators by completion. 55 | (Shift-backspace will delete one character only and restore the 56 | usual maning of backspace.) See `ess-smart-equals-cancel-keys`. 57 | 58 | By default, the minor mode activates the '=' key, but this can 59 | be customized by setting the option `ess-smart-equals-key` before 60 | this package is loaded. 61 | 62 | The function `ess-smart-equals-activate` arranges for the minor mode 63 | to be activated by mode hooks for any given list of major modes, 64 | defaulting to ESS major modes associated with R (`ess-r-mode`, 65 | `inferior-ess-r-mode`, `ess-r-transcript-mode`, `ess-roxy-mode`). 66 | 67 | 68 | ## Examples 69 | 70 | In the left column below, ^ marks the location at which an '=' 71 | key is pressed, the remaining columns show the result of 72 | consecutive presses of '=' using the package's default settings. 73 | position of point. 74 | 75 | Before '=' Press '=' Another '=' Another '=' 76 | ---------- --------- ----------- ----------- 77 | foo^ foo <- ^ foo <<- ^ foo = ^ 78 | foo ^ foo <- ^ foo <<- ^ foo = ^ 79 | foo<^ foo <- ^ foo <<- ^ foo = ^ 80 | foo=^ foo = ^ foo -> ^ foo ->> ^ 81 | foo(a^ foo(a = ^ foo( a == ^ foo( a != ^ 82 | if( foo=^ if( foo == ^ if( foo != ^ if( foo <= ^ 83 | if( foo<^ if( foo < ^ if( foo > ^ if( foo >= ^ 84 | "foo ^ "foo =^ "foo ==^ "foo ===^ 85 | #...foo ^ #...foo =^ #...foo ==^ #...foo ===^ 86 | 87 | As a bonus, the value of the variable `ess-smart-equals-extra-ops` 88 | when this package is loaded, determines some other smart operators 89 | that may prove useful. Currently, only `brace`, `paren`, and `percent` 90 | are supported, causing `ess-smart-equals-open-brace`, 91 | `ess-smart-equals-open-paren`, and `ess-smart-equals-percent` to be 92 | bound to '{', '(', and '%', respectively. The first two of these 93 | configurably places a properly indented and spaced matching pair at 94 | point or around the region if active. The paren pair also includes 95 | a magic space with a convenient keymap for managing parens. See the 96 | readme. See the customizable variable 97 | `ess-smart-equals-brace-newlines` for configuring the newlines around 98 | braces. The third operator (`ess-smart-equals-percent`) performs 99 | matching of %-operators analogously to `ess-smart-equals`. See the 100 | **Extra Operators** section below under **Customization** for details. 101 | 102 | Finally, the primary user facing functions are named with a 103 | prefix `ess-smart-equals-` to avoid conflicts with other 104 | packages. Because this is long, the internal functions and 105 | objects use a shorter (but still distinctive) prefix `essmeq-`. 106 | 107 | 108 | ## Installation and Initialization 109 | 110 | The package can be loaded from MELPA using `package-install` with 111 | 112 | M-x package-install ess-smart-equals 113 | 114 | or with the `list-packages` interface or another Emacs package 115 | manager. Alternatively, you can clone or download the source 116 | directly from the github repository and put the file 117 | `ess-smart-equals.el` in your Emacs load path. 118 | 119 | A variety of activation options is described below, 120 | but tl;dr: the *recommended* way to activate the mode (e.g., 121 | in your init file) is either directly with 122 | 123 | (setq ess-smart-equals-extra-ops '(brace paren percent)) 124 | (with-eval-after-load 'ess-r-mode 125 | (require 'ess-smart-equals) 126 | (ess-smart-equals-activate)) 127 | 128 | or with `use-package`: 129 | 130 | (use-package ess-smart-equals 131 | :init (setq ess-smart-equals-extra-ops '(brace paren percent)) 132 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 133 | :config (ess-smart-equals-activate)) 134 | 135 | A more detailed description follows, if you want to see variations. 136 | 137 | To activate, you need only do 138 | 139 | (with-eval-after-load 'ess-r-mode 140 | (require 'ess-smart-equals) 141 | (ess-smart-equals-activate)) 142 | 143 | somewhere in your init file. This will add `ess-smart-equals-mode` to 144 | a prespecified, but customizable, list of mode hooks and activate 145 | the mode in already active buffers. 146 | 147 | For those who use the outstanding `use-package`, you can do 148 | 149 | (use-package ess-smart-equals 150 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 151 | :config (ess-smart-equals-activate)) 152 | 153 | somewhere in your init file. An equivalent but less concise version 154 | of this is 155 | 156 | (use-package ess-smart-equals 157 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 158 | :hook ((ess-r-mode . ess-smart-equals-mode) 159 | (inferior-ess-r-mode . ess-smart-equals-mode) 160 | (ess-r-transcript-mode . ess-smart-equals-mode) 161 | (ess-roxy-mode . ess-smart-equals-mode)) 162 | 163 | To also activate the extra smart operators, *which I recommend*, 164 | and to automatically bind them, you can replace this with 165 | 166 | (use-package ess-smart-equals 167 | :init (setq ess-smart-equals-extra-ops '(brace paren percent)) 168 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 169 | :config (ess-smart-equals-activate)) 170 | 171 | This is the setup that I use. See **Extra Smart Operators** below. 172 | 173 | You can also enable the minor mode in any buffer with 174 | 175 | M-x ess-smart-equals-mode 176 | 177 | though you will typically want to enable the minor mode 178 | in a corresponding mode hook, e.g., 179 | 180 | (add-hook 'foo-mode-hook 'ess-smart-equals-mode) 181 | 182 | to enable the mode in `foo-mode` buffers. 183 | 184 | 185 | ## Customization 186 | 187 | 188 | ### Special Keys 189 | 190 | By default `ess-smart-equals-mode` binds the smart operator to the '=' 191 | key, and this is the recommended choice. However, this key can be 192 | changed by customizing the variable `ess-smart-equals-key`. This 193 | should be changed either with the customization facility or before 194 | the package is loaded, as the key affects several internal keymaps. 195 | 196 | When `ess-smart-equals-key` is pressed, several transient keys are 197 | bound. First, the basic key of `ess-smart-equals-key` (e.g., '=' 198 | for '=' or 'C-c ='.) reexecutes `ess-smart-equals`, cycling the 199 | operators according to context. If the smart '%' operator is enabled 200 | (see below), then '%' is also bound to `ess-smart-equals-percent`, 201 | which can be interleaved with `ess-smart-equals` as desired. 202 | (Any other key exits the transient 203 | keymap.) Second, any key in `ess-smart-equals-cancel-keys` deletes any 204 | inserted operator before point; a shifted version of such a 205 | key deletes a single character backwards and thus 206 | cancels the transient bindings. Finally, tab allows you to select an 207 | operator by completion. 208 | 209 | The `ess-smart-equals-cancel-keys` are by default `backspace` and `DEL`, 210 | but they can be customized. 211 | 212 | Several other useful smart operators can be configured; see **Extra 213 | Smart Operators** below. 214 | 215 | 216 | ### Contexts 217 | 218 | The operator inserted or completed by `ess-smart-equals` is determined 219 | by the major mode and the syntactic context at point. The customizable 220 | variable `ess-smart-equals-contexts` specifies the mapping 221 | from syntactic contexts to a list of operators to consider in the 222 | order specified. This mapping is given for all contexts for the 223 | default case (t) along with lists for any major mode 224 | that are merged into the default mapping under that mode. 225 | In this way, simple modifications can be applied to any relevant 226 | mode without repeating all the specifications. 227 | 228 | The user can create new contexts by adding additional keys to that 229 | mapping and defining `ess-smart-equals-context-function`. This is 230 | called first when the context is determined; if it returns a symbol, 231 | that is used as the context; if it returns nil, the built-in context 232 | calculation is performed. For specialized purposes, the context can 233 | be overridden locally; see `ess-smart-equals-set-overriding-context` 234 | and `ess-smart-equals-clear-overriding-context`. 235 | 236 | 237 | ### Options 238 | 239 | Operator padding is controlled by two options: 240 | `ess-smart-equals-mode-padding-left` and 241 | `ess-smart-equals-mode-padding-right`, which control the padding on 242 | the left and right side of an inserted operator. If set to the 243 | symbol `one-space` (the default), `no-space`, or `some-space`, 244 | `ess-smart-equals` ensures that there is, respectively, exactly one 245 | space, no spaces, or at least one space (possibly taken from 246 | existing whitespace) on the corresponding side of the operator. If 247 | set to the symbol `none`, no padding adjustment is performed. If set 248 | to a string, that string is used as is for the padding on the 249 | corresponding side. Finally, this can be set to a function that 250 | takes two positions and an optional boolean; this function can 251 | adjust the padding in any way desired while also providing a way to 252 | compute how much padding has been added for the deletion operator. 253 | See the documentation for the padding variables for details. 254 | 255 | The customizable variable `ess-smart-equals-mode-options` is an alist 256 | mapping major modes to assignments of minor mode options used 257 | locally in each `ess-smart-equals-mode` buffer. This allows 258 | mode-specific configuraiton of this minor mode. The default is an 259 | example: in `inferior-ess-r-mode`, `ess-smart-equals-mode` uses 260 | specialized narrowing so that previous output and commands do not 261 | interfere with the context parsing at a given point. 262 | 263 | The customizable list `ess-smart-equals-default-modes` determines 264 | the major modes that area affected by `ess-smart-equals-activate`. 265 | 266 | 267 | ### Hooks 268 | 269 | The hook `ess-smart-equals-mode-hook` is called whenever the minor 270 | mode is enabled or disabled. The hooks `ess-smart-equals-mode-on-hook` 271 | and `ess-smart-equals-mode-off-hook` are called when the mode is 272 | enabled or disabled, respectively. 273 | 274 | The variable `ess-smart-equals-narrow-function` is used to narrow 275 | the buffer to a specific region where the ESS syntax checking will 276 | be valid. This is used primarily in `inferior-ess-r-mode` to restrict 277 | attention to the current prompt line or the zone between prompts 278 | because output or erroneous commands can adversely effect the 279 | ESS syntax checking. This should not normally be needed otherwise, 280 | but it can be set in `ess-smart-equals-options` if desired. 281 | 282 | The customizable variable `ess-smart-equals-insertion-hook`, if set, 283 | allows arbitrary post-processing after an operator insertion. It is 284 | passed all the information needed to characterize the insertion; see 285 | the documentation for that variable for details. 286 | 287 | 288 | ### Extra Smart Operators 289 | 290 | If `ess-smart-equals-extra-ops` is non-nil, it should be a list 291 | containing some of the symbols `brace`, `paren`, or `percent`. 292 | These settings will cause '{', '(', and '%', respectively, to 293 | be bound in the minor mode map to a smart operator with 294 | the following features: 295 | 296 | - `brace` 297 | 298 | Binds '{' to the command `ess-smart-equals-open-brace`. This 299 | inserts a properly spaced and indented pair of braces, wrapping 300 | around the region if it is active. The customizable variable 301 | `ess-smart-equals-brace-newlines` controls the placement of 302 | newlines before and after each brace. This can be configured 303 | separately or added to your ESS style as desired. 304 | 305 | - `paren` 306 | 307 | Binds '(' to the command `ess-smart-equals-open-paren`. This 308 | inserts a matching pair of parentheses with point on a magic 309 | space between them. If region is active, it is wrapped by the 310 | parentheses. The magic space has an attached keymap 311 | that makes it easy to fill, escape, and expand the parenthesis 312 | pair. In particular, when on this space: 313 | 314 | - ')' or ';' eliminates the magic space and exits the parentheses; 315 | 316 | - ',' inserts a spaced comma, leaving point on the magic space; 317 | 318 | - 'C-;' expands the region after the parenthesis pair to 319 | encompass an additional balanced expression; and 320 | 321 | - 'M-;' moves the marked region after the pair (e.g., as 322 | constructed by 'C-;') inside the parentheses, eliminating 323 | leading spaces unless a prefix argument is given. 324 | 325 | Taken together, these make it fast to fill in function calls 326 | or conditionals. 327 | 328 | - `percent` 329 | 330 | Binds '%' to the command `ess-smart-equals-percent`. This provides 331 | expansion, cycling, and completion analogous to `ess-smart-equals` 332 | but for %-operators in R. The `ess-smart-equals` and 333 | `ess-smart-equals-percent` commands can be interleaved at will; 334 | when one follows the other, it will remove operators produced by 335 | the preceding command and start cycling anew. 336 | 337 | With a prefix argument, all of these insert the literal corresponding 338 | character, with repeats if the argument is numeric. 339 | 340 | Additional smart operators may be added in future versions. 341 | 342 | Note that if you change the setting of `ess-smart-equals-extra-ops`, 343 | you can make it take effect in all relevant buffers by doing 344 | `M-x ess-smart-equals-refresh-mode`. 345 | 346 | 347 | ## Change Log 348 | 349 | - **0.3.0:** Breaking changes in functionality, design, and configuration. 350 | No longer relies on `ess-S-assign` which was deprecated in 351 | ESS. Now provides more powerful context-sensitive, prioritized 352 | operator lists with cycling and completion. The mode is now, 353 | properly, a local minor mode, which can be added automatically 354 | to relevant mode hooks for ESS R modes. Updated required 355 | versions of emacs and ESS. 356 | 357 | - **0.2.2:** Fix for deprecated ESS variables `ess-S-assign` and 358 | `ess-smart-S-assign-key`. Thanks to Daniel Gomez (@dangom). 359 | 360 | - **0.2.1:** Initial release with simple insertion and completion, with 361 | space padding for the operators except for a single '=' 362 | used to specify named arguments in function calls. Relies on 363 | ESS variables `ess-S-assign` and `ess-smart-S-assign-key` 364 | to specify preferred operator for standard assignments. 365 | 366 | 367 | ## To Do 368 | 369 | - Allow finer control in context operator lists, both in 370 | distinguishing cycling from completion and in allowing dynamic 371 | operator lists. An example use case would be asking R for the 372 | set of current `%infix%` operators. Some of the infrastructure 373 | for this is already in place. 374 | 375 | - Add `ess-smart-equals-the-works` for simple, full feature setup 376 | 377 | - If it is worthwhile, make contexts sticky over cycling to avoid 378 | the context changing during cycling. This does not yet appear 379 | to be needed. 380 | 381 | - Add more tests 382 | 383 | - … 384 | 385 | 386 | ## Contributors 387 | 388 | - Daniel Gomez (@dangom) 389 | 390 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * ess-smart-equals.el 2 | 3 | #+begin_quote 4 | Current Version: 0.3.1. This is a major update -- indeed a 5 | complete rewrite -- relative to version 0.2.2, with significant 6 | improvements in functionality, flexibility, and performance. 7 | This has also been brought up to date with recent versions of 8 | ESS. 9 | #+end_quote 10 | 11 | Assignment in R is syntactically complicated by a few features: 12 | 1. the historical role of '_' (underscore) as an assignment 13 | character in the S language; 2. the somewhat 14 | inconvenient-to-type, if conceptually pure, '<-' operator as the 15 | preferred assignment operator; 3. the ability to use either an 16 | '=', '<-', and a variety of other operators for assignment; and 17 | 4. the multiple roles that '=' can play, including for setting 18 | named arguments in a function call. 19 | 20 | This package offers a flexible, context-sensitive assignment key 21 | for R and S that is, by default, tied to the '=' key. This key 22 | inserts or completes relevant, properly spaced operators 23 | (assignment, comparison, etc.) based on the syntactic context in 24 | the code. It allows very easy cycling through the possible 25 | operators in that context. The contexts, the operators, and 26 | their cycling order in each context are customizable. 27 | 28 | The package defines a buffer-local minor mode 29 | ~ess-smart-equals-mode~, intended for S-language modes (e.g., 30 | ess-r-mode, inferior-ess-r-mode, and ess-r-transcript-mode), that 31 | when enabled in a buffer activates the '=' key to to handle 32 | context-sensitive completion and cycling of relevant operators. When 33 | the mode is active and an '=' is pressed: 34 | 35 | 1. With a prefix argument or in specified contexts (which for 36 | most major modes means in strings or comments), just 37 | insert '='. 38 | 39 | 2. If an operator relevant to the context lies before point 40 | (with optional whitespace), it is replaced, cyclically, by the 41 | next operator in the configured list for that context. 42 | 43 | 3. Otherwise, if a prefix of an operator relevant to the 44 | context lies before point, that operator is completed. 45 | 46 | 4. Otherwise, the highest priority relevant operator is inserted 47 | with surrounding whitespace (see ~ess-smart-equals-no-spaces~). 48 | 49 | Consecutive presses of '\equal' cycle through the relevant operators. 50 | After an '=', a backspace (or other configurable keys) removes 51 | the last operator and tab offers a choice of operators by completion. 52 | (Shift-backspace will delete one character only and restore the 53 | usual maning of backspace.) See ~ess-smart-equals-cancel-keys~. 54 | 55 | By default, the minor mode activates the '=' key, but this can 56 | be customized by setting the option ~ess-smart-equals-key~ before 57 | this package is loaded. 58 | 59 | The function ~ess-smart-equals-activate~ arranges for the minor mode 60 | to be activated by mode hooks for any given list of major modes, 61 | defaulting to ESS major modes associated with R (~ess-r-mode~, 62 | ~inferior-ess-r-mode~, ~ess-r-transcript-mode~, ~ess-roxy-mode~). 63 | 64 | ** Examples 65 | 66 | In the left column below, ^ marks the location at which an '=' 67 | key is pressed, the remaining columns show the result of 68 | consecutive presses of '=' using the package's default settings. 69 | position of point. 70 | 71 | #+begin_example 72 | Before '=' Press '=' Another '=' Another '=' 73 | ---------- --------- ----------- ----------- 74 | foo^ foo <- ^ foo <<- ^ foo = ^ 75 | foo ^ foo <- ^ foo <<- ^ foo = ^ 76 | foo<^ foo <- ^ foo <<- ^ foo = ^ 77 | foo=^ foo = ^ foo -> ^ foo ->> ^ 78 | foo(a^ foo(a = ^ foo( a == ^ foo( a != ^ 79 | if( foo=^ if( foo == ^ if( foo != ^ if( foo <= ^ 80 | if( foo<^ if( foo < ^ if( foo > ^ if( foo >= ^ 81 | "foo ^ "foo =^ "foo ==^ "foo ===^ 82 | #...foo ^ #...foo =^ #...foo ==^ #...foo ===^ 83 | #+end_example 84 | 85 | As a bonus, the value of the variable ~ess-smart-equals-extra-ops~ 86 | when this package is loaded, determines some other smart operators 87 | that may prove useful. Currently, only ~brace~, ~paren~, and ~percent~ 88 | are supported, causing ~ess-smart-equals-open-brace~, 89 | ~ess-smart-equals-open-paren~, and ~ess-smart-equals-percent~ to be 90 | bound to '{', '(', and '%', respectively. The first two of these 91 | configurably places a properly indented and spaced matching pair at 92 | point or around the region if active. The paren pair also includes 93 | a magic space with a convenient keymap for managing parens. See the 94 | readme. See the customizable variable 95 | ~ess-smart-equals-brace-newlines~ for configuring the newlines around 96 | braces. The third operator (~ess-smart-equals-percent~) performs 97 | matching of %-operators analogously to ~ess-smart-equals~. See the 98 | *Extra Operators* section below under *Customization* for details. 99 | 100 | Finally, the primary user facing functions are named with a 101 | prefix ~ess-smart-equals-~ to avoid conflicts with other 102 | packages. Because this is long, the internal functions and 103 | objects use a shorter (but still distinctive) prefix ~essmeq-~. 104 | 105 | ** Installation and Initialization 106 | 107 | The package can be loaded from MELPA using ~package-install~ with 108 | 109 | #+begin_example 110 | M-x package-install ess-smart-equals 111 | #+end_example 112 | 113 | or with the ~list-packages~ interface or another Emacs package 114 | manager. Alternatively, you can clone or download the source 115 | directly from the github repository and put the file 116 | ~ess-smart-equals.el~ in your Emacs load path. 117 | 118 | A variety of activation options is described below, 119 | but tl;dr: the /recommended/ way to activate the mode (e.g., 120 | in your init file) is either directly with 121 | 122 | #+begin_src emacs-lisp 123 | (setq ess-smart-equals-extra-ops '(brace paren percent)) 124 | (with-eval-after-load 'ess-r-mode 125 | (require 'ess-smart-equals) 126 | (ess-smart-equals-activate)) 127 | #+end_src 128 | 129 | or with ~use-package~: 130 | 131 | #+begin_src emacs-lisp 132 | (use-package ess-smart-equals 133 | :init (setq ess-smart-equals-extra-ops '(brace paren percent)) 134 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 135 | :config (ess-smart-equals-activate)) 136 | #+end_src 137 | 138 | A more detailed description follows, if you want to see variations. 139 | 140 | To activate, you need only do 141 | 142 | #+begin_src emacs-lisp 143 | (with-eval-after-load 'ess-r-mode 144 | (require 'ess-smart-equals) 145 | (ess-smart-equals-activate)) 146 | #+end_src 147 | 148 | somewhere in your init file. This will add ~ess-smart-equals-mode~ to 149 | a prespecified, but customizable, list of mode hooks and activate 150 | the mode in already active buffers. 151 | 152 | For those who use the outstanding ~use-package~, you can do 153 | 154 | #+begin_src emacs-lisp 155 | (use-package ess-smart-equals 156 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 157 | :config (ess-smart-equals-activate)) 158 | #+end_src 159 | 160 | somewhere in your init file. An equivalent but less concise version 161 | of this is 162 | 163 | #+begin_src emacs-lisp 164 | (use-package ess-smart-equals 165 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 166 | :hook ((ess-r-mode . ess-smart-equals-mode) 167 | (inferior-ess-r-mode . ess-smart-equals-mode) 168 | (ess-r-transcript-mode . ess-smart-equals-mode) 169 | (ess-roxy-mode . ess-smart-equals-mode)) 170 | #+end_src 171 | 172 | To also activate the extra smart operators, /which I recommend/, 173 | and to automatically bind them, you can replace this with 174 | 175 | #+begin_src emacs-lisp 176 | (use-package ess-smart-equals 177 | :init (setq ess-smart-equals-extra-ops '(brace paren percent)) 178 | :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 179 | :config (ess-smart-equals-activate)) 180 | #+end_src 181 | 182 | This is the setup that I use. See *Extra Smart Operators* below. 183 | 184 | You can also enable the minor mode in any buffer with 185 | 186 | #+begin_example 187 | M-x ess-smart-equals-mode 188 | #+end_example 189 | 190 | though you will typically want to enable the minor mode 191 | in a corresponding mode hook, e.g., 192 | 193 | #+begin_src emacs-lisp 194 | (add-hook 'foo-mode-hook 'ess-smart-equals-mode) 195 | #+end_src 196 | 197 | to enable the mode in ~foo-mode~ buffers. 198 | 199 | ** Customization 200 | *** Special Keys 201 | 202 | By default ~ess-smart-equals-mode~ binds the smart operator to the '=' 203 | key, and this is the recommended choice. However, this key can be 204 | changed by customizing the variable ~ess-smart-equals-key~. This 205 | should be changed either with the customization facility or before 206 | the package is loaded, as the key affects several internal keymaps. 207 | 208 | When ~ess-smart-equals-key~ is pressed, several transient keys are 209 | bound. First, the basic key of ~ess-smart-equals-key~ (e.g., '\equal' 210 | for '\equal' or 'C-c ='.) reexecutes ~ess-smart-equals~, cycling the 211 | operators according to context. If the smart '%' operator is enabled 212 | (see below), then '%' is also bound to ~ess-smart-equals-percent~, 213 | which can be interleaved with ~ess-smart-equals~ as desired. 214 | (Any other key exits the transient 215 | keymap.) Second, any key in ~ess-smart-equals-cancel-keys~ deletes any 216 | inserted operator before point; a shifted version of such a 217 | key deletes a single character backwards and thus 218 | cancels the transient bindings. Finally, tab allows you to select an 219 | operator by completion. 220 | 221 | The ~ess-smart-equals-cancel-keys~ are by default =backspace= and =DEL=, 222 | but they can be customized. 223 | 224 | Several other useful smart operators can be configured; see *Extra 225 | Smart Operators* below. 226 | 227 | *** Contexts 228 | 229 | The operator inserted or completed by ~ess-smart-equals~ is determined 230 | by the major mode and the syntactic context at point. The customizable 231 | variable ~ess-smart-equals-contexts~ specifies the mapping 232 | from syntactic contexts to a list of operators to consider in the 233 | order specified. This mapping is given for all contexts for the 234 | default case (t) along with lists for any major mode 235 | that are merged into the default mapping under that mode. 236 | In this way, simple modifications can be applied to any relevant 237 | mode without repeating all the specifications. 238 | 239 | The user can create new contexts by adding additional keys to that 240 | mapping and defining ~ess-smart-equals-context-function~. This is 241 | called first when the context is determined; if it returns a symbol, 242 | that is used as the context; if it returns nil, the built-in context 243 | calculation is performed. For specialized purposes, the context can 244 | be overridden locally; see ~ess-smart-equals-set-overriding-context~ 245 | and ~ess-smart-equals-clear-overriding-context~. 246 | 247 | *** Options 248 | 249 | Operator padding is controlled by two options: 250 | ~ess-smart-equals-mode-padding-left~ and 251 | ~ess-smart-equals-mode-padding-right~, which control the padding on 252 | the left and right side of an inserted operator. If set to the 253 | symbol =one-space= (the default), =no-space=, or =some-space=, 254 | ~ess-smart-equals~ ensures that there is, respectively, exactly one 255 | space, no spaces, or at least one space (possibly taken from 256 | existing whitespace) on the corresponding side of the operator. If 257 | set to the symbol =none=, no padding adjustment is performed. If set 258 | to a string, that string is used as is for the padding on the 259 | corresponding side. Finally, this can be set to a function that 260 | takes two positions and an optional boolean; this function can 261 | adjust the padding in any way desired while also providing a way to 262 | compute how much padding has been added for the deletion operator. 263 | See the documentation for the padding variables for details. 264 | 265 | The customizable variable ~ess-smart-equals-mode-options~ is an alist 266 | mapping major modes to assignments of minor mode options used 267 | locally in each ~ess-smart-equals-mode~ buffer. This allows 268 | mode-specific configuraiton of this minor mode. The default is an 269 | example: in =inferior-ess-r-mode=, ~ess-smart-equals-mode~ uses 270 | specialized narrowing so that previous output and commands do not 271 | interfere with the context parsing at a given point. 272 | 273 | The customizable list ~ess-smart-equals-default-modes~ determines 274 | the major modes that area affected by ~ess-smart-equals-activate~. 275 | 276 | *** Hooks 277 | 278 | The hook ~ess-smart-equals-mode-hook~ is called whenever the minor 279 | mode is enabled or disabled. The hooks ~ess-smart-equals-mode-on-hook~ 280 | and ~ess-smart-equals-mode-off-hook~ are called when the mode is 281 | enabled or disabled, respectively. 282 | 283 | The variable ~ess-smart-equals-narrow-function~ is used to narrow 284 | the buffer to a specific region where the ESS syntax checking will 285 | be valid. This is used primarily in =inferior-ess-r-mode= to restrict 286 | attention to the current prompt line or the zone between prompts 287 | because output or erroneous commands can adversely effect the 288 | ESS syntax checking. This should not normally be needed otherwise, 289 | but it can be set in ~ess-smart-equals-options~ if desired. 290 | 291 | The customizable variable ~ess-smart-equals-insertion-hook~, if set, 292 | allows arbitrary post-processing after an operator insertion. It is 293 | passed all the information needed to characterize the insertion; see 294 | the documentation for that variable for details. 295 | 296 | *** Extra Smart Operators 297 | 298 | If ~ess-smart-equals-extra-ops~ is non-nil, it should be a list 299 | containing some of the symbols ~brace~, ~paren~, or ~percent~. 300 | These settings will cause '{', '(', and '%', respectively, to 301 | be bound in the minor mode map to a smart operator with 302 | the following features: 303 | 304 | + ~brace~ 305 | 306 | Binds '{' to the command ~ess-smart-equals-open-brace~. This 307 | inserts a properly spaced and indented pair of braces, wrapping 308 | around the region if it is active. The customizable variable 309 | ~ess-smart-equals-brace-newlines~ controls the placement of 310 | newlines before and after each brace. This can be configured 311 | separately or added to your ESS style as desired. 312 | 313 | + ~paren~ 314 | 315 | Binds '(' to the command ~ess-smart-equals-open-paren~. This 316 | inserts a matching pair of parentheses with point on a magic 317 | space between them. If region is active, it is wrapped by the 318 | parentheses. The magic space has an attached keymap 319 | that makes it easy to fill, escape, and expand the parenthesis 320 | pair. In particular, when on this space: 321 | 322 | - ')' or ';' eliminates the magic space and exits the parentheses; 323 | 324 | - ',' inserts a spaced comma, leaving point on the magic space; 325 | 326 | - 'C-;' expands the region after the parenthesis pair to 327 | encompass an additional balanced expression; and 328 | 329 | - 'M-;' moves the marked region after the pair (e.g., as 330 | constructed by 'C-;') inside the parentheses, eliminating 331 | leading spaces unless a prefix argument is given. 332 | 333 | Taken together, these make it fast to fill in function calls 334 | or conditionals. 335 | 336 | + ~percent~ 337 | 338 | Binds '%' to the command ~ess-smart-equals-percent~. This provides 339 | expansion, cycling, and completion analogous to ~ess-smart-equals~ 340 | but for %-operators in R. The ~ess-smart-equals~ and 341 | ~ess-smart-equals-percent~ commands can be interleaved at will; 342 | when one follows the other, it will remove operators produced by 343 | the preceding command and start cycling anew. 344 | 345 | With a prefix argument, all of these insert the literal corresponding 346 | character, with repeats if the argument is numeric. 347 | 348 | Additional smart operators may be added in future versions. 349 | 350 | Note that if you change the setting of ~ess-smart-equals-extra-ops~, 351 | you can make it take effect in all relevant buffers by doing 352 | ~M-x ess-smart-equals-refresh-mode~. 353 | 354 | ** Change Log 355 | 356 | + 0.3.0 :: Breaking changes in functionality, design, and configuration. 357 | No longer relies on ~ess-S-assign~ which was deprecated in 358 | ESS. Now provides more powerful context-sensitive, prioritized 359 | operator lists with cycling and completion. The mode is now, 360 | properly, a local minor mode, which can be added automatically 361 | to relevant mode hooks for ESS R modes. Updated required 362 | versions of emacs and ESS. 363 | 364 | + 0.2.2 :: Fix for deprecated ESS variables ~ess-S-assign~ and 365 | ~ess-smart-S-assign-key~. Thanks to Daniel Gomez (@dangom). 366 | 367 | + 0.2.1 :: Initial release with simple insertion and completion, with 368 | space padding for the operators except for a single '=' 369 | used to specify named arguments in function calls. Relies on 370 | ESS variables ~ess-S-assign~ and ~ess-smart-S-assign-key~ 371 | to specify preferred operator for standard assignments. 372 | 373 | ** To Do 374 | 375 | + Allow finer control in context operator lists, both in 376 | distinguishing cycling from completion and in allowing dynamic 377 | operator lists. An example use case would be asking R for the 378 | set of current =%infix%= operators. Some of the infrastructure 379 | for this is already in place. 380 | 381 | + Add ~ess-smart-equals-the-works~ for simple, full feature setup 382 | 383 | + If it is worthwhile, make contexts sticky over cycling to avoid 384 | the context changing during cycling. This does not yet appear 385 | to be needed. 386 | 387 | + Add more tests 388 | 389 | + ... 390 | 391 | ** Contributors 392 | 393 | + Daniel Gomez (@dangom) 394 | 395 | 396 | #+OPTIONS: ^:nil 397 | -------------------------------------------------------------------------------- /ess-smart-equals.el: -------------------------------------------------------------------------------- 1 | ;;; ess-smart-equals.el --- flexible, context-sensitive assignment key for R/S -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2019 Christopher R. Genovese, all rights reserved. 4 | 5 | ;; Author: Christopher R. Genovese 6 | ;; Maintainer: Christopher R. Genovese 7 | ;; Keywords: R, S, ESS, convenience 8 | ;; URL: https://github.com/genovese/ess-smart-equals 9 | ;; Version: 0.3.2 10 | ;; Package-Version: 0.3.2 11 | ;; Package-Requires: ((emacs "25.1") (ess "18.10")) 12 | 13 | 14 | ;;; License: 15 | ;; 16 | ;; This program is free software; you can redistribute it and/or 17 | ;; modify it under the terms of the GNU General Public License as 18 | ;; published by the Free Software Foundation; either version 3, or 19 | ;; (at your option) any later version. 20 | ;; 21 | ;; This program is distributed in the hope that it will be useful, 22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 24 | ;; General Public License for more details. 25 | ;; 26 | ;; You should have received a copy of the GNU General Public License 27 | ;; along with this program; see the file COPYING. If not, write to 28 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 29 | ;; Floor, Boston, MA 02110-1301, USA. 30 | ;; 31 | 32 | 33 | ;;; Commentary: 34 | ;; 35 | ;; Assignment in R is syntactically complicated by a few features: 36 | ;; 1. the historical role of '_' (underscore) as an assignment 37 | ;; character in the S language; 2. the somewhat 38 | ;; inconvenient-to-type, if conceptually pure, '<-' operator as the 39 | ;; preferred assignment operator; 3. the ability to use either an 40 | ;; '=', '<-', and a variety of other operators for assignment; and 41 | ;; 4. the multiple roles that '=' can play, including for setting 42 | ;; named arguments in a function call. 43 | ;; 44 | ;; This package offers a flexible, context-sensitive assignment key 45 | ;; for R and S that is, by default, tied to the '=' key. This key 46 | ;; inserts or completes relevant, properly spaced operators 47 | ;; (assignment, comparison, etc.) based on the syntactic context in 48 | ;; the code. It allows very easy cycling through the possible 49 | ;; operators in that context. The contexts, the operators, and 50 | ;; their cycling order in each context are customizable. 51 | ;; 52 | ;; The package defines a minor mode `ess-smart-equals-mode', 53 | ;; intended for S-language modes (e.g., ess-r-mode, 54 | ;; inferior-ess-r-mode, and ess-r-transcript-mode), that when 55 | ;; enabled in a buffer activates the '=' key to to handle 56 | ;; context-sensitive completion and cycling of relevant operators. 57 | ;; When the mode is active and an '=' is pressed: 58 | ;; 59 | ;; 1. With a prefix argument or in specified contexts (which for 60 | ;; most major modes means in strings or comments), just 61 | ;; insert '='. 62 | ;; 63 | ;; 2. If an operator relevant to the context lies before point 64 | ;; (with optional whitespace), it is replaced, cyclically, by the 65 | ;; next operator in the configured list for that context. 66 | ;; 67 | ;; 3. Otherwise, if a prefix of an operator relevant to the 68 | ;; context lies before point, that operator is completed. 69 | ;; 70 | ;; 4. Otherwise, the highest priority relevant operator is inserted 71 | ;; with surrounding whitespace (see `ess-smart-equals-no-spaces'). 72 | ;; 73 | ;; Consecutive presses of '=' cycle through the relevant operators. 74 | ;; After an '=', a backspace (or other configurable keys) removes 75 | ;; the last operator and tab offers a choice of operators by completion. 76 | ;; (Shift-backspace will delete one character only and restore the 77 | ;; usual maning of backspace.) See `ess-smart-equals-cancel-keys'. 78 | ;; 79 | ;; By default, the minor mode activates the '=' key, but this can 80 | ;; be customized by setting the option `ess-smart-equals-key' before 81 | ;; this package is loaded. 82 | ;; 83 | ;; The function `ess-smart-equals-activate' arranges for the minor mode 84 | ;; to be activated by mode hooks for any given list of major modes, 85 | ;; defaulting to ESS major modes associated with R (ess-r-mode, 86 | ;; inferior-ess-r-mode, ess-r-transcript-mode, ess-roxy-mode). 87 | ;; 88 | ;; Examples 89 | ;; -------- 90 | ;; In the left column below, ^ marks the location at which an '=' 91 | ;; key is pressed, the remaining columns show the result of 92 | ;; consecutive presses of '=' using the package's default settings. 93 | ;; position of point. 94 | ;; 95 | ;; Before '=' Press '=' Another '=' Another '=' 96 | ;; ---------- --------- ----------- ----------- 97 | ;; foo^ foo <- ^ foo <<- ^ foo = ^ 98 | ;; foo ^ foo <- ^ foo <<- ^ foo = ^ 99 | ;; foo<^ foo <- ^ foo <<- ^ foo = ^ 100 | ;; foo=^ foo = ^ foo -> ^ foo ->> ^ 101 | ;; foo(a^ foo(a = ^ foo( a == ^ foo( a != ^ 102 | ;; if( foo=^ if( foo == ^ if( foo != ^ if( foo <= ^ 103 | ;; if( foo<^ if( foo < ^ if( foo > ^ if( foo >= ^ 104 | ;; "foo ^ "foo =^ "foo ==^ "foo ===^ 105 | ;; #...foo ^ #...foo =^ #...foo ==^ #...foo ===^ 106 | ;; 107 | ;; 108 | ;; As a bonus, the value of the variable 109 | ;; `ess-smart-equals-extra-ops' when this package is loaded, 110 | ;; determines some other smart operators that may prove useful. 111 | ;; Currently, only `brace', `paren', and `percent' are supported, 112 | ;; causing `ess-smart-equals-open-brace', 113 | ;; `ess-smart-equals-open-paren', and `ess-smart-equals-percent' 114 | ;; to be bound to '{', '(', and '%', respectively. The first two 115 | ;; of these configurably places a properly indented and spaced 116 | ;; matching pair at point or around the region if active. The 117 | ;; paren pair also includes a magic space with a convenient keymap 118 | ;; for managing parens. See the readme. See the customizable 119 | ;; variable `ess-smart-equals-brace-newlines' for configuring the 120 | ;; newlines in braces. The third operator 121 | ;; (`ess-smart-equals-percent') performs matching of %-operators. 122 | ;; 123 | ;; Finally, the primary user facing functions are named with a 124 | ;; prefix `ess-smart-equals-' to avoid conflicts with other 125 | ;; packages. Because this is long, the internal functions and 126 | ;; objects use a shorter (but still distinctive) prefix `essmeq-'. 127 | ;; 128 | ;; 129 | ;; Installation and Initialization 130 | ;; ------------------------------- 131 | ;; The package can be loaded from MELPA using `package-install' or another 132 | ;; Emacs package manager. Alternatively, you can clone or download the source 133 | ;; directly from the github repository and put the file `ess-smart-equals.el' 134 | ;; in your Emacs load path. 135 | ;; 136 | ;; A variety of activation options is described below, but tl;dr: 137 | ;; the recommended way to activate the mode (e.g., in your init 138 | ;; file) is either directly with 139 | ;; 140 | ;; (setq ess-smart-equals-extra-ops '(brace paren percent)) 141 | ;; (with-eval-after-load 'ess-r-mode 142 | ;; (require 'ess-smart-equals) 143 | ;; (ess-smart-equals-activate)) 144 | ;; 145 | ;; or with use-package: 146 | ;; 147 | ;; (use-package ess-smart-equals 148 | ;; :init (setq ess-smart-equals-extra-ops '(brace paren percent)) 149 | ;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 150 | ;; :config (ess-smart-equals-activate)) 151 | ;; 152 | ;; A more detailed description follows, if you want to see variations. 153 | ;; 154 | ;; To activate, you need only do 155 | ;; 156 | ;; (with-eval-after-load 'ess-r-mode 157 | ;; (require 'ess-smart-equals) 158 | ;; (ess-smart-equals-activate)) 159 | ;; 160 | ;; somewhere in your init file, which will add `ess-smart-equals-mode' to 161 | ;; a prespecified (but customizable) list of mode hooks. 162 | ;; 163 | ;; For those who use the outstanding `use-package', you can do 164 | ;; 165 | ;; (use-package ess-smart-equals 166 | ;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 167 | ;; :config (ess-smart-equals-activate)) 168 | ;; 169 | ;; somewhere in your init file. An equivalent but less concise version 170 | ;; of this is 171 | ;; 172 | ;; (use-package ess-smart-equals 173 | ;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 174 | ;; :hook ((ess-r-mode . ess-smart-equals-mode) 175 | ;; (inferior-ess-r-mode . ess-smart-equals-mode) 176 | ;; (ess-r-transcript-mode . ess-smart-equals-mode) 177 | ;; (ess-roxy-mode . ess-smart-equals-mode)) 178 | ;; 179 | ;; To also activate the extra smart operators and bind them automatically, 180 | ;; you can replace this with 181 | ;; 182 | ;; (use-package ess-smart-equals 183 | ;; :init (setq ess-smart-equals-extra-ops '(brace paren percent)) 184 | ;; :after (:any ess-r-mode inferior-ess-r-mode ess-r-transcript-mode) 185 | ;; :config (ess-smart-equals-activate)) 186 | ;; 187 | ;; Details on customization are provided in the README file. 188 | ;; 189 | ;; Testing 190 | ;; ------- 191 | ;; To run the tests, install cask and do `cask install' in the 192 | ;; ess-smart-equals project directory. Then, at the command line, 193 | ;; from the project root directory do 194 | ;; 195 | ;; cask exec ert-runner 196 | ;; cask exec ecukes --reporter magnars 197 | ;; 198 | ;; and if manual testing is desired do 199 | ;; 200 | ;; cask emacs -Q -l test/manual-init.el --eval '(cd "~/")' & 201 | ;; 202 | ;; Additional test cases are welcome in pull requests. 203 | ;; 204 | 205 | ;;; Change Log: 206 | ;; 207 | ;; 0.3.x -- Breaking changes in functionality, design, and configuration. 208 | ;; No longer relies on `ess-S-assign' which was deprecated in 209 | ;; ESS. Now provides more powerful context-sensitive, prioritized 210 | ;; operator lists with cycling and completion. The mode is now, 211 | ;; properly, a local minor mode, which can be added automatically 212 | ;; to relevant mode hooks for ESS R modes. Updated required 213 | ;; versions of emacs and ESS. 214 | ;; 215 | ;; 0.2.2 -- Fix for deprecated ESS variables `ess-S-assign' and 216 | ;; `ess-smart-S-assign-key'. Thanks to Daniel Gomez (@dangom). 217 | ;; 218 | ;; 0.2.1 -- Initial release with simple insertion and completion, with 219 | ;; space padding for the operators except for a single '=' 220 | ;; used to specify named arguments in function calls. Relies on 221 | ;; ESS variables `ess-S-assign' and `ess-smart-S-assign-key' 222 | ;; to specify preferred operator for standard assignments. 223 | 224 | ;;; Code: 225 | 226 | (eval-when-compile (require 'cl-lib)) 227 | (eval-when-compile (require 'subr-x)) 228 | (eval-when-compile (require 'pcase)) 229 | (require 'map) 230 | (require 'skeleton) 231 | 232 | (require 'ess-r-mode) ;; included in ess package 233 | 234 | 235 | ;;; Utility Macros 236 | 237 | (defmacro essmeq--with-struct-slots (type spec-list inst &rest body) 238 | "Execute BODY with vars in SPEC-LIST bound to slots in struct INST of TYPE. 239 | TYPE is an unquoted symbol corresponding to a type defined by 240 | `cl-defstruct'. SPEC-LIST is a list, each of whose entries can 241 | either be a symbol naming both a slot and a variable or a list of 242 | two symbols (VAR SLOT) associating VAR with the specified SLOT. 243 | INST is an expression giving a structure of type TYPE as defined 244 | by `cl-defstruct', and BODY is a list of forms. 245 | 246 | This code was based closely on code given at 247 | www.reddit.com/r/emacs/comments/8pbbpe/why_no_withslots_for_cldefstruct/ 248 | which was in turn borrowed from the EIEIO package." 249 | (declare (indent 3) (debug (sexp sexp sexp def-body))) 250 | (let ((obj (make-symbol "struct"))) 251 | `(let ((,obj ,inst)) 252 | (cl-symbol-macrolet ;; each spec => a symbol macro to an (aref ....) 253 | ,(mapcar (lambda (entry) 254 | (let* ((slot-var (if (listp entry) (car entry) entry)) 255 | (slot (if (listp entry) (cadr entry) entry)) 256 | (idx (cl-struct-slot-offset type slot))) 257 | (list slot-var `(aref ,obj ,idx)))) 258 | spec-list) 259 | (unless (cl-typep ,obj ',type) 260 | (error "%s is not of type %s" ',inst ',type)) 261 | ,(if (cdr body) `(progn ,@body) (car body)))))) 262 | 263 | (defmacro essmeq--with-matcher (spec-list inst &rest body) 264 | "Execute BODY with vars in SPEC-LIST bound to slots essmeq-matcher INST. 265 | SPEC-LIST is a list, each of whose entries can either be a symbol 266 | naming both a slot and a variable or a list of two symbols (VAR 267 | SLOT) associating VAR with the specified SLOT. INST is an 268 | expression giving a structure of type essmeq-matcher, and BODY is 269 | a list of forms." 270 | (declare (indent 2) (debug (sexp sexp def-body))) 271 | `(essmeq--with-struct-slots essmeq-matcher ,spec-list ,inst 272 | ,@body)) 273 | 274 | (defmacro essmeq--with-temporary-insert (text where &rest body) 275 | "Inserting TEXT after point, execute BODY, delete TEXT. 276 | Returns the value of BODY and does not change point." 277 | (declare (indent 2) (debug (sexp def-body))) 278 | (let ((txt (make-symbol "text")) 279 | (len (make-symbol "text-len")) 280 | (after (eq where :after))) 281 | `(let ((,txt ,text) 282 | (,len ,(if (stringp text) (length text) `(length ,txt)))) 283 | (save-excursion 284 | ,(if after `(save-excursion (insert ,txt)) `(insert ,txt)) 285 | (prog1 (save-excursion ,@body) 286 | (delete-char ,(if after len `(- ,len)))))))) 287 | 288 | (defmacro essmeq--with-markers (specs &rest body) 289 | "Execute BODY with markers defined by SPEC. Markers cleared after BODY forms. 290 | Return the value of the last form in BODY. Markers are guaranteed 291 | to be cleared even if BODY exits non-locally. Note that as a 292 | consequence, the markers themselves should not be returned. If 293 | any of the markers is a desired value of this form, either 294 | `ess-smart-equals-copy-marker' or `copy-marker' should be used, 295 | but note that unlike the former, the latter does not copy the 296 | insertion type of the marker by default. 297 | 298 | SPEC is a list whose elements must have one of the following 299 | forms: 1. SYMBOL; 2. (SYMBOL POSITION), where POSITION is an 300 | expression used to initialize the marker as in the corresponding 301 | argument to `set-marker'; or 3. (SYMBOL POSITION INSERTION-TYPE), 302 | were INSERTION-TYPE is either t or nil (the default) as in 303 | `set-marker-insertion-type'. 304 | 305 | If POSITION is one of the forms (point), (point-min), 306 | or (point-max), those expressions are not evaluated but instead 307 | the marker is created with the corresponding `point-marker', 308 | `point-min-marker', or `point-max-marker', respectively." 309 | (declare (indent 1) (debug (sexp def-body))) 310 | (let ((marker-symbols (mapcar (lambda (s) (if (listp s) (car s) s)) specs)) 311 | (marker-info (mapcar (lambda (s) (if (listp s) (cadr s) nil)) specs)) 312 | (marker-itypes 313 | (delq nil (mapcar (lambda (s) 314 | (if (and (listp s) (caddr s)) 315 | (cons (car s) (caddr s)) 316 | nil)) 317 | specs)))) 318 | (unless (cl-every #'symbolp marker-symbols) 319 | (error "Only symbols allowed in first entry of marker specification.")) 320 | `(let (,@(cl-map 'list (lambda (s i) 321 | (cond 322 | ((equal i '(point)) 323 | `(,s (point-marker))) 324 | ((equal i '(point-min)) 325 | `(,s (point-min-marker))) 326 | ((equal i '(point-max)) 327 | `(,s (point-max-marker))) 328 | ((null i) 329 | `(,s (make-marker))) 330 | (t 331 | `(,s (set-marker (make-marker) ,i))))) 332 | marker-symbols marker-info)) 333 | ,@(mapcar (lambda (s) `(set-marker-insertion-type ,(car s) ,(cdr s))) 334 | marker-itypes) 335 | (unwind-protect ,(if (cdr body) `(progn ,@body) (car body)) 336 | ,@(mapcar (lambda (m) `(set-marker ,m nil)) marker-symbols))))) 337 | 338 | 339 | ;;; Marker Interface 340 | 341 | (defun ess-smart-equals-make-marker (&optional position type init) 342 | "Like `make-marker' but also optionally initializes POSITION and TYPE. 343 | POSITION can be any value of the same argument in `set-marker'. 344 | TYPE is nil or t, as with the corresponding argument to 345 | `set-marker-insertion-type'. INIT, if non-nil, should be nullary 346 | function (e.g,. point-marker) to be called instead of `make-marker' 347 | to initialize the marker 348 | 349 | Returns the initialized marker." 350 | (let ((m (if init (funcall init) (make-marker)))) 351 | (when position (set-marker m position)) 352 | (when type (set-marker-insertion-type m t)) 353 | m)) 354 | 355 | (defun ess-smart-equals-copy-marker (&optional marker type) 356 | "Like `copy-marker' but copies insertion type if MARKER but not TYPE is given." 357 | (copy-marker marker (or type (and marker (marker-insertion-type marker))))) 358 | 359 | (defsubst ess-smart-equals-clear-marker (marker) 360 | "Reset MARKER so that it points nowhere and does not affect current buffer." 361 | (set-marker marker nil)) 362 | 363 | 364 | ;;; Behavior Configuration 365 | 366 | (defcustom ess-smart-equals-padding-left 'one-space 367 | "Specifies padding used on left side of inserted and completed operators. 368 | 369 | This can have one of the following values: 370 | 371 | * The symbol `one-space' means to insert exactly one space, eliminating 372 | any other contiguous whitespace on the left. 373 | 374 | * The symbol `no-space' means to eliminate all adjacent whitespace on 375 | the left. 376 | 377 | * The symbol `some-space' means to ensure there is at least one space 378 | on the left, either in existing whitespace (which is kept as is) 379 | or by inserting a space if none. 380 | 381 | * The symbol `none' means to insert no padding and make no change 382 | to the surrounding whitespace. (A nil value has the same effect 383 | but is marginally slower.) 384 | 385 | * A string means to insert that string on the left. 386 | 387 | * A function with signature (begin-ws begin &optional extent) 388 | 389 | When inserting or completing an operator this function 390 | should insert desired padding on the right. The function is 391 | called within a `save-excursion', so point can be moved and 392 | insertions made. In this case, the function is called with 393 | two positions: BEGIN-WS is the position of the leftmost 394 | contiguous whitespace character to the left of the operator 395 | and BEGIN is the position of the left side of the operator. 396 | 397 | When removing an operator, this function should return the 398 | beginning of the padded region assuming that an operator has 399 | just been inserted and padded (i.e., by calling this 400 | function). It should not change the current buffer. This 401 | case is distinguished by having the third argument EXTENT eq 402 | to t and *both* BEGIN-WS and BEGIN pointing to the leftmost 403 | point of the inserted operator. 404 | " 405 | :group 'ess-edit 406 | :type '(choice (const :tag "Only One Space" one-space) 407 | (const :tag "No Spaces" no-space) 408 | (const :tag "At Least One Space" some-space) 409 | (const :tag "No Padding" none) 410 | string 411 | function)) 412 | 413 | (defcustom ess-smart-equals-padding-right 'one-space 414 | "Specifies padding used on right side of inserted and completed operators. 415 | 416 | This can have one of the following values: 417 | 418 | * The symbol `one-space' means to insert exactly one space, eliminating 419 | any other contiguous whitespace on the right. 420 | 421 | * The symbol `no-space' means to eliminate all adjacent whitespace on 422 | the right. 423 | 424 | * The symbol `some-space' means to ensure there is at least one space 425 | on the right, either in existing whitespace (which is kept as is) 426 | or by inserting a space if none. 427 | 428 | * The symbol `none' means to insert no padding and make no change 429 | to the surrounding whitespace. (A nil value has the same effect 430 | but is marginally slower.) 431 | 432 | * A string means to insert that string on the right. 433 | 434 | * A function with signature (end end-ws &optional extent) 435 | 436 | When inserting or completing an operator this function 437 | should insert desired padding on the right. The function is 438 | called within a `save-excursion', so point can be moved and 439 | insertions made. In this case, the function is called with 440 | two positions: END is the position of the right side of the 441 | inserted operator and END-WS is the position of the 442 | rightmost contiguous whitespace character to the right of 443 | the operator. 444 | 445 | When removing an operator, this function should return the 446 | beginning of the padded region assuming that an operator has 447 | just been inserted and padded (i.e., by calling this 448 | function). It should not change the current buffer. This 449 | case is distinguished by having the third argument EXTENT eq 450 | to t and *both* END and END-WS pointing to the rightmost 451 | point of the inserted operator. 452 | " 453 | :group 'ess-edit 454 | :type '(choice (const :tag "Only One Space" one-space) 455 | (const :tag "No Spaces" no-space) 456 | (const :tag "At Least One Space" some-space) 457 | (const :tag "No Padding" none) 458 | string 459 | function)) 460 | 461 | (defvar-local ess-smart-equals-narrow-function nil 462 | "If non-nil, a nullary function to restrict syntax checking to a region. 463 | This is useful in cases such as `inferior-ess-r-mode' where 464 | attention should be focused on a prompt line or the region 465 | between prompts, both for efficiency and because output or 466 | erroneous input on earlier prompts can confuse the syntax 467 | checker. See `ess-smart-equals-repl-narrow' and 468 | `ess-smart-equals-mode-options'.") 469 | 470 | (defcustom ess-smart-equals-insertion-hook nil 471 | "A function called when an operator is inserted into the current buffer. 472 | This (non-standard hook) function should accept six arguments 473 | 474 | CONTEXT MATCH-TYPE STRING START OLD-END PAD 475 | 476 | where CONTEXT is a context symbol, representing a key in the 477 | inner alists of `ess-smart-equals-contexts'; MATCH-TYPE is one of 478 | the keywords :exact, :partial, :no-match, or :literal; STRING is 479 | the operator string that was inserted; START is the buffer 480 | positions at the beginning of the inserted string (plus padding); 481 | OLD-END was the ending position of the previous content in the 482 | buffer; and PAD is a string giving the padding used on either 483 | side of the inserted operator, typically either empty or a single 484 | space. 485 | 486 | This feature is experimental and may be removed in a future version." 487 | :group 'ess-edit 488 | :type '(choice (const :tag "None" nil) function)) 489 | 490 | (defcustom ess-smart-equals-default-modes 491 | '(ess-r-mode inferior-ess-r-mode ess-r-transcript-mode ess-roxy-mode) 492 | "List of major modes where `ess-smart-equals-activate' binds '=' by default." 493 | :group 'ess-edit 494 | :type '(repeat symbol)) 495 | 496 | (defcustom ess-smart-equals-brace-newlines '((open after) 497 | (close before)) 498 | "Controls auto-newlines for braces in `electric-smart-equals-open-brace'. 499 | Only applicable when `ess-smart-equals-extra-ops' contains the 500 | symbol `brace'. This is an alist with keys `open' and `close' and 501 | with values that are lists containing the symbols `after' and/or 502 | `before', indicating when a newline should be placed. A missing 503 | key is equivalent to a nil value, meaning to place no newlines. 504 | 505 | This can be controlled via Emacs's customization mechanism or can 506 | be added to your ESS style specification, as preferred." 507 | :group 'ess-edit 508 | :type '(alist :key-type (choice (const open) (const close)) 509 | :value-type (repeat (choice (const before) (const after))))) 510 | 511 | 512 | ;;; Specialized overriding context and transient exit functions 513 | 514 | (defvar-local ess-smart-equals-overriding-context nil 515 | "If non-nil, a context symbol that overrides the usual context calculation. 516 | Intended to be used in a transient manner, see 517 | `ess-smart-equals-transient-exit-function'.") 518 | 519 | (defvar-local ess-smart-equals-transient-exit-function nil 520 | "If non-nil, a nullary function to be called on exit from the transient keymap. 521 | This can be used, for instance, to clear an overriding context. 522 | See `essmeq--transient-map'") 523 | 524 | (defvar-local essmeq--stop-transient nil 525 | "A nullary function called to deactivate the most recent transient map. 526 | This is set automatically and should not be set explicitly. If 527 | non-nil, a nullary function to be called on exit from the 528 | transient keymap. This can be used, for instance, to clear an 529 | overriding context if something goes awry. See 530 | `essmeq--transient-map'.") 531 | 532 | (defun ess-smart-equals-clear-overriding-context () 533 | "Transient exit function that resets both itself and any overriding context. 534 | This is a convenience function for fixing a context during one 535 | cycle of smart equals insertion. See 536 | `ess-smart-equals-overriding-context' and 537 | `ess-smart-equals-transient-exit-function'.." 538 | (setq ess-smart-equals-overriding-context nil 539 | ess-smart-equals-transient-exit-function nil)) 540 | 541 | (defun ess-smart-equals-set-overriding-context (context) 542 | "Force context to be symbol CONTEXT for next insertion only. 543 | This sets `ess-smart-equals-transient-exit-function' to clear the context 544 | the next time the transient map in `ess-smart-equals' exits." 545 | (setq ess-smart-equals-overriding-context context 546 | ess-smart-equals-transient-exit-function 547 | #'ess-smart-equals-clear-overriding-context)) 548 | 549 | 550 | ;;; Key Configuration and Utilities 551 | 552 | (defun ess-smart-equals-refresh-mode () 553 | "Re-enable `ess-smart-equals-mode' in all buffers where it is enabled. 554 | This has the effect of refreshing all the mode's keymaps, 555 | contexts, and options. It is intended for use in customization 556 | setters for options that affect pre-computed tables or keymaps, 557 | but it can be used interactively as well, for instance, after 558 | manually updating the values of such options." 559 | (interactive) 560 | (dolist (buf (buffer-list)) 561 | (with-current-buffer buf 562 | (when (and (boundp 'ess-smart-equals-mode) ess-smart-equals-mode) 563 | (let ((inhibit-message t)) 564 | (ess-smart-equals-mode 1)))))) 565 | 566 | (defcustom ess-smart-equals-key "=" 567 | "The key for smart assignment operators when `ess-smart-equals-mode' active. 568 | 569 | For changes in this variable to take effect, some precomputed 570 | information must be refreshed in `ess-smart-equals-mode' buffers. 571 | Changing the variable through the customization mechanism does 572 | such a refresh automatically. If instead you manually change the 573 | value of this option (e.g., with `setq'), you can either disabled 574 | and re-enabled the minor mode in one such buffer or do 575 | 576 | M-x ess-smart-equals-refresh-mode 577 | 578 | interactively, or (ess-smart-equals-refresh-mode) in lisp, to 579 | make this change take effect." 580 | :group 'ess-edit 581 | :type 'string 582 | :initialize 'custom-initialize-default 583 | :set (lambda (sym val) 584 | (set-default sym val) 585 | (ess-smart-equals-refresh-mode))) 586 | 587 | (defcustom ess-smart-equals-extra-ops nil 588 | "If non-nil, a symbol list of extra smart operators to bind in the mode map. 589 | Currently, only `brace' and `paren' are supported. 590 | 591 | For changes in this variable to take effect, some precomputed 592 | information must be refreshed in `ess-smart-equals-mode' buffers. 593 | Changing the variable through the customization mechanism does 594 | such a refresh automatically. If instead you manually change the 595 | value of this option (e.g., with `setq'), you can either disabled 596 | and re-enabled the minor mode in one such buffer or do 597 | 598 | M-x ess-smart-equals-refresh-mode 599 | 600 | interactively, or (ess-smart-equals-refresh-mode) in lisp, to 601 | make this change take effect." 602 | :group 'ess-edit 603 | :type '(choice (const nil) (repeat (const brace) (const paren))) 604 | :initialize 'custom-initialize-default 605 | :set (lambda (sym val) 606 | (set-default sym val) 607 | (ess-smart-equals-refresh-mode))) 608 | 609 | (defcustom ess-smart-equals-cancel-keys (list [backspace] 610 | (kbd "")) 611 | "List of keys transiently bound to cancel operator insertion or cycling. 612 | A shifted version of each will instead delete backwards a 613 | character, clearing the transient keymap and making it easy to 614 | delete only part of an operator if desired. 615 | 616 | For changes in this variable to take effect, some precomputed 617 | information must be refreshed in `ess-smart-equals-mode' buffers. 618 | Changing the variable through the customization mechanism does 619 | such a refresh automatically. If instead you manually change the 620 | value of this option (e.g., with `setq'), you can either disabled 621 | and re-enabled the minor mode in one such buffer or do 622 | 623 | M-x ess-smart-equals-refresh-mode 624 | 625 | interactively, or (ess-smart-equals-refresh-mode) in lisp, to 626 | make this change take effect." 627 | :group 'ess-edit 628 | :type '(repeat 629 | (choice string (restricted-sexp :match-alternatives (vectorp)))) 630 | :initialize 'custom-initialize-default 631 | :set (lambda (sym val) 632 | (set-default sym val) 633 | (ess-smart-equals-refresh-mode))) 634 | 635 | (defun essmeq--transient-equals (&optional literal) 636 | "A version of `ess-smart-equals' for use in the transient key map. 637 | This detects previous use of `ess-smart-equals-percent' and clears that 638 | operator if the user switches to equals." 639 | (interactive "P") 640 | (ignore literal) 641 | (when (eq last-command 'ess-smart-equals-percent) 642 | (let ((ess-smart-equals-overriding-context '%)) 643 | (essmeq--remove 'only-match))) 644 | (call-interactively #'ess-smart-equals)) 645 | 646 | (defun essmeq--make-transient-map (&optional cancel-keys) 647 | "Resets transient keymap used after `ess-smart-equals'. 648 | CANCEL-KEYS, if non-nil, is a list of keys in that map that will 649 | clear the last insertion. It defaults to 650 | `ess-smart-equals-cancel-keys', which see. See also 651 | `essmeq--transient-map'." 652 | (let ((cancel-keys (or cancel-keys ess-smart-equals-cancel-keys)) 653 | (percentp (memq 'percent ess-smart-equals-extra-ops)) 654 | (map (make-sparse-keymap))) 655 | (if (not percentp) 656 | (define-key map (kbd ess-smart-equals-key) #'ess-smart-equals) 657 | (define-key map "%" #'ess-smart-equals-percent) 658 | (define-key map (kbd ess-smart-equals-key) #'essmeq--transient-equals)) 659 | (define-key map "\t" #'essmeq--selected) 660 | (dolist (key cancel-keys) 661 | (define-key map key #'essmeq--remove) 662 | (when (and (or (stringp key) (vectorp key)) 663 | (= (length key) 1)) 664 | (define-key map ;; make shift-cancel just do regular backspace 665 | (vector (if (listp (aref key 0)) 666 | (cons 'shift (aref key 0)) 667 | (list 'shift (aref key 0)))) 668 | 'delete-backward-char))) 669 | map)) 670 | 671 | (defun essmeq--make-mode-map () 672 | "Returns the `ess-smart-equals-mode' keymap using current parameter values." 673 | (let ((map (make-sparse-keymap))) 674 | (define-key map ess-smart-equals-key 'ess-smart-equals) 675 | (when (memq 'brace ess-smart-equals-extra-ops) 676 | (define-key map "{" 'ess-smart-equals-open-brace)) 677 | (when (memq 'paren ess-smart-equals-extra-ops) 678 | (define-key map "(" 'ess-smart-equals-open-paren)) 679 | (when (memq 'percent ess-smart-equals-extra-ops) 680 | (define-key map "%" 'ess-smart-equals-percent)) 681 | map)) 682 | 683 | (defvar ess-smart-equals-mode-map (essmeq--make-mode-map) 684 | "Keymap used in `ess-smart-equals-mode' binding smart operators.") 685 | 686 | (defvar essmeq--transient-map (essmeq--make-transient-map) 687 | "Map bound transiently after `ess-smart-equals' key is pressed. 688 | The map continues to be active as long as that key is pressed.") 689 | 690 | (defun ess-smart-equals-update-keymaps () 691 | "Force update of `ess-smart-equals-mode' keymaps to adjust for config changes. 692 | This should not usually need to be done explicitly by the user." 693 | (interactive) 694 | ;; The `ess-smart-equals-mode' entry in `minor-mode-map-alist' is identical 695 | ;; to `ess-smart-equals-mode-map', if the map is set. In this case, 696 | ;; simply doing `setq' will break the synchrony and the new map will 697 | ;; not be reflected in the minor mode bindings. So we use `setcdr' instead. 698 | (if (keymapp ess-smart-equals-mode-map) 699 | (setcdr ess-smart-equals-mode-map (cdr (essmeq--make-mode-map))) 700 | (setq ess-smart-equals-mode-map (essmeq--make-mode-map))) 701 | (setq essmeq--transient-map (essmeq--make-transient-map))) 702 | 703 | (defun essmeq--keep-transient () 704 | "Predicate that returns t when the transient keymap should be maintained." 705 | (let ((command-keys (this-command-keys-vector))) 706 | (or (equal command-keys (vconcat ess-smart-equals-key)) 707 | (and (memq 'percent ess-smart-equals-extra-ops) 708 | (equal command-keys (vector ?%)))))) 709 | 710 | 711 | ;;; Buffer Contents 712 | 713 | (defun essmeq--whitespace-span-forward (&optional position) 714 | "Scan forward from POSITION to the end of contiguous whitespace. 715 | Return the position after contiguous whitespace but stopping at 716 | any character with a non-nil `essmeq--magic-space' text property. 717 | POS defaults to point." 718 | (let ((pos (or position (point)))) 719 | (save-excursion 720 | (when position (goto-char pos)) 721 | (skip-syntax-forward " ") 722 | (let* ((after-ws (point)) 723 | (magic-pos (text-property-any pos after-ws 'essmeq--magic-space t))) 724 | (or magic-pos after-ws))))) 725 | 726 | (defun essmeq--whitespace-span-backward (&optional position) 727 | "Scan backward from POSITION to the beginning of contiguous whitespace. 728 | Return the position at the start of contiguous whitespace. POS 729 | defaults to point." 730 | (let ((pos (or position (point)))) 731 | (save-excursion 732 | (when position (goto-char pos)) 733 | (skip-syntax-backward " ") 734 | (point)))) 735 | 736 | (defun essmeq--find-padded-region (beg end) 737 | "Find the padding extent for unpadded text spanning BEG..END in current buffer. 738 | The assumption is that the text has been inserted with padding 739 | according to the padding rules specified by 740 | `ess-smart-equals-padding-left' and 741 | `ess-smart-equals-padding-right', which see. This assumption is 742 | not checked; specifically, this does not check that BEG..END is 743 | free of spaces nor that the padding characters around that region 744 | are correct. 745 | 746 | Return (BEG' . END') where BEG' and END' are the beginning and ending 747 | positions of the padded region BEG..END under the padding rules." 748 | (cons 749 | (cond ;; left padding (must come second to avoid affecting end) 750 | ((memq ess-smart-equals-padding-left '(one-space some-space)) 751 | (1- beg)) 752 | ((memq ess-smart-equals-padding-left '(no-space none)) 753 | beg) 754 | ((stringp ess-smart-equals-padding-left) 755 | (- beg (length ess-smart-equals-padding-left))) 756 | ((functionp ess-smart-equals-padding-left) 757 | (funcall ess-smart-equals-padding-left beg beg t))) 758 | (cond ;; right padding 759 | ((memq ess-smart-equals-padding-right '(one-space some-space)) 760 | (1+ end)) 761 | ((memq ess-smart-equals-padding-right '(no-space none)) 762 | end) 763 | ((stringp ess-smart-equals-padding-right) 764 | (+ end (length ess-smart-equals-padding-right))) 765 | ((functionp ess-smart-equals-padding-right) 766 | (funcall ess-smart-equals-padding-right end end t))))) 767 | 768 | (defun essmeq--normalize-padding (beg end) 769 | "Adjust space padding on either side of BEG and END in the current buffer. 770 | Return (BEG' . END') where BEG' and END' are the beginning and ending 771 | positions of the padded region BEG..END that account for insertions 772 | and deletions. 773 | 774 | The type of padding used, if any, on each side is determined by 775 | the values of the options `ess-smart-equals-padding-left' and 776 | `ess-smart-equals-padding-right', which see." 777 | (let ((beg-ws (essmeq--whitespace-span-backward beg)) 778 | (end-ws (essmeq--whitespace-span-forward end))) 779 | (essmeq--with-markers ((mbeg beg) (mend end t)) 780 | (cond ;; right padding 781 | ((eq ess-smart-equals-padding-right 'one-space) 782 | (delete-region end end-ws) 783 | (save-excursion (goto-char mend) (insert " "))) 784 | ((eq ess-smart-equals-padding-right 'some-space) 785 | (unless (> end-ws end) 786 | (save-excursion (goto-char mend) (insert " ")))) 787 | ((eq ess-smart-equals-padding-right 'no-space) 788 | (delete-region end end-ws)) 789 | ((eq ess-smart-equals-padding-right 'none)) 790 | ((stringp ess-smart-equals-padding-right) 791 | (save-excursion 792 | (goto-char mend) 793 | (insert ess-smart-equals-padding-right))) 794 | ((functionp ess-smart-equals-padding-right) 795 | (save-excursion 796 | (goto-char mend) 797 | (funcall ess-smart-equals-padding-right end end-ws)))) 798 | (cond ;; left padding (must come second to avoid affecting end) 799 | ((eq ess-smart-equals-padding-left 'one-space) 800 | (delete-region beg-ws beg) 801 | (save-excursion (goto-char mbeg) (insert " "))) 802 | ((eq ess-smart-equals-padding-left 'some-space) 803 | (unless (< beg-ws beg) 804 | (save-excursion (goto-char mbeg) (insert " ")))) 805 | ((eq ess-smart-equals-padding-left 'no-space) 806 | (delete-region beg-ws beg)) 807 | ((eq ess-smart-equals-padding-left 'none)) 808 | ((stringp ess-smart-equals-padding-left) 809 | (save-excursion 810 | (goto-char mbeg) 811 | (insert ess-smart-equals-padding-left))) 812 | ((functionp ess-smart-equals-padding-left) 813 | (save-excursion 814 | (goto-char mbeg) 815 | (funcall ess-smart-equals-padding-left beg-ws beg)))) 816 | (cons (marker-position mbeg) (marker-position mend))))) 817 | 818 | (defun essmeq--replace-region (text start end &optional ignore-padding) 819 | "Replace region START..END with TEXT plus optional padding in current buffer. 820 | Padding is determined by customization options as in 821 | `essmeq--normalize-padding', but if IGNORE-PADDING is non-nil, 822 | these settings are ignored and no padding is added. 823 | 824 | Return (TSTART . TEND) giving, respectively, the starting and ending 825 | positions of the (padded) text." 826 | (essmeq--with-markers ((mstart start) (mend end t)) 827 | (delete-region mstart mend) 828 | (save-excursion 829 | (goto-char mstart) 830 | (insert text)) 831 | (if ignore-padding 832 | (cons (marker-position mstart) (marker-position mend)) 833 | (essmeq--normalize-padding mstart mend)))) 834 | 835 | (defun essmeq--after-whitespace-p (&optional pos) 836 | "Does POS (point by default) follow a whitespace character?" 837 | (eq (char-syntax (char-before pos)) ?\ )) 838 | 839 | 840 | ;;; Finite-State Machine for Operator Matching 841 | ;; 842 | ;; We do backwards anchored matching of operator lists using a 843 | ;; pre-built finite-state machine. This offers several advantages 844 | ;; over a direct sequence of regular expression matches. First, in 845 | ;; benchmarks with compiled code, the FSM matcher gives comparable, 846 | ;; though typically better, performance than the regexp approach. 847 | ;; Second, backwards regex matching in emacs (excluding 848 | ;; looking-back, which is slow) does not give the longest match, 849 | ;; requiring disambiguation between say '<-' and '<<-'. Third, we 850 | ;; can handle partial matches automatically with the information 851 | ;; computed at fsm build time, making it easy to offer completion. 852 | ;; Fourth, we can control priority order in the match easily and 853 | ;; can associate additional information with the matched operator. 854 | ;; Note that the search is backward, so the FSM matching starts 855 | ;; in state 0 at the *end* of the strings. 856 | ;; 857 | ;; Each FSM is reepresented by an `essmeq-matcher' object (a struct). 858 | ;; This has several slots: 859 | ;; 860 | ;; :fsm The finite state machine. Each state is either nil or an 861 | ;; alist mapping characters to transitions of the form 862 | ;; (CHAR NEXT-STATE ACCEPTED?) where ACCEPTED? is either 863 | ;; nil when the transition is not to an accepting state 864 | ;; or an index into the target string vector when it is. 865 | ;; Note that acceptance is signaled on the *transition* 866 | ;; not in the state itself, so many accepting states are 867 | ;; often nil. Because the search is anchored, a state 868 | ;; can only accept zero or one strings and there can 869 | ;; be multiple accepting states along the path. 870 | ;; 871 | ;; :targets The vector of strings being matched, without padding. 872 | ;; 873 | ;; :span The maximum length of the target strings 874 | ;; 875 | ;; :data A vector, the same length as targets, of optional 876 | ;; associated data. 877 | ;; 878 | ;; :partial A table of links that can be used for computing 879 | ;; partial matches. Each partial element is a list of 880 | ;; the form (CHAR (STATE . SLEN)), where STATE is a 881 | ;; candidate STATE to start in for finding the partial 882 | ;; match and SLEN is the number of skipped characters 883 | ;; at the end of the string for that partial match. 884 | ;; 885 | ;; These matchers are built with `essmeq--build-fsm' and matched 886 | ;; with `essmeq--match' (for exact matches) and `essmeq--complete' 887 | ;; (for partial matches). 888 | 889 | (cl-defstruct (essmeq-matcher 890 | (:constructor nil) 891 | (:constructor essmeq-make-matcher 892 | (strings 893 | &key 894 | (fsm (make-vector 895 | (1+ (apply #'+ (mapcar #'length strings))) 896 | nil)) 897 | (span (apply #'max 0 (mapcar #'length strings))) 898 | (info (make-vector (length strings) nil)) 899 | (partial nil) 900 | &aux 901 | (targets (vconcat strings)) 902 | (data (vconcat info)))) 903 | (:copier essmeq-copy-matcher) 904 | (:predicate essmeq-matcher-p)) 905 | fsm targets span data partial) 906 | 907 | (defun essmeq--build-fsm (ops &optional data) 908 | "Build backward matching finite-state machine for string vector OPS." 909 | (declare (pure t) (side-effect-free t)) 910 | (let ((fsm (make-vector (1+ (apply #'+ (mapcar #'length ops))) nil)) 911 | (partial nil) 912 | (next-state 1) ;; start state 0 always exists 913 | (max-len 0) 914 | (num-ops (length ops)) 915 | (op-index 0)) 916 | (while (< op-index num-ops) 917 | (let* ((state 0) 918 | (op (elt ops op-index)) 919 | (len (length op)) 920 | (ind (1- len))) 921 | (when (> len max-len) 922 | (setq max-len len)) 923 | (while (> ind 0) 924 | (if-let* ((ch (aref op ind)) 925 | (in-state (aref fsm state)) 926 | (goto (assoc ch in-state))) 927 | (setq state (cadr goto)) ; transition exists, follow it 928 | (push (cl-list* ch next-state nil) (aref fsm state)) ; new state 929 | (when (> state 0) ; goto for partial match 930 | (push (cons state (- len ind 1)) (map-elt partial ch))) 931 | (setq state next-state 932 | next-state (1+ next-state))) 933 | (setq ind (1- ind))) 934 | (if-let* ((ch (aref op 0)) 935 | (in-state (aref fsm state)) 936 | (goto (assoc ch in-state))) 937 | (setf (cddr goto) op-index) ; transition exists, accept it 938 | (push (cl-list* ch next-state op-index) (aref fsm state)) ; new accept 939 | (when (> state 0) ; goto for partial match 940 | (push (cons state (- len 1)) (map-elt partial ch))) 941 | (setq next-state (1+ next-state)))) 942 | (setq op-index (1+ op-index))) 943 | (essmeq-make-matcher ops 944 | :fsm (cl-map 'vector #'nreverse 945 | (substring fsm 0 next-state)) 946 | :span max-len 947 | :info (if data (vconcat data) nil) 948 | :partial (mapcar (lambda (x) 949 | (cl-callf reverse (cdr x)) 950 | x) 951 | (nreverse partial))))) 952 | 953 | (defun essmeq--match (fsm &optional pos bound) 954 | "Search backward to exactly match a string specified by machine FSM. 955 | Anchor the search at POS, or at point if nil. BOUND, if non-nil, 956 | limits the search to positions not before position BOUND. Assumes 957 | that surrounding whitespace is handled elsewhere. 958 | 959 | Return a dotted list of the form (ACCEPT 0 START . POS) if a match 960 | exists, or nil otherwise. ACCEPT is the number of the accepting 961 | state in FSM, START is the position of the matching string's 962 | beginning, and POS is the position where scanning started, as 963 | passed to this function." 964 | (let* ((pos (or pos (point))) 965 | (limit (or bound (point-min))) 966 | (state 0) 967 | (accepted nil) 968 | (start pos)) 969 | (while (and (not (eq state :fail)) (>= start limit)) 970 | (if-let (next (assoc (char-before start) (aref fsm state))) 971 | (setq state (cadr next) 972 | accepted (cddr next) 973 | start (1- start)) 974 | (setq state :fail))) 975 | (if accepted 976 | (cl-list* accepted 0 start pos) 977 | nil))) 978 | 979 | (defun essmeq--complete (fsm partial &optional pos bound) 980 | "Search backward for farthest partial match to a string specified by FSM. 981 | A partial match is a prefix of one of the target operators; the 982 | `farthest' match is the one that moves the position as far back 983 | as possible. Note that this respects the priority order only for 984 | equally far matches. 985 | 986 | FSM is the finite-state machine from an `essmeq-matcher'; PARTIAL 987 | is an alist mapping characters to a list of (STATE . SLEN) pairs, 988 | where STATE represents a state to jump to for partial match from POS 989 | and SLEN is the length of the omitted suffix for that partial match. 990 | The search is anchored at POS, or at point if nil. BOUND, if non-nil, 991 | limits the search to positions not before position BOUND. Assumes 992 | that surrounding whitespace is handled elsewhere. 993 | 994 | Return a dotted list of the form (ACCEPT SLEN START . POS) if a 995 | match exists, or nil otherwise. ACCEPT is the number of the 996 | accepting state in FSM, SLEN is the length of the missing suffix 997 | in the partially matched string (0 for full match), START is the 998 | position of the matching string's beginning, and POS is the 999 | position where scanning started, as passed to this function." 1000 | (let* ((pos (or pos (point))) 1001 | (limit (or bound (point-min))) 1002 | (start pos) 1003 | (ch0 (char-before start)) 1004 | (skip (copy-sequence (map-elt partial ch0))) 1005 | (state (caar skip)) 1006 | (slen (cdar skip)) 1007 | (accepted nil) 1008 | (farthest-start (1+ start)) 1009 | (farthest-slen 0)) 1010 | (when skip 1011 | (pop skip) 1012 | (while (and (not (eq state :fail)) (>= start limit)) 1013 | (if-let (next (assoc (char-before start) (aref fsm state))) 1014 | (let ((acc* (cddr next)) 1015 | (farther (< start farthest-start))) 1016 | (when (and acc* farther) 1017 | (setq accepted acc* 1018 | farthest-start start 1019 | farthest-slen slen)) 1020 | (setq state (cadr next) 1021 | start (1- start))) 1022 | (if-let ((jump (pop skip))) 1023 | (setq state (car jump) 1024 | slen (cdr jump) 1025 | start pos) 1026 | (setq state :fail))))) 1027 | (if accepted 1028 | ;; don't move to failure above so farthest-start off by one 1029 | (cl-list* accepted farthest-slen (1- farthest-start) pos) 1030 | nil))) 1031 | 1032 | (defun essmeq--fallback (pos) 1033 | "Fallback completion at position POS forcing use of the `base' context. 1034 | If a completion is made, return a dotted list of the 1035 | form (OP-STRING 0 START . POS), with a form similar to that 1036 | returned by `essmeq--complete' except the first element is the 1037 | operator string to use rather than an integer index into the 1038 | target table. This enables the fallback to be used when matching 1039 | under an arbitrary context. If no completion can be made, return 1040 | nil." 1041 | (when-let ((matcher (map-elt essmeq--matcher-alist 'base))) 1042 | (essmeq--with-matcher (fsm targets span partial) matcher 1043 | (let ((m (essmeq--complete fsm partial pos (- pos span)))) 1044 | ;; Replace the index by the operator string 1045 | (when m (cons (aref targets (car m)) (cdr m))))))) 1046 | 1047 | 1048 | ;;; Context and Matcher Configuration and Utilities 1049 | 1050 | (defun essmeq--build-matchers (context-alist) 1051 | "ATTN" 1052 | (declare (pure t) (side-effect-free t)) 1053 | (let (matchers 1054 | (car-or-id (lambda (x) (if (consp x) (car x) x)))) 1055 | (dolist (context context-alist (nreverse matchers)) 1056 | (push (cons (car context) 1057 | (essmeq--build-fsm (mapcar car-or-id (cdr context)) 1058 | (let* ((info (cdr context)) 1059 | (data (mapcar #'cdr-safe info))) 1060 | (if (cl-every #'null data) 1061 | nil 1062 | data)))) 1063 | matchers)))) 1064 | 1065 | (defcustom ess-smart-equals-mode-options 1066 | '((inferior-ess-r-mode 1067 | (ess-smart-equals-narrow-function . ess-smart-equals-comint-narrow))) 1068 | "Mode-specific updates of `ess-smart-equals-mode' options. 1069 | This is an alist mapping major mode (symbols) to an alist of 1070 | option settings that will supersede the default settings when 1071 | that mode is in effect. Only options that need to be changed from 1072 | their default value need to be included, and only major modes 1073 | where such a change is made. These settings take effect in a 1074 | buffer when the minor mode is enabled, so after any changes in 1075 | this variable, the mode needs to be toggled twice for the changes 1076 | to take effect." 1077 | :group 'ess-edit 1078 | :type '(alist :key-type symbol 1079 | :value-type (alist :key-type symbol :value-type sexp))) 1080 | 1081 | ;; ATTN: Allow `:cycle' in context lists. Everything before is a candidate 1082 | ;; for exact matching/cycling; everything after can be completed only. 1083 | ;; Need to add `max-cycle' slot to essmeq-matcher structure, which 1084 | ;; is useful in several places in *--search anyway. Also consider 1085 | ;; the possibility of including symbols in this list, were these are 1086 | ;; nullary functions that splice into the targets vector at that point, 1087 | ;; computed at mode enable when the matchers are created. Use case for 1088 | ;; this: querying R for names of %infix% operators. 1089 | (defcustom ess-smart-equals-contexts 1090 | '((t (comment) 1091 | (string) 1092 | (arglist "=" "==" "!=" "<=" ">=" "<-" "<<-" "%>%") 1093 | (index "==" "!=" "%in%" "<" "<=" ">" ">=" "=") 1094 | ;; This order of inequalities makes cycling align with completion 1095 | (conditional "==" "!=" "<" "<=" ">" ">=" "%in%") 1096 | ;; base holds all operators that are assignment or complete with '=' 1097 | (base "<-" "<<-" "=" "==" "!=" "<=" ">=" "->" "->>" ":=") 1098 | ;; Used for smart %-completion and cycling 1099 | (% "%*%" "%%" "%/%" "%in%" "%>%" "%<>%" "%o%" "%x%") 1100 | ;; Used for removal in ess-smart-equals-percent 1101 | (not-% "<-" "<<-" "=" "->" "->>" 1102 | "==" "!=" "<" "<=" ">" ">=" 1103 | "+" "-" "*" "**" "/" "^" "&" "&&" "|" "||") 1104 | ;; All the principal binary operators 1105 | (all "<-" "<<-" "=" "->" "->>" 1106 | "==" "!=" "<" "<=" ">" ">=" 1107 | "%*%" "%%" "%/%" "%in%" "%x%" "%o%" 1108 | "%<>%" "%>%" ; not builtin but common (dynamic?) 1109 | "+" "-" "*" "**" "/" "^" "&" "&&" "|" "||") 1110 | (t "<-" "<<-" "=" "==" "->" "->>" "%<>%")) 1111 | (ess-roxy-mode 1112 | (comment "<-" "=" "==" "<<-" "->" "->>" "%<>%"))) 1113 | "Prioritized lists of operator strings for each context and major mode. 1114 | This is an alist where each key is either t or the symbol of a 1115 | major mode and each value is in turn an alist mapping context 1116 | symbols to lists of operator strings in the preferred order. 1117 | 1118 | The mappings for each mode are actually computed by merging the 1119 | default (t) mapping with that specified for the mode, with the 1120 | latter taking priority. 1121 | 1122 | An empty symbol list for a context means to insert 1123 | `ess-smart-equals-key' literally. 1124 | 1125 | If this is changed while the minor mode is running, you will need 1126 | to disable and the re-enable the mode to make changes take 1127 | effect." 1128 | :group 'ess-edit 1129 | :type '(alist 1130 | :key-type symbol 1131 | :value-type (alist :key-type symbol :value-type (repeat string)))) 1132 | 1133 | (defcustom ess-smart-equals-context-function nil 1134 | "If non-nil, a nullary function to calculate the syntactic context at point. 1135 | It should return nil, which indicates to fall back on the usual 1136 | context calculation, or a symbol corresponding to a context, 1137 | i.e., one of the keys in `ess-smart-equals-contexts', either 1138 | pre-defined or user-defined. Absent any specific context, the 1139 | function can return `t', which is used as a default. When set, 1140 | this is called as the first step in the context calculation. This 1141 | function has access to `ess-smart-equals-overriding-context' and 1142 | can choose to respect it (by returning it or nil if set) or 1143 | ignore it. That variable is next in priority in determining the 1144 | context." 1145 | :group 'ess-edit 1146 | :type 'function) 1147 | 1148 | (defvar-local essmeq--matcher-alist 1149 | (essmeq--build-matchers (map-elt ess-smart-equals-contexts t)) 1150 | "Alist mapping context symbols to operator matchers. 1151 | Do not set this directly") 1152 | 1153 | (defun ess-smart-equals-set-contexts (&optional mode context-alist) 1154 | (interactive (list (if current-prefix-arg major-mode nil) nil)) 1155 | (let ((contexts (or context-alist ess-smart-equals-contexts))) 1156 | (if mode 1157 | (setq essmeq--matcher-alist 1158 | (essmeq--build-matchers 1159 | (map-merge 'list (map-elt contexts t) (map-elt contexts mode)))) 1160 | (setq essmeq--matcher-alist 1161 | (essmeq--build-matchers (map-elt contexts t)))))) 1162 | 1163 | (defun ess-smart-equals-set-options (mode &optional option-alist) 1164 | "Set mode-specific options for MODE in current buffer. 1165 | OPTION-ALIST, which defaults to `ess-smart-equals-mode-options', 1166 | maps major mode symbols to an alist mapping option variables to 1167 | their values. The entries in this latter alist are of the 1168 | form (VAR . VALUE). Typically, VAR will be a symbol for a 1169 | buffer-local option." 1170 | (interactive (list major-mode nil)) 1171 | (let ((options-alist (or option-alist ess-smart-equals-mode-options))) 1172 | (when mode 1173 | (let ((options (alist-get mode options-alist))) 1174 | (dolist (option options mode) 1175 | (set (car option) (cdr option))))))) 1176 | 1177 | (defun ess-smart-equals-prompts () 1178 | "Ask R for current primary and secondary prompts. 1179 | Return pair of regexes matching the two prompts, (PRIMARY . SECONDARY)." 1180 | (let ((proc (if (derived-mode-p 'inferior-ess-mode) 1181 | (get-buffer-process (current-buffer)) 1182 | (ess-get-next-available-process "R" t))) 1183 | (cmd "c(options()$prompt, options()$continue)\n")) 1184 | (if (not proc) 1185 | (cons (regexp-quote (or inferior-ess-primary-prompt "> ")) 1186 | (regexp-quote (or inferior-ess-secondary-prompt "+ "))) 1187 | (with-temp-buffer 1188 | (ess-command cmd (current-buffer) nil nil nil proc) 1189 | (goto-char (point-min)) 1190 | (search-forward-regexp " \"\\([^\"]+\\)\" \"\\([^\"]+\\)\" *$" nil t) 1191 | (cons (regexp-quote (match-string 1)) 1192 | (regexp-quote (match-string 2))))))) 1193 | 1194 | ;; ATTN: only handles the comint-use-prompt-regexp case in the input sender, 1195 | ;; though comint-prompt-regexp is not used explicitly. This should probably 1196 | ;; handle the field case for completeness, but it is unclear whether that 1197 | ;; will make any practical difference. 1198 | (defun essmeq--r-repl-current-region () 1199 | "Return region around point for syntax checking in R repl output buffer." 1200 | (if-let* ((proc (get-buffer-process (current-buffer))) 1201 | (pm (process-mark proc)) 1202 | ((> (point) pm))) 1203 | (list (marker-position pm) (point-max)) 1204 | (pcase-let* ((`(,primary . ,secondary) (ess-smart-equals-prompts)) 1205 | (prompt-re (concat primary "\\|" secondary)) 1206 | (point-line-p 1207 | (save-excursion 1208 | (beginning-of-line) 1209 | (looking-at-p prompt-re))) 1210 | (paragraph-separate "\^L") 1211 | (paragraph-start (concat primary "\\|" paragraph-separate))) 1212 | (if point-line-p 1213 | (save-excursion 1214 | (unless (looking-at-p primary) 1215 | (backward-paragraph) 1216 | (skip-chars-forward "\n ")) 1217 | (comint-skip-prompt) 1218 | (list (point) (progn 1219 | (forward-line) 1220 | (while (looking-at-p secondary) 1221 | (forward-line)) 1222 | (point)))) 1223 | (list (save-excursion 1224 | (backward-paragraph) 1225 | (skip-chars-forward "\n ") 1226 | (while (looking-at-p prompt-re) 1227 | (forward-line)) 1228 | (point)) 1229 | (save-excursion (forward-paragraph) (point))))))) 1230 | 1231 | (defun ess-smart-equals-comint-narrow () 1232 | "ATTN" 1233 | (apply #'narrow-to-region (essmeq--r-repl-current-region))) 1234 | 1235 | 1236 | ;;; Contexts 1237 | 1238 | (defun essmeq--inside-call-p () 1239 | "Return non-nil if point is in a function call (or indexing construct). 1240 | This is like `ess-inside-call-p' except it also returns true if a closing 1241 | parenthesis after point will put point in a call. This is intended to be 1242 | used after checking for indexing constructs." 1243 | (or (ess-inside-call-p) 1244 | (essmeq--with-temporary-insert ")" :after (ess-inside-call-p)))) 1245 | 1246 | (defun essmeq--context (&optional pos) 1247 | "Compute context at position POS. Returns a context symbol or t. 1248 | If `ess-smart-equals-context-function' is non-nil, that function 1249 | is called and a non-nil return value is used as the context; a 1250 | nil value falls back on the ordinary computation. 1251 | 1252 | There are two known issues here. First, `ess-inside-call-p' does 1253 | not detect a function call if the end parens are not closed. This 1254 | is mostly fixed by using `essmeq--inside-call-p' instead. Second, 1255 | because the R modes characterize % as a string character, a 1256 | single % (e.g., an incomplete operator) will cause checks for 1257 | function calls or brackets to fail. This can be fixed with a 1258 | temporary % insertion, but at the moment, the added complexity 1259 | does not seem worthwhile. Note similarly that when 1260 | `ess-inside-string-p' returns a ?%, we could use the % context to 1261 | limit to matches to the %-operators." 1262 | (save-excursion 1263 | (when pos (goto-char pos)) 1264 | (cond 1265 | ((and ess-smart-equals-context-function 1266 | (funcall ess-smart-equals-context-function))) 1267 | (ess-smart-equals-overriding-context) 1268 | ((ess-inside-comment-p) 'comment) 1269 | ((let ((closing-char (ess-inside-string-p))) 1270 | (and closing-char (/= closing-char ?%))) 1271 | ;; R syntax table makes % a string character, which we ignore 1272 | 'string) 1273 | ((ess-inside-brackets-p) 'index) 1274 | ((essmeq--inside-call-p) 1275 | (if (save-excursion 1276 | (goto-char (ess-containing-sexp-position)) 1277 | (or (ess-climb-call-name "if") 1278 | (ess-climb-call-name "while"))) 1279 | 'conditional 1280 | 'arglist)) 1281 | (t)))) 1282 | 1283 | (defun ess-smart-equals-percent-operators () 1284 | "Ask R for currently valid %-operators and return list of operator strings." 1285 | (let ((proc (if (derived-mode-p 'inferior-ess-mode) 1286 | (get-buffer-process (current-buffer)) 1287 | (ess-get-next-available-process))) 1288 | (cmd (format 1289 | "unique(sort(%s))\n" 1290 | "unlist(Map(function(s){ls(s, pattern='%.*%')}, search()))")) 1291 | ops) 1292 | (if (not proc) 1293 | '("default list ATTN") 1294 | (with-temp-buffer 1295 | (ess-command cmd (current-buffer) nil nil nil proc) 1296 | (goto-char (point-min)) 1297 | (while (search-forward-regexp "\"\\(%[^%]*%\\)\"" nil t) 1298 | (push (match-string 1) ops)) 1299 | (nreverse ops))))) 1300 | 1301 | 1302 | ;;; Processing the Action Key 1303 | 1304 | (defun essmeq--search (&optional initial-pos no-partial no-fallback) 1305 | "Search backwards for an operator matching the current context. 1306 | Search is anchored at INITIAL-POS, or point if nil. If NO-PARTIAL 1307 | is nil, then partial matches of a prefix of relevant operators 1308 | strings are allowed. If NO-FALLBACK is nil, then failure to match 1309 | or complete will fall back to insertion from the `base' context. 1310 | 1311 | Return a list (CONTEXT MTYPE STRING START END IGNORE-PADDING), 1312 | where CONTEXT is a context symbol in `ess-smart-equals-contexts'; 1313 | MTYPE is one of the keyword :exact, a positive integer indicating 1314 | a partial match, :literal (for literal '=' insertion), and 1315 | :no-match; STRING is the operator string to be inserted, 1316 | replacing the region between START and END. Finally, 1317 | IGNORE-PADDING is non-nil when padding options should be 1318 | ignored." 1319 | ;; Putting an integer rather than :partial in the return list is a 1320 | ;; practical choice, if somewhat obscure, to make it easier to use 1321 | ;; downstream, e.g., in essmeq--replace-region. The alternative is 1322 | ;; to add the integer to the result list for all the other cases, 1323 | ;; a more straightforward but less convenient choice. 1324 | (let* ((pt (or initial-pos (point))) 1325 | (pos (save-excursion 1326 | (when initial-pos (goto-char pt)) 1327 | (+ pt (skip-syntax-backward " ")))) 1328 | (context (essmeq--context pos)) 1329 | (matcher (map-elt essmeq--matcher-alist context))) 1330 | (essmeq--with-matcher (fsm targets span partial) matcher 1331 | (if-let* ((num-ops (length targets)) ;; ATTN: this will be max-cycle, 1332 | ((> num-ops 0)) ;; and can get rid of num-ops 1333 | (limit (- pos span))) 1334 | (pcase-let ((`(,accepted ,slen ,start . ,_) 1335 | (or (essmeq--match fsm pos limit) 1336 | (and (not no-partial) 1337 | (or (essmeq--complete fsm partial pos limit) 1338 | (and (not no-fallback) 1339 | (essmeq--fallback pos))))))) 1340 | (if accepted 1341 | (let* ((op (if (zerop slen) 1342 | (mod (1+ accepted) num-ops) 1343 | accepted)) 1344 | (mtype (if (zerop slen) :exact slen)) 1345 | (op-string (if (integerp op) (aref targets op) op))) 1346 | (list context mtype op-string start pos)) 1347 | (list context :no-match (aref targets 0) pos pos))) 1348 | (list context :literal "=" pt pt t))))) 1349 | 1350 | (defun essmeq--process (&optional no-partial no-fallback) 1351 | "Insert, cycle, or complete an operator at point based on context. 1352 | Point ends up at the end of the inserted string. Calls 1353 | `ess-smart-equals-insertion-hook', if the hook is non-nil, with 1354 | results of the search (from `essmeq--search') modified to account 1355 | for the beginning and ending of the inserted operator string. 1356 | Arguments NO-PARTIAL and NO-FALLBACK are passed to 1357 | `essmeq--search', which see." 1358 | (save-restriction 1359 | (when ess-smart-equals-narrow-function 1360 | (funcall ess-smart-equals-narrow-function)) 1361 | (let* ((match (essmeq--search (point) no-partial no-fallback)) 1362 | (spec (cddr match)) 1363 | (after (apply #'essmeq--replace-region spec))) 1364 | (goto-char (cdr after)) ; end position after padding 1365 | (when ess-smart-equals-insertion-hook 1366 | (setf (nth 3 spec) (car after) ; adjust beg..end positions 1367 | (nth 4 spec) (cdr after)) 1368 | (apply ess-smart-equals-insertion-hook spec))))) 1369 | 1370 | (defun essmeq--remove (&optional only-match) 1371 | "Remove a matching operator at point based on context, else one character." 1372 | (interactive) 1373 | (save-restriction 1374 | (when ess-smart-equals-narrow-function 1375 | (funcall ess-smart-equals-narrow-function)) 1376 | (let* ((match (cdr (essmeq--search nil t))) ; ATTN: allow completion? 1377 | (mtype (car match)) 1378 | (beg (caddr match)) 1379 | (end (cadddr match)) 1380 | (exact (eq mtype :exact))) 1381 | (if (or exact (integerp mtype)) 1382 | (let* ((padded (essmeq--find-padded-region beg end)) 1383 | (start (if exact (car padded) (- end mtype))) 1384 | (region (essmeq--replace-region "" start (cdr padded) t))) 1385 | (goto-char (cdr region))) 1386 | (unless only-match (delete-char -1)))))) 1387 | 1388 | (defun essmeq--selected (op-string) 1389 | "Insert operator string at point with padding, replacing existing operator. 1390 | If called interactively, the typical case, select the operator by 1391 | completion. If the context operator list is empty, insert 1392 | operator string as is." 1393 | (interactive (list (completing-read "Operator: " 1394 | (thread-last ess-smart-equals-contexts 1395 | (alist-get 't) 1396 | (mapcar #'cdr) 1397 | (apply #'append) 1398 | delete-dups)))) 1399 | (save-restriction 1400 | (when ess-smart-equals-narrow-function 1401 | (funcall ess-smart-equals-narrow-function)) 1402 | (let* ((match (cdr (essmeq--search nil t))) 1403 | (mtype (car match)) 1404 | (beg (caddr match)) 1405 | (end (cadddr match))) 1406 | (if (or (eq mtype :exact) (eq mtype :no-match) (integerp mtype)) 1407 | (let* ((pad (essmeq--find-padded-region beg end)) 1408 | (reg (essmeq--replace-region op-string (car pad) (cdr pad)))) 1409 | (goto-char (cdr reg))) 1410 | (insert op-string))))) 1411 | 1412 | 1413 | ;;; Extra Smart Operators 1414 | 1415 | ;;;###autoload 1416 | (defun ess-smart-equals-percent (&optional literal) 1417 | "Completion and cycling through %-operators only, unless in comment or string. 1418 | Outside a comment or string, this forces a % context as described 1419 | in `ess-smart-equals-contexts', so the corresponding list can be 1420 | customized to determine ordering. This should be bound to the `%' 1421 | key." 1422 | (interactive "P") 1423 | (if (or literal 1424 | (let ((closing-char (ess-inside-string-or-comment-p))) 1425 | (and closing-char (not (equal closing-char ?%))))) 1426 | (self-insert-command (if (integerp literal) literal 1)) 1427 | (unless (eq last-command this-command) 1428 | (let ((ess-smart-equals-overriding-context 'not-%)) 1429 | (essmeq--remove 'only-match)) 1430 | (setq essmeq--stop-transient 1431 | (set-transient-map essmeq--transient-map 1432 | #'essmeq--keep-transient 1433 | ess-smart-equals-transient-exit-function))) 1434 | (ess-smart-equals-set-overriding-context '%) 1435 | (essmeq--process nil t) 1436 | (ess-smart-equals-clear-overriding-context))) 1437 | 1438 | ;;;###autoload 1439 | (defun ess-smart-equals-open-brace (&optional literal) 1440 | "Inserts properly indented and spaced brace pair." 1441 | (interactive "P") 1442 | (if (or literal (ess-inside-string-or-comment-p)) 1443 | (self-insert-command (if (integerp literal) literal 1)) 1444 | (when (not (eq (char-syntax (char-before)) ?\ )) 1445 | (insert " ")) 1446 | (let ((pt (point)) 1447 | (skeleton-pair t) 1448 | (skeleton-pair-alist '((?\{ "\n" > _ "\n" > ?\})))) 1449 | (skeleton-pair-insert-maybe nil) 1450 | (goto-char pt) 1451 | (ess-indent-exp) 1452 | (forward-char 2) 1453 | (ess-indent-command)))) 1454 | 1455 | (defun essmeq--paren-escape () 1456 | "Escape paren pair, deleting magic space if starting there." 1457 | (interactive) 1458 | (when (= (char-after) ?\ ) (delete-char 1)) 1459 | (ess-up-list)) 1460 | 1461 | (defun essmeq--paren-comma () 1462 | "Insert spaced comma, keeping point on magic space." 1463 | (interactive) 1464 | (insert ", ") 1465 | (unless (derived-mode-p 'inferior-ess-mode) 1466 | (indent-according-to-mode))) 1467 | 1468 | (defun essmeq--paren-expand () 1469 | "With point on magic space, expand region over following balanced expressions. 1470 | This can be followed with `essmeq--paren-slurp' (C-;) to move 1471 | those expressions inside the parentheses." 1472 | (interactive) 1473 | (save-excursion 1474 | (ess-up-list) 1475 | (mark-sexp nil t))) 1476 | 1477 | (defun essmeq--paren-slurp (&optional save-initial-space) 1478 | "Moves marked region following paren pair inside parentheses. 1479 | Initial spaces following the end of the current paren pair are 1480 | deleted unless a prefix argument is given (SAVE-INITIAL-SPACE 1481 | non-nil). This is usually preceded by `essmeq--paren-expand' but 1482 | applies to any region from point forward." 1483 | (interactive "P") 1484 | (when (and mark-active (> (mark) (point))) 1485 | (let* ((end-of-list (save-excursion (ess-up-list) (point))) 1486 | (delta-ws (if save-initial-space 1487 | 0 1488 | (save-excursion 1489 | (ess-up-list) 1490 | (skip-syntax-forward " ")))) 1491 | (end-of-slurp (- (mark) 2 delta-ws)) 1492 | (yank-excluded-properties (remq 'keymap yank-excluded-properties))) 1493 | (unless save-initial-space 1494 | (delete-region end-of-list (+ end-of-list delta-ws))) 1495 | (kill-region (point) end-of-list) 1496 | (goto-char end-of-slurp) 1497 | (yank) 1498 | (forward-char -2)))) 1499 | 1500 | (defvar essmeq--paren-map (let ((m (make-sparse-keymap))) 1501 | (define-key m (kbd ",") 'essmeq--paren-comma) 1502 | (define-key m (kbd ")") 'essmeq--paren-escape) 1503 | (define-key m (kbd ";") 'essmeq--paren-escape) 1504 | (define-key m (kbd "C-;") 'essmeq--paren-expand) 1505 | (define-key m (kbd "M-;") 'essmeq--paren-slurp) 1506 | m) 1507 | "Keymap active in fresh space in the middle of a new smart open paren.") 1508 | (fset 'essmeq--paren-map essmeq--paren-map) 1509 | 1510 | ;;;###autoload 1511 | (defun ess-smart-equals-open-paren (&optional literal) 1512 | "Inserts properly a properly spaced paren pair with an active keymap inside. 1513 | Point is left in the middle of the paren pair and associated with 1514 | a special keymap, where tab deletes the extra space and moves 1515 | point out of the parentheses and comma inserts a spaced comma, 1516 | keeping point on the special space character. " 1517 | (interactive "P") 1518 | (if (or literal (ess-inside-string-or-comment-p)) 1519 | (self-insert-command (if (integerp literal) literal 1)) 1520 | ;; Check syntax table for inferior-ess-r-mode for ', apparently not string 1521 | (let ((skeleton-pair t) 1522 | (skeleton-pair-alist '((?\( _ _ " " 1523 | '(let ((pt (point))) 1524 | (add-text-properties 1525 | (1- pt) pt 1526 | '(essmeq--magic-space t 1527 | keymap essmeq--paren-map))) 1528 | ?\))))) 1529 | (skeleton-pair-insert-maybe nil)))) 1530 | 1531 | 1532 | ;;; Entry Points 1533 | 1534 | ;;;###autoload 1535 | (defun ess-smart-equals-activate (&rest active-modes) 1536 | "Turn on `ess-smart-equals-mode' in current and future buffers of ACTIVE-MODES. 1537 | If non-nil, each entry of ACTIVE-MODES is either a major-mode 1538 | symbol or a list of two symbols (major-mode major-mode-hook). In 1539 | the former case, the hook symbol is constructed by adding 1540 | \"-hook\" to the major mode symbol name. If ACTIVE-MODES is nil, 1541 | the specification in `ess-smart-equals-default-modes' is used 1542 | instead. 1543 | 1544 | This adds to each specified major-mode hook a function that will 1545 | enable `ess-smart-equals-mode' and also enables the minor mode in 1546 | all current buffers whose major mode is one of the major modes 1547 | just described." 1548 | (interactive) 1549 | (dolist (mode-spec (or active-modes ess-smart-equals-default-modes)) 1550 | (let* ((mode (if (listp mode-spec) (car mode-spec) mode-spec)) 1551 | (hook (if (listp mode-spec) 1552 | (cdr mode-spec) 1553 | (intern (concat (symbol-name mode) "-hook"))))) 1554 | (add-hook hook #'ess-smart-equals-mode) 1555 | (dolist (buf (buffer-list)) 1556 | (with-current-buffer buf 1557 | (when (derived-mode-p mode) 1558 | (ess-smart-equals-mode 1))))))) 1559 | 1560 | ;;;###autoload 1561 | (defun ess-smart-equals (&optional literal) 1562 | "Insert or complete a properly-spaced R/S (assignment) operator at point. 1563 | With a prefix argument (or with LITERAL non-nil) insert this key 1564 | literally, repeated LITERAL times if a positive integer. 1565 | Otherwise, complete a partial operator or insert a new operator 1566 | based on context (major mode and syntactic context) according to 1567 | the specification given in `ess-smart-equals-contexts'. 1568 | Immediately following invocations of the command cycle through 1569 | operators in this context based list in the specified priority 1570 | order. Immediately following insertion selected keys (e.g., 1571 | backspace) will remove the inserted operator or (e.g., tab) allow 1572 | selection of an inserted operator by completion. See 1573 | `ess-smart-equals-cancel-keys'; a shift-modified one of these 1574 | keys (except 'C-g') will do a single character deletion and 1575 | restore the standard meaning of keys." 1576 | (interactive "P") 1577 | (if (and literal (not (equal literal '(16)))) 1578 | (self-insert-command (if (integerp literal) literal 1)) 1579 | (when literal 1580 | (message "Cycling over all operators") 1581 | (ess-smart-equals-set-overriding-context 'all)) 1582 | (essmeq--process) 1583 | (unless (eq last-command this-command) 1584 | (setq essmeq--stop-transient 1585 | (set-transient-map essmeq--transient-map 1586 | #'essmeq--keep-transient 1587 | ess-smart-equals-transient-exit-function))))) 1588 | 1589 | 1590 | ;;; Minor Mode 1591 | 1592 | ;;;###autoload 1593 | (define-minor-mode ess-smart-equals-mode 1594 | "Minor mode enabling a smart key for context-aware operator insertion/cycling. 1595 | 1596 | Ess-smart-equals-mode is a buffer-local minor mode. Enabling it 1597 | binds a key ('=' by default) to a function that inserts, 1598 | completes, or cycles among operators chosen by the syntactic 1599 | context at point. These contexts and the priorities of insertion 1600 | and cycling are customizable. The operators inserted are usually 1601 | assignment operators but can include others as well, e.g., 1602 | comparison operators in and `if' or `while'. When 1603 | `ess-smart-equals-extra-ops' is appropriately set, this minor 1604 | mode also activates additional smart operators for convenience. 1605 | 1606 | When called interactively, `ess-smart-equals-mode' toggles the 1607 | mode without a prefix argument; disables the mode if the prefix 1608 | argument is a non-positive integer; and enables the mode if the 1609 | prefix argument is a positive integer. When called from Lisp, the 1610 | command toggles the mode with argument `toggle'; disables the 1611 | mode for a non-positive integer; and enables the mode otherwise, 1612 | even with an omitted or nil argument. 1613 | 1614 | Do not set the variable `ess-smart-equals-mode' directly; use the 1615 | function of the same name instead." 1616 | :lighter nil 1617 | :keymap ess-smart-equals-mode-map 1618 | (when ess-smart-equals-mode 1619 | (ess-smart-equals-update-keymaps) 1620 | (ess-smart-equals-set-contexts major-mode) 1621 | (ess-smart-equals-set-options major-mode))) 1622 | 1623 | 1624 | (provide 'ess-smart-equals) 1625 | 1626 | ;;; ess-smart-equals.el ends here 1627 | -------------------------------------------------------------------------------- /features/ess-smart-equals.feature: -------------------------------------------------------------------------------- 1 | Feature: Use equals in R source code 2 | In order to conveniently enter R's multi-character assignment 3 | As an Emacs user 4 | I want to use equals to do what I mean 5 | 6 | Background: 7 | Given I turn on ess-smart-equals 8 | 9 | Scenario: Enter equals that should expand to assignment 10 | When I insert "foo " 11 | And I type "=function" 12 | Then I should see "foo <- function" 13 | 14 | Scenario: Enter multiple equals that should expand to parent assignment operator 15 | When I insert "foo" 16 | And I repeat "=" 2 times 17 | And I type "a" 18 | Then I should see "foo <<- a" 19 | 20 | Scenario: Enter multiple equals that should expand to equals operator 21 | When I insert "foo" 22 | And I repeat "=" 3 times 23 | And I type "a" 24 | Then I should see "foo = a" 25 | 26 | Scenario: Enter multiple equals that should expand to assignment operator 27 | When I insert "foo" 28 | And I repeat "=" 8 times 29 | And I type "a" 30 | Then I should see "foo <- a" 31 | 32 | Scenario: Enter equals in call that should not expand for default arguments 33 | When I insert "f(x, answer" 34 | And I type "=42)" 35 | Then I should see "f(x, answer = 42)" 36 | 37 | Scenario: Enter equals in conditional that should expand to equality comparison 38 | When I type "if( a=" 39 | And I type "42 )" 40 | Then I should see "if( a == 42 )" 41 | 42 | Scenario: Enter equals several times that should cyclically expand to neq comparison 43 | When I type "if( a" 44 | And I repeat "=" 2 times 45 | And I type "42 )" 46 | Then I should see "if( a != 42 )" 47 | 48 | Scenario: Enter equals several times that should cyclically expand to leq comparison 49 | When I type "if( a" 50 | And I repeat "=" 3 times 51 | And I type "42 )" 52 | Then I should see "if( a < 42 )" 53 | 54 | Scenario: Enter equals several times that should cyclically expand to geq comparison 55 | When I type "if( a" 56 | And I repeat "=" 6 times 57 | And I type "42 )" 58 | Then I should see "if( a >= 42 )" 59 | 60 | Scenario: Enter equals several times that should cyclically expand to equality comparison 61 | When I type "if( a" 62 | And I repeat "=" 8 times 63 | And I type "42 )" 64 | Then I should see "if( a == 42 )" 65 | 66 | Scenario: Enter equals that should complete an inequality comparison 67 | When I type "if( a<=" 68 | And I type "42 )" 69 | Then I should see "if( a <= 42 )" 70 | 71 | Scenario: Enter equals that should complete to inequality comparison 72 | When I insert "while( a!" 73 | And I type "=42 )" 74 | Then I should see "while( a != 42 )" 75 | 76 | Scenario: Enter equals that should complete to leq comparison 77 | When I insert "while( a<" 78 | And I type "=42 )" 79 | Then I should see "while( a <= 42 )" 80 | 81 | Scenario: Enter equals that should complete to geq comparison 82 | When I insert "while( a>" 83 | And I type "=42 )" 84 | Then I should see "while( a >= 42 )" 85 | 86 | Scenario: Enter equals in index that should complete to eq comparison 87 | When I insert "a[u" 88 | And I type "=42,]" 89 | Then I should see "a[u == 42,]" 90 | 91 | Scenario: Enter equals in index that should complete to .in. comparison 92 | When I insert "a[u" 93 | And I repeat "=" 3 times 94 | And I type "v, ]" 95 | Then I should see "a[u %in% v, ]" 96 | 97 | Scenario: Enter equals that should not expand in a comment 98 | When I insert "# foo " 99 | And I type "= function" 100 | Then I should see "# foo = function" 101 | 102 | Scenario: Enter equals that should not expand in a string 103 | When I enter '"foo ' 104 | And I enter '= function"' 105 | Then I should see ""foo = function"" 106 | 107 | Scenario: Enter underscore that should not expand 108 | When I insert "foo" 109 | And I type "_bar" 110 | Then I should see "foo_bar" 111 | 112 | Scenario: Use backspace to remove an assignment operator 113 | When I insert "foo" 114 | And I type "=" 115 | And I press "" 116 | Then I should see "foo" 117 | 118 | Scenario: Use backspace to remove an assignment operator after cycle 119 | When I insert "foo" 120 | And I type "==" 121 | And I press "" 122 | Then I should see "foo" 123 | 124 | Scenario: Use backspace to remove an assignment operator after two cycles 125 | When I insert "foo" 126 | And I type "===" 127 | And I press "" 128 | Then I should see "foo" 129 | 130 | Scenario: Use fallback completion in a conditional context 131 | When I insert "if" 132 | And I type "( a :=u" 133 | Then I should see "if( a := u )" 134 | 135 | Scenario: Use removal key after insertion to erase operator 136 | When I insert "f" 137 | And I type "=" 138 | And I press "" 139 | And I type "oo" 140 | Then I should see "foo" 141 | 142 | Scenario: Use removal key after insertion to erase operator, in arg list 143 | When I insert "f(a" 144 | And I type "=" 145 | And I press "" 146 | And I type "bc" 147 | And I press ")" 148 | Then I should see "f(abc)" 149 | -------------------------------------------------------------------------------- /features/ess-smart-ops.feature: -------------------------------------------------------------------------------- 1 | Feature: Use smart operators in R source code 2 | In order to conveniently enter paired parentheses in R 3 | As an Emacs user 4 | I want to use parentheses to do what I mean 5 | 6 | Background: 7 | Given I turn on ess-smart-equals 8 | 9 | # 10 | # Parenthesis 11 | # 12 | 13 | Scenario: Smart parentheses activated 14 | Then key "(" should be bound in minor mode map 15 | 16 | Scenario: Use parentheses, end on magic space, see paired parens 17 | When I insert "foo" 18 | And I type "(" 19 | Then current point should be on a magic space with character " " 20 | And I should see "foo( )" 21 | 22 | Scenario: Use parentheses, escape parens, see paired parens 23 | When I insert "foo" 24 | And I type "()" 25 | Then I should see "foo()" 26 | 27 | Scenario: Use parentheses, add args, escape parens, see paired parens 28 | When I insert "foo" 29 | And I type "(a=2,b=4,c=8)" 30 | Then I should see "foo(a = 2, b = 4, c = 8)" 31 | 32 | Scenario: Use parentheses to capture additional arguments 33 | When I insert "foo" 34 | And I set the mark 35 | And I insert " 8" 36 | And I pop the mark 37 | And I type "(a=2,b=4,c=" 38 | And I press "C-;" 39 | And I press "M-;" 40 | And I press ";" 41 | Then I should see "foo(a = 2, b = 4, c = 8)" 42 | 43 | Scenario: Smart parens do not work in comment 44 | When I insert "# abc" 45 | And I type "(" 46 | And I insert "u" 47 | And I go to end of line 48 | And I insert "v" 49 | Then I should see "# abc(uv" 50 | 51 | Scenario: Smart parens do not work in string 52 | When I insert "" abc" 53 | And I type "(" 54 | And I insert "u" 55 | And I go to end of line 56 | And I insert "v" 57 | Then I should see "" abc(uv" 58 | 59 | # 60 | # Percent 61 | # 62 | 63 | Scenario: Smart percent activated 64 | Then key "%" should be bound in minor mode map 65 | 66 | Scenario: Insert percent operator 67 | When I insert "a" 68 | And I type "%b" 69 | Then I should see "a %*% b" 70 | 71 | Scenario: Insert percent operator, cycle once 72 | When I insert "a" 73 | And I type "%%b" 74 | Then I should see "a %% b" 75 | 76 | Scenario: Insert percent operator, cycle twice 77 | When I insert "a" 78 | And I type "%%%b" 79 | Then I should see "a %/% b" 80 | 81 | Scenario: Insert percent operator, cycle thrice 82 | When I insert "a" 83 | And I type "%%%%b" 84 | Then I should see "a %in% b" 85 | 86 | Scenario: Complete percent operator 87 | When I insert "a %i" 88 | And I type "%b" 89 | Then I should see "a %in% b" 90 | 91 | Scenario: Cycling with equals and percent interleaved 112211 92 | When I insert "a" 93 | And I type "==%%==b" 94 | Then I should see "a <<- b" 95 | 96 | Scenario: Cycling with equals and percent interleaved 11222 97 | When I insert "a" 98 | And I type "==%%%b" 99 | Then I should see "a %/% b" 100 | 101 | Scenario: Cycling with equals and percent interleaved 222111 102 | When I insert "a" 103 | And I type "%%%===b" 104 | Then I should see "a = b" 105 | 106 | Scenario: Cycling with equals and percent interleaved 112211, conditional 107 | When I insert "if" 108 | And I type "(a==%%%%==b;" 109 | Then I should see "if(a != b)" 110 | 111 | Scenario: Cycling with equals and percent interleaved 22221111, conditional 112 | When I insert "if" 113 | And I type "(a%%%%====b;" 114 | Then I should see "if(a <= b)" 115 | 116 | 117 | # 118 | # Braces 119 | # 120 | 121 | Scenario: Smart braces activated 122 | Then key "{" should be bound in minor mode map 123 | 124 | Scenario: Use braces to delimit a block with point at first line 125 | When I insert "if( a < b )" 126 | And I inhibit messages 127 | And I type "{" 128 | And I allow messages 129 | Then the cursor should be at point "19" 130 | 131 | Scenario: Use braces to delimit a block 132 | When I insert "if( a < b )" 133 | And I inhibit messages 134 | And I type "{" 135 | And I allow messages 136 | Then I should see across lines 137 | """ 138 | if( a < b ) { 139 | 140 | } 141 | """ 142 | 143 | Scenario: Smart braces do not work in comment 144 | When I insert "# abc" 145 | And I type "{" 146 | And I inhibit messages 147 | And I go to end of buffer 148 | And I allow messages 149 | And I insert "u" 150 | Then I should see "# abc{u" 151 | 152 | Scenario: Smart braces do not work in string 153 | When I insert "" abc" 154 | And I type "{" 155 | And I inhibit messages 156 | And I go to end of buffer 157 | And I allow messages 158 | And I insert "u" 159 | Then I should see "" abc{u" 160 | -------------------------------------------------------------------------------- /features/step-definitions/ess-smart-equals-steps.el: -------------------------------------------------------------------------------- 1 | ;; This file contains your project specific step definitions. All 2 | ;; files in this directory whose names end with "-steps.el" will be 3 | ;; loaded automatically by Ecukes. 4 | 5 | (Given "^I turn on ess-smart-equals$" 6 | (lambda () 7 | (ess-smart-equals-mode 1))) 8 | 9 | (Given "^I turn off ess-smart-equals$" 10 | (lambda () 11 | (ess-smart-equals-mode -1))) 12 | 13 | (When "^I enter '\\(.+\\)'$" 14 | "If action chaining is active, add TYPING to the action chain. 15 | Otherwise simulate typing the string TYPING. Use single-quoted 16 | strings to demarcate argument" 17 | (lambda (typing) 18 | (let ((chars (string-to-vector typing))) 19 | (if espuds-chain-active 20 | (espuds-add-to-chain chars) 21 | (execute-kbd-macro chars))))) 22 | 23 | (When "^I repeat \"\\(.+\\)\" \\([0-9]+\\) times$" 24 | "Add to action chain or simulate typing TYPING repeated REPEATS times." 25 | (lambda (typing repeats) 26 | (let ((execute (if espuds-chain-active 27 | #'espuds-add-to-chain 28 | #'execute-kbd-macro)) 29 | (r (string-to-number repeats))) 30 | (thread-last (if-let ((c (and (= (length typing) 1) (aref typing 0)))) 31 | (make-string r c) 32 | (apply #'concat (make-list r typing))) 33 | string-to-vector 34 | (funcall execute))))) 35 | 36 | (Then "^current point should be on a magic space with character \"\\(.+\\)\"$" 37 | (lambda (ch) 38 | (cl-assert 39 | (and (eq (get-text-property (point) 'essmeq--magic-space) t) 40 | (= (char-after) (aref ch 0))) 41 | nil 42 | "Expected current point to be a magic space on char '%s'. %s" 43 | ch 44 | (buffer-string)))) 45 | 46 | (Then "^key \"\\(.+\\)\" should be bound in minor mode map" 47 | (lambda (binding) 48 | (cl-assert 49 | (minor-mode-key-binding (kbd binding)) 50 | nil 51 | "Expected key '%s' to be bound in current minor-mode map. 52 | %s" 53 | (kbd binding) 54 | (list 55 | (minor-mode-key-binding "(") 56 | (alist-get 'ess-smart-equals-mode minor-mode-map-alist) 57 | ess-smart-equals-mode 58 | ess-smart-equals-extra-ops 59 | (progn 60 | (ess-smart-equals-update-keymaps) 61 | (minor-mode-key-binding "(")) 62 | (where-is-internal 'ess-smart-equals-open-paren) 63 | (lookup-key ess-smart-equals-mode-map "("))))) 64 | 65 | (Then "^I should see multi-line \"\\([^\"]+\\)\"$" 66 | "Asserts that the current buffer includes some text." 67 | (lambda (expected) 68 | (let ((actual (buffer-string)) 69 | (message "Expected\n%s\nto be part of:\n%s") 70 | (text (replace-regexp-in-string 71 | "[^\\\\]\\(\\\\n\\)" "\n" expected nil nil 1))) 72 | (cl-assert (s-contains? text actual) nil message text actual)))) 73 | 74 | (Then "^I should see across lines" 75 | "Asserts that the current buffer includes some text." 76 | (lambda (expected) 77 | (let ((actual (buffer-string)) 78 | (message "Expected\n%s\nto be part of:\n%s")) 79 | (cl-assert (s-contains? expected actual) nil message expected actual)))) 80 | 81 | (When "^I inhibit messages$" 82 | "Inhibits minibuffer messages during subsequent steps" 83 | (lambda () 84 | (setq inhibit-message t))) 85 | 86 | (When "^I allow messages$" 87 | "Enable minibuffer messages during subsequent steps" 88 | (lambda () 89 | (setq inhibit-message nil))) 90 | -------------------------------------------------------------------------------- /features/support/env.el: -------------------------------------------------------------------------------- 1 | (require 'f) 2 | (require 'subr-x) 3 | 4 | (defvar ess-smart-equals-root-path 5 | (thread-first load-file-name 6 | f-dirname 7 | f-parent 8 | f-parent)) 9 | 10 | (add-to-list 'load-path ess-smart-equals-root-path) 11 | 12 | (defvar ess-smart-equals-major-mode #'ess-r-mode 13 | "Major mode to use in smart-equals ecukes tests.") 14 | 15 | (require 'ert) 16 | (require 'espuds) 17 | 18 | (require 'ess) 19 | 20 | (setq ess-smart-equals-extra-ops '(brace paren percent)) 21 | (let ((load-prefer-newer t)) 22 | (require 'ess-smart-equals)) 23 | (ess-smart-equals-activate) 24 | 25 | (defun espuds-add-to-chain (v) 26 | "Add sequence v to the current action chain." 27 | (setq espuds-action-chain (vconcat espuds-action-chain v))) 28 | 29 | (Setup 30 | (when (boundp 'flymake-diagnostic-functions) 31 | (remove-hook 'flymake-diagnostic-functions 'flymake-proc-legacy-flymake))) 32 | 33 | (Before 34 | (switch-to-buffer 35 | (get-buffer-create "*ess-smart-equals-tests*")) 36 | (erase-buffer) 37 | (funcall ess-smart-equals-major-mode) 38 | (ess-smart-equals-mode 1)) 39 | 40 | (After 41 | (kill-buffer "*ess-smart-equals-tests*")) 42 | 43 | ;; Local Variables: 44 | ;; no-byte-compile: t 45 | ;; End: 46 | -------------------------------------------------------------------------------- /test/ess-smart-equals-test.el: -------------------------------------------------------------------------------- 1 | (ert-deftest ess-smart-equals/build-fsm-test-0 () 2 | "Tests building and matching of reverse-search finite-state machine." 3 | (let ((ops0 ["<-"]) 4 | (fsm0 [((45 1)) ((60 2 . 0)) nil])) 5 | (let ((f0 (essmeq--build-fsm ops0))) 6 | (should (equal (essmeq-matcher-fsm f0) fsm0)) 7 | (should (equal (essmeq-matcher-targets f0) ops0)) 8 | (essmeq--with-struct-slots essmeq-matcher (fsm targets span) f0 9 | (should (equal fsm fsm0)) 10 | (should (equal targets ops0)) 11 | (should (= span 2)) 12 | (with-ess-buffer 13 | (insert "a <- b ;;\n123\nZ") 14 | (should (equal (essmeq--match fsm 5 1) '(0 0 3 . 5))) 15 | (should (equal (essmeq--match fsm 12 1) nil))))))) 16 | 17 | (ert-deftest ess-smart-equals/build-fsm-test-1 () 18 | "Tests building and matching of reverse-search finite-state machine." 19 | (let ((ops1 ["<-" "=" "==" "<<-" "->" "->>" "%<>%"]) 20 | (fsm1 [((?- 1 . nil) ;; state 0 21 | (?= 3 . 1) 22 | (?> 6 . nil) 23 | (?% 10 . nil)) 24 | ((?< 2 . 0)) ;; state 1 25 | ((?< 5 . 3)) ;; state 2 26 | ((?= 4 . 2)) ;; state 3 27 | nil ;; state 4 28 | nil ;; state 5 29 | ((?- 7 . 4) ;; state 6 30 | (?> 8 . nil)) 31 | nil ;; state 7 32 | ((?- 9 . 5)) ;; state 8 33 | nil ;; state 9 34 | ((?> 11 . nil)) ;; state 10 35 | ((?< 12 . nil)) ;; state 11 36 | ((?% 13 . 6)) ;; state 12 37 | nil])) 38 | (let ((f1 (essmeq--build-fsm ops1))) 39 | (should (equal (essmeq-matcher-fsm f1) fsm1)) 40 | (should (equal (essmeq-matcher-targets f1) ops1)) 41 | (essmeq--with-struct-slots essmeq-matcher (fsm targets span partial) f1 42 | (should (equal fsm fsm1)) 43 | (should (equal targets ops1)) 44 | (should (= span 4)) 45 | (with-ess-buffer 46 | (insert "a <- b;\nc = d;\ne == f;\ng ->> h;\ni %<>% j;\n") 47 | (should (equal (essmeq--match fsm 5) '(0 0 3 . 5))) 48 | (should (equal (essmeq--match fsm 12) '(1 0 11 . 12))) 49 | (should (equal (essmeq--match fsm 20) '(2 0 18 . 20))) 50 | (should (equal (essmeq--match fsm 29) '(5 0 26 . 29))) 51 | (should (equal (essmeq--match fsm 39) '(6 0 35 . 39))) 52 | (should (equal (essmeq--match fsm 10 1) nil))) 53 | (with-ess-buffer 54 | (insert "a < b;\nc - d;\ne = f;\ng -> h;\ni %< j;\n") 55 | (should (equal (essmeq--complete fsm partial 4) '(0 1 3 . 4))) 56 | (should (equal (essmeq--complete fsm partial 11) '(4 1 10 . 11))) 57 | (should (equal (essmeq--complete fsm partial 18) '(2 1 17 . 18))) 58 | (should (equal (essmeq--complete fsm partial 26) '(5 1 24 . 26))) 59 | (should (equal (essmeq--complete fsm partial 34) '(6 2 32 . 34))) 60 | (should (equal (essmeq--complete fsm partial 6 1) nil))))))) 61 | 62 | (ert-deftest ess-smart-equals/inside-call-p-test () 63 | "Test modified check of whether we are inside a syntactic call" 64 | (with-ess-buffer 65 | (insert "if( z ) x g(y, z, w) h(a, ") 66 | (goto-char 5) 67 | (should (essmeq--inside-call-p)) 68 | (goto-char 9) 69 | (should-not (essmeq--inside-call-p)) 70 | (goto-char 16) 71 | (should (essmeq--inside-call-p)) 72 | (goto-char 20) 73 | (should (essmeq--inside-call-p)) 74 | (goto-char 21) 75 | (should-not (essmeq--inside-call-p)) 76 | (goto-char 27) 77 | (should (essmeq--inside-call-p)))) 78 | 79 | (ert-deftest ess-smart-equals/contexts () 80 | "Test modified check of whether we are inside a syntactic call" 81 | (with-ess-buffer 82 | (insert "#2345\n") 83 | (insert "\"89AB\"\n") 84 | (insert "a\n") 85 | (insert "if( u )\n") 86 | (insert "while( u )\n") 87 | (insert "f( u, v )\n") 88 | (insert "f() + 2") 89 | (should (eq (essmeq--context 4) 'comment)) 90 | (should (eq (essmeq--context 9) 'string)) 91 | (should (eq (essmeq--context 15) t)) 92 | (should (eq (essmeq--context 20) 'conditional)) 93 | (should (eq (essmeq--context 31) 'conditional)) 94 | (should (eq (essmeq--context 40) 'arglist)) 95 | (should (eq (essmeq--context) t)) 96 | (let ((ess-smart-equals-context-function (lambda () 'foo))) 97 | (should (eq (essmeq--context) 'foo))) 98 | (let ((ess-smart-equals-overriding-context 'bar)) 99 | (should (eq (essmeq--context) 'bar))))) 100 | 101 | (defmacro ess-smart-equals-th/replace-region 102 | (line left right text beg end &rest body ) 103 | "Construct a single `essmeq--replace-region' test." 104 | (declare (indent 3)) 105 | `(progn 106 | (erase-buffer) 107 | (insert "A0123456789ABCDEFGHIJKLM\n") 108 | (insert "A0123456789ABCDEFGHIJKLM\n") 109 | (insert "A0123456789ABCDEFGHIJKLM\n") 110 | (insert "A0 123456789ABCDEFGHIJKLM\n") 111 | (insert "A0 123456789ABCDEFGHIJKLM\n") 112 | (insert "A0 123456789ABCDEFGHIJKLM\n") 113 | (insert "A0 123456789ABCDEFGHIJKLM\n") 114 | (insert "A0 123456789ABCDEFGHIJKLM\n") 115 | (insert "A0 123456789ABCDEFGHIJKLM\n") 116 | (insert "A0123456789ABCDEFGHIJKLM\n") 117 | (insert "A0123456789ABCDEFGHIJKLM\n") 118 | (insert "A0123456789ABCDEFGHIJKLM\n") 119 | (goto-char (point-min)) 120 | (save-excursion 121 | (goto-char (line-beginning-position ,line)) 122 | (let ((ess-smart-equals-padding-left ,left) 123 | (ess-smart-equals-padding-right ,right)) 124 | (essmeq--replace-region ,text (+ (point) ,beg) (+ (point) ,end)) 125 | ,@body)))) 126 | 127 | (ert-deftest ess-smart-equals/replace-region () 128 | "Test essmeq--replace-region under various settings." 129 | (with-ess-buffer 130 | (ess-smart-equals-th/replace-region 1 'one-space 'one-space 131 | "<-" 2 2 132 | (should (looking-at-p ".0 <- 12"))) 133 | (ess-smart-equals-th/replace-region 2 'one-space 'one-space 134 | "<-" 2 2 135 | (should (looking-at-p ".0 <- 12"))) 136 | (ess-smart-equals-th/replace-region 1 'some-space 'some-space 137 | "<-" 2 2 138 | (should (looking-at-p ".0 <- 12"))) 139 | (ess-smart-equals-th/replace-region 1 'no-space 'no-space 140 | "<-" 2 2 141 | (should (looking-at-p ".0<-12"))) 142 | (ess-smart-equals-th/replace-region 1 'none 'none 143 | "<-" 2 2 144 | (should (looking-at-p ".0<-12"))) 145 | (ess-smart-equals-th/replace-region 1 'none "ZZZ" 146 | "<-" 2 2 147 | (should (looking-at-p ".0<-ZZZ12"))) 148 | (ess-smart-equals-th/replace-region 1 "ZZZ" 'none 149 | "<-" 2 2 150 | (should (looking-at-p ".0ZZZ<-12"))) 151 | (ess-smart-equals-th/replace-region 1 "ZZZ" 'one-space 152 | "<-" 2 2 153 | (should (looking-at-p ".0ZZZ<- 12"))) 154 | (ess-smart-equals-th/replace-region 1 'one-space (lambda (e ews &optional f) 155 | (if f 156 | (cons e (+ e 3)) 157 | (goto-char e) 158 | (insert "==="))) 159 | "<-" 2 2 160 | (should (looking-at-p ".0 <-===12"))) 161 | (ess-smart-equals-th/replace-region 1 (lambda (bws b &optional f) 162 | (if f 163 | (cons (- b 3) b) 164 | (goto-char b) 165 | (insert "==="))) 166 | 'one-space 167 | "<-" 2 2 168 | (should (looking-at-p ".0===<- 12"))) 169 | (ess-smart-equals-th/replace-region 1 'one-space (lambda (e ews &optional f) 170 | (if f 171 | (cons e (+ e 3)) 172 | (goto-char e) 173 | (insert "==="))) 174 | "<-" 2 3 175 | (should (looking-at-p ".0 <-===23"))) 176 | (ess-smart-equals-th/replace-region 5 'one-space 'one-space 177 | "<-" 5 5 178 | (should (looking-at-p ".0 <- 12"))) 179 | (ess-smart-equals-th/replace-region 4 'some-space 'some-space 180 | "<-" 5 5 181 | (should (looking-at-p ".0 <- 12"))) 182 | (ess-smart-equals-th/replace-region 4 'no-space 'no-space 183 | "<-" 5 5 184 | (should (looking-at-p ".0<-12"))) 185 | (ess-smart-equals-th/replace-region 4 'none 'none 186 | "<-" 5 5 187 | (should (looking-at-p ".0 <- 12"))) 188 | (ess-smart-equals-th/replace-region 4 'no-space 'no-space 189 | "<-" 3 10 190 | (should (looking-at-p ".0<-12"))) 191 | (ess-smart-equals-th/replace-region 4 'one-space 'no-space 192 | "<-" 2 10 193 | (should (looking-at-p ".0 <-12"))) 194 | (ess-smart-equals-th/replace-region 4 'one-space 'one-space 195 | "<-" 1 11 196 | (should (looking-at-p "A <- 2"))) 197 | (ess-smart-equals-th/replace-region 5 (lambda (bws b &optional f) 198 | (if f 199 | (cons (- b 3) b) 200 | (goto-char b) 201 | (insert "==="))) 202 | 'one-space 203 | "<-" 5 5 204 | (should (looking-at-p ".0 ===<- 12"))))) 205 | 206 | (ert-deftest ess-smart-equals/inferior-narrowing () 207 | "Test narrowing in inferior-ess-r-mode." 208 | (let ((ess-history-file nil) 209 | (ess-smart-equals-major-mode #'inferior-ess-r-mode) 210 | lo hi tmp) 211 | ;; Note: cannot test the process-mark case here 212 | (with-ess-buffer 213 | (insert "> \n> ") 214 | (setq lo (point)) 215 | (insert "if(a") 216 | (setq hi (point)) 217 | (should (equal (list lo hi) (essmeq--r-repl-current-region)))) 218 | (with-ess-buffer 219 | (insert "> ") 220 | (setq lo (point)) 221 | (insert "f(\n+ \n+ )") 222 | (setq hi (point)) 223 | (should (equal (list lo hi) (essmeq--r-repl-current-region)))) 224 | (with-ess-buffer 225 | (insert "> a\n") 226 | (setq lo (point)) 227 | (insert " [1] \"a\" \"b\" \"c\"\n") 228 | (insert " [4] ") 229 | (setq tmp (point)) 230 | (insert "\"d\" \"e\" \"f\"\n") 231 | (insert " [7] \"g\" \"h\" \"i\"\n") 232 | (setq hi (point)) 233 | (insert "> ls()\n") 234 | (insert " [1] \"stuff\"\n") 235 | (insert "> ") 236 | (goto-char tmp) 237 | (should (equal (list lo hi) (essmeq--r-repl-current-region)))) 238 | (with-ess-buffer 239 | (insert "> g(\n") 240 | (insert " Error: ill-formed expression\n") 241 | (insert "> ") 242 | (setq lo (point)) 243 | (insert "2 ") 244 | (setq tmp (point)) 245 | (insert "+ 3\n") 246 | (setq hi (point)) 247 | (insert "> a\n") 248 | (goto-char tmp) 249 | (should (equal (list lo hi) (essmeq--r-repl-current-region)))) 250 | (with-ess-buffer 251 | (insert "> u\n") 252 | (insert " \"a...\n") 253 | (insert "> ") 254 | (setq lo (point)) 255 | (insert "2 ") 256 | (setq tmp (point)) 257 | (insert "+ 3\n") 258 | (setq hi (point)) 259 | (insert "> a\n") 260 | (goto-char tmp) 261 | (should (equal (list lo hi) (essmeq--r-repl-current-region)))))) 262 | -------------------------------------------------------------------------------- /test/manual-init.el: -------------------------------------------------------------------------------- 1 | ;; Initialization code for manual tests -*- lexical-binding: t; -*- 2 | ;; 3 | ;; Use with `cask emacs -Q', so we assume that 4 | ;; cask has already setup the load path except for the package 5 | ;; directory. 6 | 7 | (add-to-list 'load-path default-directory) 8 | (setq ess-smart-equals-extra-ops '(brace paren percent)) 9 | (require 'ess) 10 | (require 'ess-smart-equals) 11 | (ess-smart-equals-activate) 12 | -------------------------------------------------------------------------------- /test/test-helper.el: -------------------------------------------------------------------------------- 1 | ;; test-helper.el -- ert-runner setup -*- lexical-binding: t; -*- 2 | ;; 3 | ;; Run automatically 4 | ;; 5 | ;; cask exec ert-runner 6 | ;; 7 | ;; or manually with 8 | ;; cask emacs -Q 9 | ;; M-x ert 10 | ;; 11 | 12 | (require 's) 13 | (require 'f) 14 | 15 | (setq debug-on-error nil) 16 | 17 | (defvar ess-smart-equals-root-path 18 | (f-parent (f-dirname load-file-name))) 19 | 20 | (defvar ess-smart-equals-major-mode #'ess-r-mode 21 | "Major mode to use in smart-equals ecukes tests.") 22 | 23 | (add-to-list 'load-path ess-smart-equals-root-path) 24 | 25 | (setq ess-smart-equals-extra-ops '(brace paren percent)) 26 | 27 | (require 'ess) 28 | (require 'ess-smart-equals) 29 | 30 | (ess-smart-equals-activate) 31 | 32 | (defmacro with-ess-buffer (&rest body) 33 | "Do BODY in a temporary buffer with an ESS major mode and smart-equals-mode. 34 | The variable `ess-smart-equals-major-mode' determines which ESS 35 | major mode is invoked." 36 | (declare (indent 0)) 37 | `(with-temp-buffer 38 | (funcall ess-smart-equals-major-mode) 39 | (ess-smart-equals-mode 1) 40 | ,@body)) 41 | --------------------------------------------------------------------------------