├── .gitignore ├── example └── example.py ├── test.jl ├── LICENSE ├── plugin └── ripple.vim ├── README.md ├── doc └── ripple.txt └── autoload └── ripple.vim /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | # `yrap` to run a paragraph 4 | a = 1 5 | b = 2 6 | c = 3 7 | 8 | # `yrr` to run line 9 | print("Hello, world!") 10 | 11 | # `yp` to rerun previous selection 12 | 13 | # `yr}` works here 14 | def hello(name): 15 | print("Hello, {}!".format(name)) 16 | 17 | # `yrf"` to run until " 18 | hello("") 19 | 20 | # Does not hang on long commands 21 | for i in range(10): 22 | time.sleep(1) 23 | print(i) 24 | 25 | # Works also with visual selection 26 | print("Hello, world!") 27 | -------------------------------------------------------------------------------- /test.jl: -------------------------------------------------------------------------------- 1 | for i in 1:10 2 | 1; 3 | 1; 4 | 1; 5 | 1; 6 | 1; 7 | 1; 8 | 1; 9 | 1; 10 | 1; 11 | 1; 12 | 1; 13 | 1; 14 | 1; 15 | 1; 16 | 1; 17 | 1; 18 | 1; 19 | 1; 20 | 1; 21 | 1; 22 | 1; 23 | 1; 24 | 1; 25 | 1; 26 | 1; 27 | 1; 28 | 1; 29 | 1; 30 | 1; 31 | 1; 32 | 1; 33 | 1; 34 | 1; 35 | 1; 36 | 1; 37 | 1; 38 | 1; 39 | 1; 40 | 1; 41 | 1; 42 | 1; 43 | 1; 44 | end 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Urbain Vaes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugin/ripple.vim: -------------------------------------------------------------------------------- 1 | " The MIT License (MIT) 2 | " 3 | " Copyright (c) 2020 Urbain Vaes 4 | " 5 | " Permission is hereby granted, free of charge, to any person obtaining a copy 6 | " of this software and associated documentation files (the "Software"), to deal 7 | " in the Software without restriction, including without limitation the rights 8 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | " copies of the Software, and to permit persons to whom the Software is 10 | " furnished to do so, subject to the following conditions: 11 | " 12 | " The above copyright notice and this permission notice shall be included in 13 | " all copies or substantial portions of the Software. 14 | " 15 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | " THE SOFTWARE. 22 | 23 | if exists('g:loaded_ripple') || &compatible 24 | finish 25 | endif 26 | let g:loaded_ripple = 1 27 | 28 | let s:default_enable_mappings = 1 29 | 30 | nnoremap (ripple_open_repl) :call ripple#open_repl(1) 31 | nnoremap (ripple_send_motion) ripple#send_motion() 32 | nnoremap (ripple_send_previous) :call ripple#send_previous() 33 | nnoremap (ripple_send_buffer) :call ripple#send_buffer() 34 | xnoremap (ripple_send_selection) :call ripple#send_visual() 35 | nmap (ripple_send_line) (ripple_send_motion)_ 36 | nnoremap (ripple_link_term) :RippleLink term 37 | 38 | if get(g:, 'ripple_enable_mappings', s:default_enable_mappings) 39 | nmap y (ripple_open_repl) 40 | nmap yr (ripple_send_motion) 41 | nmap yr (ripple_send_buffer) 42 | nmap yrr (ripple_send_line) 43 | nmap yp (ripple_send_previous) 44 | xmap R (ripple_send_selection) 45 | nmap yrL (ripple_link_term) 46 | 47 | nmap 1yr "1yr 48 | nmap 1yrr "1yrr 49 | endif 50 | 51 | command! -range -nargs=* Ripple call ripple#command(, , ) 52 | command! -complete=buffer -nargs=1 RippleLink call ripple#link_term() 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ripple 2 | 3 | This thin plugin makes it easy to send code to a REPL (read-evaluate-print loop) running within `vim` or `nvim`, 4 | which enables a workflow similar to that in a Jupyter notebook. 5 | Some advantages of this plugin over some of the alternatives (such as [iron.nvim](https://github.com/Vigemus/iron.nvim)) are the following: 6 | 7 | - This plugin is written and can be configured fully in `viml`. 8 | 9 | - The cursor does not move when a code chunk is sent to the REPL. 10 | 11 | - If [vim-highlightedyank](https://github.com/machakann/vim-highlightedyank) is installed, 12 | motions sent to the REPL are highlighted. 13 | 14 | - If not explicitly opened by `y`, 15 | the REPL opens automatically once a code chunk is sent. 16 | 17 | - The plugin is compatible with Tim Pope's [vim-repeat](https://github.com/tpope/vim-repeat). 18 | 19 | - Previous code selections are saved and can be reused easily (see `yp` in the documentation). 20 | 21 | ![](https://raw.github.com/urbainvaes/vim-ripple/demo/demo.gif) 22 | (Click to enlarge.) 23 | 24 | ## Installation 25 | 26 | Using [vim-plug](https://github.com/junegunn/vim-plug): 27 | 28 | ```vim 29 | Plug 'urbainvaes/vim-ripple' 30 | 31 | " OPTIONAL DEPENDENCIES 32 | 33 | " Highlight code chunks sent to REPL 34 | Plug 'machakann/vim-highlightedyank' 35 | 36 | " Streamline navigation (e.g. autoinsert in terminal) 37 | Plug 'urbainvaes/vim-tmux-pilot' 38 | ``` 39 | 40 | ## Configuration of the REPLs 41 | 42 | New REPLs can be defined in the dictionary `g:ripple_repls`. 43 | These definitions take precedence over the default REPLs defined in the plugin, 44 | which are listed below. 45 | In the dictionary, the keys are `filetype`s 46 | and the values are dictionaries specifying options of the REPL. 47 | The entries that each of these dictionaries can contain are given in the following table: 48 | 49 | | Keys | Default values (except for **Python**) | Description | 50 | | ---- | -------------------------------------- | ----------- | 51 | | `command` | Mandatory argument | String | 52 | | `pre` | `""` | String | 53 | | `post` | `""` | String | 54 | | `addcr` | `0` | Boolean (`0` or `1`) | 55 | | `filter` | `{x -> x}` (no effect) | `0` or a function reference | 56 | 57 | - The mandatory key `command` contains the command to start the REPL (e.g. `julia`, `guile`, `bash`). 58 | 59 | - The parameters `pre` and `post` contain strings to prepend and append to code sent to the REPL, 60 | respectively. 61 | This is sometimes necessary to enable sending several lines of code to the REPL at once 62 | (see Python example below). 63 | 64 | - The parameter `addcr` controls whether an additional `` should be appended to the code chunks that are followed by a blank line. 65 | (This can be useful to avoid the need to press `` manually in the terminal window. 66 | In `ipython`, for example, two `` are required to run an indented block.) 67 | 68 | - Finally, the parameter `filter` is a function employed to format the code before sending it to the REPL. 69 | For example, this is used in the default settings for removing comments from `zsh` code chunks, 70 | which is useful because comments are not allowed in interactive shells by default 71 | (this can be changed using `setopt interactivecomments`). 72 | 73 | The default configuration for `python` can be reproduced by the following lines in `.vimrc`: 74 | 75 | ```vim 76 | let g:ripple_repls = {} 77 | let g:ripple_repls["python"] = { 78 | \ "command": "ipython", 79 | \ "pre": "\[200~", 80 | \ "post": "\[201~", 81 | \ "addcr": 0, 82 | \ "filter": 0, 83 | \ } 84 | ``` 85 | 86 | If one wishes the plugin to work with indented code, 87 | for example in a `main()` function, 88 | one may add a filter as follows : 89 | 90 | ```vim 91 | function! Remove_leading_whitespaces(code) 92 | " Check if the first line is indented 93 | let leading_spaces = matchstr(a:code, '^\s\+') 94 | 95 | if leading_spaces == "" 96 | return a:code 97 | endif 98 | 99 | " Calculate indentation 100 | let indentation = strlen(leading_spaces) 101 | 102 | " Remove further indentations 103 | return substitute(a:code, '\(^\|\r\zs\)\s\{'.indentation.'}', "", "g") 104 | endfunction 105 | 106 | " Add filter to REPL configuration 107 | let g:ripple_repls["python"]["filter"] = function('Remove_leading_whitespaces') 108 | ``` 109 | This filter is not enabled by default, 110 | but it is implemented in the plugin by the function `ripple#remove_leading_whitespaces`, 111 | which you can use in the REPL configuration. 112 | Currently only the following languages have default configurations: 113 | *Python*, *Julia*, *Lua*, *R*, *Ruby*, *Scheme*, *Sh* and *Zsh*. 114 | Feel free to open a pull request to add support for other languages. 115 | 116 | ## Mappings 117 | 118 | The functions are exposed via `` mappings. 119 | If `g:ripple_enable_mappings` is set to `1`, 120 | then additional mappings to keys are defined as follows: 121 | 122 | | `` Mapping | Default key mapping | Description | 123 | | ----------------------------- | ------------------- | ----------- | 124 | | `(ripple_open_repl)` | `y` (`nmap`) | Open REPL | 125 | | `(ripple_send_motion)` | `yr` (`nmap`) | Send motion to REPL | 126 | | `(ripple_send_previous)` | `yp` (`nmap`) | Resend previous code selection | 127 | | `(ripple_send_selection)` | `R` (`xmap`) | Send selection to REPL | 128 | | `(ripple_send_line)` | `yrr` (`nmap`) | Send line to REPL | 129 | | `(ripple_send_buffer)` | `yr` (`nmap`) | Send whole buffer to REPL | 130 | 131 | If `(ripple_send_motion)` is issued but no REPL is open, 132 | a REPL will open automatically. 133 | A mnemonic for `yr` is *you run*. 134 | Counts and registers can be passed to `yp` in order to refer to code selections other than the last; 135 | see the documentation for details. 136 | 137 | ## Additional customization 138 | 139 | | Config | Default | Description | 140 | | ------ | ------- | ----------- | 141 | | `g:ripple_winpos` | `"vertical"` | Window position | 142 | | `g:ripple_term_name` | undefined | Name of the terminal buffer | 143 | | `g:ripple_enable_mappings` | `1` | Whether to enable default mappings | 144 | | `g:ripple_highlight` | `"DiffAdd"` | Highlight group | 145 | | `g:ripple_always_return` | `0` | Add `` even for charwise motions | 146 | 147 | The options `g:ripple_winpos` is the modifier to prepend to `new` (in `nvim`) or `term` (in `vim`) when opening the REPL window. 148 | To disable the highlighting of code chunks sent to the REPL, simply `let g:ripple_highlight = ""`. 149 | Highlighting works only when the plugin [vim-highlightedyank](https://github.com/machakann/vim-highlightedyank) is installed. 150 | For more information, see 151 | 152 | ```vim 153 | :help ripple 154 | ``` 155 | 156 | ## License 157 | 158 | MIT 159 | -------------------------------------------------------------------------------- /doc/ripple.txt: -------------------------------------------------------------------------------- 1 | *ripple.txt* 2 | 3 | Author: Urbain Vaes 4 | License: MIT 5 | 6 | ============================================================================== 7 | OVERVIEW *ripple* 8 | 9 | Ripple is a tool to seamlessly send code chunks to an interactive language 10 | shell, or read-eval-print loop (REPL). 11 | 12 | 1. Usage ............... |ripple-usage| 13 | 2. Commands ............ |ripple-commands| 14 | 3. Mappings ............ |ripple-mappings| 15 | 3. Configuration ....... |ripple-config| 16 | 17 | ============================================================================== 18 | USAGE *ripple-usage* 19 | 20 | Ripple aims to provide vim users with the ability to easily send code chunks 21 | to a language shell. This can prove very useful for quickly testing a subset 22 | of a script, or for running a script bit by bit without a debugger. 23 | 24 | ============================================================================== 25 | COMMANDS *ripple-commands* 26 | 27 | *ripple-:Ripple* 28 | :[range] Ripple [text] 29 | If the argument [text] is specified, send it to the REPL. Else, if the 30 | command is called with a [range], send the corresponding line range in 31 | the current buffer to the REPL. 32 | 33 | It can be convenient to combine this command with a mapping, e.g. > 34 | nnoremap yrc :Ripple clear 35 | < 36 | *ripple-:RippleLink* 37 | :RippleLink [bufname] 38 | Manually link the current buffer to the terminal buffer named [bufname]. 39 | Completion is enabled for buffer names. 40 | 41 | ============================================================================== 42 | MAPPINGS *ripple-mappings* 43 | 44 | Global~ 45 | 46 | The mappings are always defined, but the mappings to physical keys are 47 | enabled only if |g:ripple_enable_mappings| is set to `1`. 48 | 49 | (ripple_open_repl), |y| 50 | Open a new isolated REPL window (Normal mode mapping). 51 | Mnemonic: "you enter (the REPL)" 52 | 53 | ["x](ripple_send_motion), |yr| 54 | Send text object to the REPL window linked to the current buffer. 55 | 56 | If the current buffer hasn't been assigned a REPL yet, then it will be 57 | automatically linked to a filetype REPL, i.e. a REPL to which all buffers 58 | of the same filetype are assigned unless otherwise specified. 59 | If this filetype REPL does not exist yet, then it will be opened 60 | automatically by the plugin. 61 | 62 | If instead of using the filetype REPL, you wish to open a REPL 63 | specifically for the current buffer, run `y` to open an isolated REPL 64 | link the current buffer to it. 65 | 66 | When a register, say `x`, is passed to the mapping, the code selection is 67 | saved under label `x` for future reuse with `"x(ripple_send_previous)`. 68 | The register mechanism is employed only to pass information to the plugin; 69 | the vim registers themselves are left untouched. 70 | 71 | Mnemonic: "you run". 72 | 73 | ["x](ripple_send_selection), |R| 74 | Send text object to the REPL window (Visual mode mapping), and save the 75 | code selection under label `x`. 76 | Mnemonic: "run". 77 | 78 | ["x](ripple_send_previous), |yp| 79 | Send the latest code selection. More precisely, send the code that is 80 | currently at the location of the latest code chunk sent to the REPL. If 81 | the contents of the buffer changed in the meantime, this code might be 82 | different from the latest code selection. 83 | 84 | If a register `x` is used, `yp` will send the latest code selection saved 85 | under label `x`. For example, if a code line was previously sent to 86 | the REPL with `"ayrr`, this line can be run again with `"ayp`. 87 | 88 | A count `` may also be passed to `yp`. The code chunks sent to a 89 | REPL are stored internally in a stack, with each new code selection added 90 | at the top, and the mapping `yp` just sends to the REPL the element 91 | of the stack in position ``, where position indices start at 0. For 92 | example `1yp` sends to the REPL not the latest code selection, but the one 93 | just before, i.e. the second code selection on the stack. 94 | 95 | ["x](ripple_send_line), |yrr| 96 | Send the current line to the REPL. This is equivalent to > 97 | ["x](ripple_send_motion)_ 98 | 99 | <["x](ripple_send_buffer), |yr| 100 | Send the whole buffer to the REPL. 101 | 102 | (ripple_link_term), |yrL| 103 | Convenience mapping to > 104 | :RippleLink term 105 | < 106 | ============================================================================== 107 | CONFIGURATION *ripple-config* 108 | 109 | Global configuration~ 110 | 111 | *g:ripple_repls* 112 | Dictionary for user-defined REPLs. For buffer-local repl definitions, see 113 | |b:ripple_repl| below. 114 | 115 | Internally, a REPL configuration for a given filetype is a combination of 116 | five variables: 117 | 118 | - A {command} to start the REPL. 119 | 120 | - A pair of strings {pre} and {post} to prepend and append to 121 | code sent to the REPL, respectively. This is sometimes necessary 122 | to enable sending several lines of code to the REPL at once. 123 | 124 | - A boolean {addcr} that controls whether an additional 125 | should be appended to the code chunks that are followed by a blank 126 | line. (This can be useful to avoid the need to press manually 127 | in the terminal window. In ipython, for example, two are 128 | required to run an indented block.) 129 | 130 | - A parameter {filter} to further filter the code before sending it 131 | to the REPL. This parameter is a function reference, or just `0` 132 | if no filter is in use. The vim function |function()| enables to 133 | obtain the reference to a function. 134 | 135 | In the dictionary |g:ripple_repl|, the keys are |filetype|s and the values 136 | are either of the following: 137 | 138 | - A string containing the {command} to start the REPL (e.g. 'bash', 139 | 'guile'). In this case the other parameters are assumed to be as 140 | follows: {pre} = "", {post} = "", {addcr} = 0 and {filter} = 0. 141 | 142 | - A dictionary specifying the REPL options, with keys corresponding to 143 | the five parameters described above; that is, 'command', 'pre', 144 | 'post', 'addcr', and 'filter'. The 'command' parameter is required 145 | and the other parameters are optional. If an optional parameter is 146 | omitted, the plugin employs a language-specific default value, if 147 | one is defined in the plugin code, or else the same defaults values 148 | as in the previous item: {pre} = "", {post} = "", {addcr} = 0 and 149 | {filter} = 0. At the moment, language-specific defaults are defined 150 | only for Python and Zsh, as presented below. 151 | 152 | Default values: > 153 | let s:default_repls = { 154 | \ "python": { 155 | \ "exec": "ipython", 156 | \ "pre": "\\[200~", 157 | \ "post": "\[201~", 158 | \ "addcr": 1, 159 | \ "filter": 0, 160 | \ }, 161 | \ "julia": "julia", 162 | \ "lua": "lua", 163 | \ "r": "R", 164 | \ "ruby": "irb", 165 | \ "scheme": "guile", 166 | \ "sh": "bash", 167 | \ "zsh": { 168 | \ "exec": "zsh", 169 | \ "filter": function('s:remove_comments'), 170 | \ } 171 | \ } 172 | 173 | *g:ripple_winpos* 174 | Modifier for the window position. In nvim, this modifier is prepended to 175 | the |new| command when opening the terminal window. In vim, it is 176 | prepended to the |term| command. 177 | Default: 'vertical'. 178 | 179 | *g:ripple_enable_mappings* 180 | Set this to 0 to disable all mappings apart from the mappings. 181 | Default: 1. 182 | 183 | *g:ripple_highlight* 184 | Highlight group employed to highlight text objects sent to the REPL. 185 | Default: '|DiffAdd|'. 186 | 187 | *g:ripple_term_name* 188 | Name of the REPL buffer. 189 | 190 | *g:ripple_term_options* (vim only) 191 | Dictionary with additional options to pass to `term_start()`. 192 | 193 | *g:ripple_always_return* 194 | If this parameter is 1, then a newline character is always appended to 195 | motions sent to a REPL. If this parameter is 0, then a newline character 196 | is sent only if the motion is |linewise|, and not if the motion is |charwise|. 197 | Default: 0. 198 | 199 | *g:ripple_prev_in_buf* 200 | If 1, then |yp| looks for previous code chunks only within the current 201 | buffer. If 0, |yp| resends the latest code chunk among all buffers paired 202 | with the terminal. 203 | Default: 1. 204 | 205 | Buffer-local configuration~ 206 | 207 | *b:ripple_repl* 208 | REPL configuration for current buffer. This takes precedence over 209 | the global option `g:ripple_repls[&filetype]`. REPLs which are created 210 | from buffers where this variable is defined are isolated: they can receive 211 | input only from the parent buffer. 212 | 213 | *b:ripple_term_name* 214 | Name of the REPL buffer. This takes precedence over 215 | the global option `g:ripple_term_name`. 216 | 217 | ============================================================================== 218 | CHANGELOG *ripple-changelog* 219 | 220 | Nov 2021~ 221 | Modify the default behavior of |yp|: this now reruns the previous code 222 | chunk from within the current buffer, and no longer from all buffers 223 | paired with the current terminal. The previous behavior can be restored by 224 | setting the option > 225 | :let g:ripple_prev_in_buf=0 226 | < 227 | Nov 2021~ 228 | Add the |:RippleLink| command. 229 | 230 | Jul 2021~ 231 | Add the |g:ripple_always_return| option. 232 | 233 | Jul 2021~ 234 | Improve the data structures for specifying the REPLS. Configuration for 235 | the REPLS can now be achieved through dictionaries, which are more 236 | descriptive and flexible than lists. In particular, only a subset of the 237 | options can be specified, in which case the other options take default 238 | values. 239 | 240 | Dec 2020~ 241 | Previous selections associated with a given buffer are now stored in a 242 | stack, and they can be run by passing a count to `yp`. For example, `1yp` 243 | will send to the REPL the second element of the stack, and `2yp` will run 244 | the third. Without a count, `yp` behaves as it used to: it sends to the 245 | REPL the code chunk at the top of the stack, i.e. the one in position 0. 246 | 247 | Nov 2020~ 248 | REPLs are now either isolated or not isolated. Isolated REPLs receive 249 | input from only one buffer, whereas non-isolated REPLs can reveive input 250 | from several buffers that have the same filetype. 251 | 252 | The mapping `y` will now start an isolated REPL. To start or attach to 253 | a REPL common to all buffers of the filetype, simply run a command of the 254 | type `yr`. 255 | 256 | Oct 2020~ 257 | Enable saving several selections for later use with `"yp`. 258 | For example, a line initially run with `"3yrr` can be run again with 259 | `"3yp`. The register mechanism is used only to pass information; the vim 260 | registers themselves are left untouched by the plugin. 261 | 262 | ============================================================================== 263 | 264 | vim:tw=78:ts=4:et:ft=help:norl: 265 | -------------------------------------------------------------------------------- /autoload/ripple.vim: -------------------------------------------------------------------------------- 1 | " The MIT License (MIT) 2 | " 3 | " Copyright (c) 2020 Urbain Vaes 4 | " 5 | " Permission is hereby granted, free of charge, to any person obtaining a copy 6 | " of this software and associated documentation files (the "Software"), to deal 7 | " in the Software without restriction, including without limitation the rights 8 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | " copies of the Software, and to permit persons to whom the Software is 10 | " furnished to do so, subject to the following conditions: 11 | " 12 | " The above copyright notice and this permission notice shall be included in 13 | " all copies or substantial portions of the Software. 14 | " 15 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | " THE SOFTWARE. 22 | 23 | let s:default_autoinsert = 1 24 | let s:default_highlight = "DiffAdd" 25 | let s:default_winpos = "vertical" 26 | let s:default_delay = "500m" 27 | let s:default_term_name = "term: ripple" 28 | let s:default_always_return = 0 29 | let s:default_prev_in_buf = 0 30 | 31 | function! s:remove_comments(code) 32 | return substitute(a:code, "^#[^\r]*\r\\|\r#[^\r]*", "", "g") 33 | endfunction 34 | 35 | let s:default_repls = { 36 | \ "python": { 37 | \ "command": "ipython", 38 | \ "pre": "\[200~", 39 | \ "post": "\[201~", 40 | \ "addcr": 0, 41 | \ "filter": 0, 42 | \ }, 43 | \ "julia": "julia", 44 | \ "lua": "lua", 45 | \ "r": "R", 46 | \ "ruby": "irb", 47 | \ "scheme": "guile", 48 | \ "sh": "bash", 49 | \ "zsh": { 50 | \ "command": "zsh", 51 | \ "filter": function('s:remove_comments'), 52 | \ } 53 | \ } 54 | 55 | " Memory for the state of the plugin 56 | let s:sources = {} 57 | let s:repl_params = {} 58 | let s:buf_to_term = {} 59 | let s:ft_to_term = {} 60 | 61 | " Last used source 62 | let s:source = "" 63 | 64 | function! s:echo(string) 65 | echohl Type 66 | echon "vim-ripple: " 67 | echohl None 68 | echon a:string 69 | echon "\" 70 | endfunction 71 | 72 | function! ripple#status() 73 | let [bufn, ft] = [bufnr('%'), &ft] 74 | if !has_key(s:buf_to_term, bufn) 75 | call s:echo("Buffer is not paired to any terminal yet…") 76 | return 0 77 | elseif s:is_isolated() 78 | call s:echo("Buffer is paired with isolated REPL in buffer number ".s:buf_to_term[bufn].".") 79 | else 80 | call s:echo("Buffer is paired with shared ".ft." REPL in buffer number ".s:buf_to_term[bufn].".") 81 | endif 82 | endfunction 83 | 84 | function! s:set_repl_params() 85 | " FIXME: The keys of s:repl_params should probably be buffers instead of 86 | " filetypes… Also, the presence of this buffer variable should force a 87 | " separate REPL. EDIT: probably fine because dictionary is used only at 88 | " REPL startup… 89 | if has_key(b:, 'ripple_repl') 90 | let repl = b:ripple_repl 91 | else 92 | let repls = deepcopy(s:default_repls) 93 | if has_key(g:, 'ripple_repls') 94 | call extend(repls, g:ripple_repls) 95 | endif 96 | if has_key(repls, &ft) 97 | let repl = repls[&ft] 98 | else 99 | echom "No repl for filetype '".&ft."'…" 100 | return -1 101 | endif 102 | endif 103 | 104 | if type(repl) == 1 105 | let repl = {"command": repl} 106 | endif 107 | 108 | " Legacy 109 | if type(repl) == 3 110 | let repl = { 111 | \ "command": repl[0], 112 | \ "pre": repl[1], 113 | \ "post": repl[2], 114 | \ "addcr": repl[3], 115 | \ "filter": len(repl) > 4 ? repl[4] : 0 116 | \ } 117 | endif 118 | 119 | let params = {"pre": "", "post": "", "addcr": 0, "filter": 0} 120 | call extend(params, repl) 121 | 122 | let s:repl_params[&ft] = params 123 | return 0 124 | endfunction 125 | 126 | function! s:is_isolated() 127 | let [bufn, ft] = [bufnr('%'), &ft] 128 | 129 | " If term opened 130 | if has_key(s:buf_to_term, bufn) 131 | if !has_key(s:ft_to_term, ft) 132 | return 1 133 | elseif s:ft_to_term[ft] != s:buf_to_term[bufn] 134 | return 1 135 | else 136 | return 0 137 | endif 138 | endif 139 | 140 | " If term not opened 141 | if has_key(b:, 'ripple_term_name') || has_key(b:, 'ripple_repl') 142 | return 1 143 | else 144 | return 0 145 | endif 146 | endfunction 147 | 148 | function! s:assign_repl() 149 | if has_key(b:, 'ripple_term_name') || has_key(b:, 'ripple_repl') 150 | return ripple#open_repl(1) 151 | endif 152 | 153 | let [bufn, ft] = [bufnr('%'), &ft] 154 | if has_key(s:ft_to_term, ft) && bufexists(s:ft_to_term[ft]) 155 | \ && (has('nvim') || term_getstatus(s:ft_to_term[ft]) != "finished") 156 | let s:buf_to_term[bufn] = s:ft_to_term[ft] 157 | return 0 158 | endif 159 | return ripple#open_repl(0) 160 | endfunction 161 | 162 | function! ripple#open_repl(isolated) 163 | let [bufn, ft] = [bufnr('%'), &ft] 164 | 165 | if a:isolated 166 | echohl Type 167 | echon "vim-ripple: " 168 | echohl None 169 | echon "Opening an isolated REPL. To open a REPL common to all buffers of filetype '" 170 | echohl Identifier 171 | echon ft 172 | echohl None 173 | echon "', use '" 174 | echohl Identifier 175 | echon "yr" 176 | echohl None 177 | echon "' directly.\" 178 | endif 179 | 180 | let winid = win_getid() 181 | if s:set_repl_params() == -1 182 | return -1 183 | endif 184 | 185 | let legacy = 'no' 186 | if has_key(g:, 'ripple_window') 187 | let legacy = 'g:ripple_window' 188 | elseif has_key(g:, 'ripple_term_command') 189 | let legacy = 'g:ripple_term_command' 190 | endif 191 | if legacy == 'no' 192 | let winpos = get(g:, 'ripple_winpos', s:default_winpos) 193 | 194 | let term_name = "" 195 | if has_key(b:, 'ripple_term_name') 196 | let term_name = b:ripple_term_name 197 | else 198 | let term_name = get(g:, 'ripple_term_name', s:default_term_name) 199 | if a:isolated 200 | let term_name = term_name."_".ft."_b".bufn 201 | else 202 | let term_name = term_name."_".ft."_common" 203 | endif 204 | endif 205 | 206 | if !a:isolated && !has('nvim') && bufexists(term_name) 207 | let nr = bufnr(term_name) 208 | if term_getstatus(nr) == "finished" 209 | call s:echo("Wiping out finished term buffer!") 210 | execute "bwipeout" nr 211 | endif 212 | endif 213 | 214 | " To enable resourcing the plugin 215 | if !a:isolated && bufexists(term_name) 216 | let nr = bufnr(term_name) 217 | let s:ft_to_term[ft] = nr 218 | let s:buf_to_term[bufn] = nr 219 | return 0 220 | endif 221 | 222 | while bufexists(term_name) 223 | if !a:isolated 224 | echom "Buffer '".term_name."' already exists…" 225 | return -1 226 | endif 227 | let term_name = term_name . "_new" 228 | endwhile 229 | 230 | if has_key(g:, 'ripple_winexpr') 231 | silent execute winpos.eval(g:ripple_winexpr)." new" 232 | else 233 | silent execute winpos." new" 234 | endif 235 | 236 | if has("nvim") 237 | silent execute "term" s:repl_params[ft]["command"] 238 | if term_name != "" 239 | exec "file ".term_name 240 | endif 241 | else 242 | let term_options = {"curwin": 1} 243 | if term_name != "" 244 | let term_options["term_name"] = term_name 245 | endif 246 | if has_key(g:, 'ripple_term_options') 247 | call extend(term_options, g:ripple_term_options) 248 | endif 249 | silent call term_start(s:repl_params[ft]["command"], term_options) 250 | endif 251 | else 252 | " Legacy code 253 | echohl Type 254 | echon "vim-ripple: " 255 | echohl Identifier 256 | echon "'".legacy."'" 257 | echohl None 258 | echon " is deprecated; use " 259 | echohl Identifier 260 | echon "'g:ripple_winpos'" 261 | echohl None 262 | echon " instead.\" 263 | 264 | let s:default_window = "vnew" 265 | let s:default_term_command = "vertical terminal" 266 | if has("nvim") 267 | let new_window = get(g:, 'ripple_window', s:default_window) 268 | silent execute new_window 269 | silent execute "term" s:repl_params[ft]["command"] 270 | else 271 | let term_command = get(g:, 'ripple_term_command', s:default_term_command) 272 | silent execute term_command s:repl_params[ft]["command"] 273 | endif 274 | endif 275 | 276 | if has("nvim") 277 | " Move cursor to last line to follow output 278 | norm G 279 | if &runtimepath =~ 'vim-tmux-pilot' 280 | call pilot#autoinsert() 281 | endif 282 | endif 283 | 284 | let term_buf = bufnr('%') 285 | call win_gotoid(winid) 286 | 287 | let delay = get(g:, 'ripple_delay', s:default_delay) 288 | execute "sleep" delay 289 | 290 | let s:buf_to_term[bufn] = term_buf 291 | if !a:isolated 292 | let s:ft_to_term[ft] = term_buf 293 | endif 294 | return 0 295 | endfunction 296 | 297 | function! s:send_to_buffer(formatted) 298 | let [ft, bufn] = [&ft, bufnr('%')] 299 | if !has_key(s:buf_to_term, bufn) 300 | echom "No term buffer opened for buffer '".bufn."'…" 301 | return -1 302 | endif 303 | let tabnr = tabpagenr() 304 | tab split 305 | " Silent for vim 306 | silent execute "noautocmd buffer" s:buf_to_term[bufn] 307 | norm G$ 308 | if has("nvim") 309 | if s:repl_params[ft]["command"] == "radian" 310 | put =a:formatted 311 | else 312 | put =a:formatted 313 | " call chansend(getbufvar("%", '&channel'), a:formatted) 314 | end 315 | else 316 | let typed_string = "\\a".a:formatted 317 | call feedkeys(typed_string, "ntx") 318 | endif 319 | tab close 320 | noautocmd execute 'tabnext' tabnr 321 | endfunction 322 | 323 | function! s:send_code(...) 324 | let bufn = bufnr('%') 325 | if !has_key(s:buf_to_term, bufn) || !buffer_exists(s:buf_to_term[bufn]) 326 | \ || (!has('nvim') && term_getstatus(s:buf_to_term[bufn]) == "finished") 327 | if s:assign_repl() == -1 328 | return 329 | endif 330 | endif 331 | 332 | if a:0 == 0 333 | " Add (useful e.g. so that python functions get run) 334 | let ft = s:source['ft'] 335 | let code = s:extract_code() 336 | let code = (s:is_end_paragraph() && s:repl_params[ft]["addcr"]) ? code."\" : code 337 | let always_return = get(g:, "ripple_always_return", s:default_always_return) 338 | let newline = (s:is_charwise() && !always_return) ? "" : "\" 339 | if s:repl_params[ft]["filter"] != 0 340 | let code = s:repl_params[ft]["filter"](code) 341 | endif 342 | else 343 | let ft = &ft 344 | let code = a:1 345 | let newline = "\" 346 | endif 347 | let bracketed_paste = [s:repl_params[ft]["pre"], s:repl_params[ft]["post"]] 348 | let formatted_code = bracketed_paste[0].code.bracketed_paste[1] 349 | call s:send_to_buffer(formatted_code) 350 | 351 | " Hack for windows 352 | " Before, the new line was appended to `formatted_code`. 353 | if has_key(g:, "ripple_sleep_hack") 354 | redraw 355 | exe "sleep ".g:ripple_sleep_hack 356 | end 357 | call s:send_to_buffer(newline) 358 | endfunction 359 | 360 | function! s:is_charwise() 361 | let mode = s:source['mode'] 362 | return (mode ==# "char" || mode ==# "v") 363 | endfunction 364 | 365 | function! s:is_end_paragraph() 366 | return s:source['mode'] == "line" 367 | \ && getline(s:source['line_end']) != "" 368 | \ && getline(s:source['line_end'] + 1) == "" 369 | endfunction 370 | 371 | function! s:extract_code() 372 | let lines = getbufline(s:source['bufnr'], s:source['line_start'], s:source['line_end']) 373 | if s:is_charwise() && len(lines) > -1 374 | let lines[-1] = lines[-1][:s:source['column_end'] - 1] 375 | let lines[0] = lines[0][s:source['column_start'] - 1:] 376 | endif 377 | " Sometimes, for example with the motion `}`, the line where the cursor 378 | " lands is not included, which is often undesirable for this plugin. 379 | " For example, without an extra , running `yr}` on a Python function 380 | " with an empty line after it will paste the code of the function but not 381 | " execute it. 382 | if empty(lines) 383 | return 384 | endif 385 | let code = join(lines, "\") 386 | return code 387 | endfunction 388 | 389 | function! s:highlight() 390 | let buf_win = bufwinnr(s:source['bufnr']) 391 | if buf_win == -1 392 | return 393 | endif 394 | 395 | let cur_win = winnr() 396 | exe buf_win."wincmd w" 397 | 398 | let higroup = get(g:, 'ripple_highlight', s:default_highlight) 399 | if &runtimepath =~ 'highlightedyank' && higroup != "" 400 | let start = [0, s:source['line_start'], s:source['column_start'], 0] 401 | let end = [0, s:source['line_end'], s:source['column_end'], 0] 402 | let type = s:is_charwise() ? 'v' : 'V' 403 | let delay = 1000 404 | call highlightedyank#highlight#add(higroup, start, end, type, delay) 405 | endif 406 | 407 | exe cur_win."wincmd w" 408 | endfunction 409 | 410 | function! s:new_source(reg) 411 | let key = s:is_isolated() ? bufnr('%') : &ft 412 | if !has_key(s:sources, key) 413 | let s:sources[key] = {} 414 | endif 415 | if !has_key(s:sources[key], a:reg) 416 | let s:sources[key][a:reg] = [] 417 | endif 418 | if len(s:sources[key][a:reg]) > 9 419 | call remove(s:sources[key][a:reg], -1) 420 | end 421 | call insert(s:sources[key][a:reg], {}, 0) 422 | return s:sources[key][a:reg][0] 423 | endfunction 424 | 425 | function! s:send_lines(l1, l2) 426 | let s:ft = &ft 427 | let s:source = s:new_source(v:register) 428 | let [s:source['mode'], s:source['ft']] = ["line", &ft] 429 | let [s:source['line_start'], s:source['line_end']] = [a:l1, a:l2] 430 | let [s:source['column_start'], s:source['column_end']] = [-1, -1] 431 | let s:source['bufnr'] = bufnr("%") 432 | call s:send_code() 433 | call s:highlight() 434 | endfunction 435 | 436 | function! s:extract_source() 437 | let is_visual = (s:source['mode'] ==# "v" || s:source['mode'] ==# "V") 438 | let m1 = is_visual ? "'<" : "'[" 439 | let m2 = is_visual ? "'>" : "']" 440 | let [s:source['line_start'], s:source['column_start']] = getpos(l:m1)[1:2] 441 | let [s:source['line_end'], s:source['column_end']] = getpos(l:m2)[1:2] 442 | let s:source['bufnr'] = bufnr("%") 443 | if s:is_charwise() && is_visual && &selection=='exclusive' 444 | let s:source['column_end'] = s:source['column_end'] - 1 445 | endif 446 | endfunction 447 | 448 | function! ripple#command(l1, l2, text) 449 | if a:text != "" 450 | call s:send_code(a:text) 451 | else 452 | call s:send_lines(a:l1, a:l2) 453 | endif 454 | endfunction 455 | 456 | function! ripple#send_previous() 457 | let [myreg, mycount] = [v:register, v:count] 458 | let key = s:is_isolated() ? bufnr('%') : &ft 459 | 460 | if !has_key(s:sources, key) 461 | echom "No previous selection…" 462 | return -1 463 | endif 464 | if !has_key(s:sources[key], myreg) 465 | echom "Register is empty…" 466 | return -1 467 | endif 468 | 469 | let sources = s:sources[key][myreg] 470 | let buf_sources = sources 471 | if get(g:, 'ripple_prev_in_buf', s:default_prev_in_buf) 472 | let bufnr = bufnr("%") 473 | let buf_sources = [] 474 | for source in sources 475 | if source['bufnr'] == bufnr 476 | call add(buf_sources, source) 477 | endif 478 | endfor 479 | endif 480 | 481 | if len(buf_sources) <= mycount 482 | echom "There are only ".len(buf_sources)." in memory…" 483 | return -1 484 | endif 485 | if !buflisted(buf_sources[mycount]['bufnr']) 486 | echom "Buffer no longer exists…" 487 | return -1 488 | endif 489 | 490 | let s:source = buf_sources[mycount] 491 | call s:send_code() 492 | call s:highlight() 493 | endfunction 494 | 495 | function! ripple#send_buffer() 496 | let reg = v:register 497 | let s:source = s:new_source(reg) 498 | let s:source["mode"] = "line" 499 | let s:source["ft"] = &ft 500 | let [s:source['line_start'], s:source['line_end']] = [1, line('$')] 501 | let [s:source['column_start'], s:source['column_end']] = [-1, -1] 502 | let s:source['bufnr'] = bufnr("%") 503 | call s:send_code() 504 | call s:highlight() 505 | silent! call repeat#set(":\call ripple#send_buffer()\", v:count) 506 | endfunction 507 | 508 | function! ripple#send_visual() 509 | let reg = v:register 510 | let s:source = s:new_source(reg) 511 | let s:source['mode'] = visualmode() 512 | let s:source['ft'] = &ft 513 | call s:extract_source() 514 | call s:send_code() 515 | call s:highlight() 516 | endfunction 517 | 518 | function! ripple#save() 519 | let s:save_cursor = getcurpos() 520 | let s:save_view = winsaveview() 521 | endfunction 522 | 523 | function! ripple#accept_motion(...) 524 | let reg = v:register 525 | let s:source = s:new_source(reg) 526 | let s:source['mode'] = a:1 527 | let s:source['ft'] = &ft 528 | call s:extract_source() 529 | call s:send_code() 530 | call s:highlight() 531 | call setpos('.', s:save_cursor) 532 | call winrestview(s:save_view) 533 | silent! call repeat#set(":\call ripple#save() \ norm! .\", v:count) 534 | endfunction 535 | 536 | function! ripple#send_motion() 537 | call ripple#save() 538 | set operatorfunc=ripple#accept_motion 539 | return 'g@' 540 | endfunction 541 | 542 | function! ripple#link_term(buffer) 543 | let bufn = bufnr(a:buffer) 544 | let s:buf_to_term[bufnr("%")] = bufn 545 | endfunction 546 | 547 | function! ripple#remove_leading_whitespaces(code) 548 | 549 | " Check if the first line is indented 550 | let leading_spaces = matchstr(a:code, '^\s\+') 551 | 552 | if leading_spaces == "" 553 | return a:code 554 | endif 555 | 556 | " Calculate indentation 557 | let indentation = strlen(leading_spaces) 558 | 559 | " Remove further indentations 560 | return substitute(a:code, '\(^\|\r\zs\)\s\{'.indentation.'}', "", "g") 561 | endfunction 562 | --------------------------------------------------------------------------------