├── .gitignore ├── styles └── r-exec.less ├── keymaps └── r-exec.cson ├── package.json ├── LICENSE ├── menus └── r-exec.cson ├── lib ├── applescript │ ├── safari-rstudio.applescript │ └── chrome-rstudio.applescript └── r-exec.coffee ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Rapp.history 3 | npm-debug.log 4 | node_modules 5 | -------------------------------------------------------------------------------- /styles/r-exec.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .r-exec { 8 | } 9 | -------------------------------------------------------------------------------- /keymaps/r-exec.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 11 | # eventually make it like R-box with cmd-enter 12 | # and have custom command palette to choose application 13 | # https://discuss.atom.io/t/custom-use-of-the-command-palette/15702/2 14 | 'atom-text-editor': 15 | 'shift-cmd-e': 'r-exec:setwd' 16 | 'cmd-enter': 'r-exec:send-command' 17 | 'shift-cmd-u': 'r-exec:send-function' 18 | 'shift-cmd-k': 'r-exec:send-knitr' 19 | 'shift-cmd-m': 'r-exec:send-paragraph' 20 | 'alt--': 'r-exec:insert-assignment' 21 | 'shift-alt-m': 'r-exec:insert-pipe' 22 | 'shift-alt-p': 'r-exec:send-previous-command' 23 | # shift-cmd y, u, i, j, k, x, m 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r-exec", 3 | "main": "./lib/r-exec", 4 | "version": "0.5.0", 5 | "description": "Send R code to various R consoles", 6 | "activationCommands": { 7 | "atom-text-editor": [ 8 | "r-exec:send-command", 9 | "r-exec:send-function", 10 | "r-exec:send-paragraph", 11 | "r-exec:send-knitr", 12 | "r-exec:setwd", 13 | "r-exec:set-chrome", 14 | "r-exec:set-iterm", 15 | "r-exec:set-iterm2", 16 | "r-exec:set-rapp", 17 | "r-exec:set-rstudio", 18 | "r-exec:set-safari", 19 | "r-exec:set-terminal", 20 | "r-exec:insert-assignment", 21 | "r-exec:insert-pipe" 22 | ] 23 | }, 24 | "repository": "https://github.com/pimentel/atom-r-exec", 25 | "keywords": [ 26 | "R", 27 | "rstats", 28 | "code", 29 | "run", 30 | "runner" 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "atom": ">1.0.0" 35 | }, 36 | "dependencies": { 37 | "node-osascript": "1.0.4", 38 | "path": "0.12.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Harold Pimentel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /menus/r-exec.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | { 3 | 'label': 'Packages' 4 | 'submenu': [ 5 | 'label': 'R-exec' 6 | 'submenu': [ 7 | { 'label': 'Send line/selection', 'command': 'r-exec:send-command'} 8 | { 'label': 'Send function', 'command': 'r-exec:send-function'} 9 | { 'label': 'Send paragraph', 'command': 'r-exec:send-paragraph'} 10 | { 'label': 'Send knitr', 'command': 'r-exec:send-knitr'} 11 | { 'label': 'Send previous command', 'command': 'r-exec:send-previous-command'} 12 | { 'label': 'Change working directory', 'command': 'r-exec:setwd'} 13 | 'label': 'Change app' 14 | 'submenu': [ 15 | { 'label': 'Chrome', 'command': 'r-exec:set-chrome'} 16 | { 'label': 'iTerm', 'command': 'r-exec:set-iterm'} 17 | { 'label': 'iTerm2', 'command': 'r-exec:set-iterm2'} 18 | { 'label': 'R.app', 'command': 'r-exec:set-rapp'} 19 | { 'label': 'RStudio', 'command': 'r-exec:set-rstudio'} 20 | { 'label': 'Safari', 'command': 'r-exec:set-safari'} 21 | { 'label': 'Terminal', 'command': 'r-exec:set-terminal'} 22 | ] 23 | ] 24 | ] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /lib/applescript/safari-rstudio.applescript: -------------------------------------------------------------------------------- 1 | -- returns 0 if no windows found 2 | -- returns 1 if successfully sent command 3 | -- returns 2 if more than 1 window found and a RStudio window is not currently active 4 | global shouldActivate 5 | 6 | on runSafari(code, whichWindow, whichTab) 7 | -- code adapted from sublime package: 8 | -- https://github.com/randy3k/SendREPL 9 | tell application "Safari" 10 | if shouldActivate then 11 | activate 12 | -- having trouble figuring out how to raise the proper tab 13 | -- tell window whichWindow to set current tab to whichTab 14 | -- set current tab to whichTab 15 | end if 16 | set cmd to " 17 | var input = document.getElementById('rstudio_console_input'); 18 | var textarea = input.getElementsByTagName('textarea')[0]; 19 | textarea.value += \"" & code & "\"; 20 | var e = document.createEvent('KeyboardEvent'); 21 | e.initKeyboardEvent('input'); 22 | textarea.dispatchEvent(e); 23 | var e = document.createEvent('KeyboardEvent'); 24 | e.initKeyboardEvent('keydown'); 25 | Object.defineProperty(e, 'keyCode', {'value' : 13}); 26 | input.dispatchEvent(e); 27 | " 28 | tell tab whichTab of window whichWindow to do JavaScript cmd 29 | end tell 30 | end runSafari 31 | 32 | tell application "Safari" 33 | set currentTabName to name of front document 34 | if (currentTabName is "RStudio") then 35 | my runSafari(incoming, 1, index of current tab of window 1) 36 | return 1 37 | end if 38 | 39 | set potentialWindows to {} 40 | set windowCount to number of windows 41 | repeat with w from 1 to windowCount 42 | try 43 | set tabCount to number of tabs in window w 44 | repeat with t from 1 to tabCount 45 | set tabName to name of tab t of window w 46 | if (tabName is "RStudio") then 47 | set potentialWindows to potentialWindows & {{w, t, URL of tab t of window w}} 48 | end if 49 | end repeat 50 | on error msg 51 | -- intentionally empty 52 | end try 53 | end repeat 54 | 55 | if (count of potentialWindows) is 1 then 56 | set executeContext to item 1 of potentialWindows 57 | my runSafari(incoming, item 1 of executeContext, item 2 of executeContext) 58 | return 1 59 | else if (count of potentialWindows) is 0 then 60 | return 0 61 | end if 62 | 63 | return 2 64 | end tell 65 | -------------------------------------------------------------------------------- /lib/applescript/chrome-rstudio.applescript: -------------------------------------------------------------------------------- 1 | -- returns 0 if no windows found 2 | -- returns 1 if successfully sent command 3 | -- returns 2 if more than 1 window found and a RStudio window is not currently active 4 | global shouldActivate 5 | 6 | on runGoogleChrome(code, whichWindow, whichTab) 7 | -- code adapted from sublime package: 8 | -- https://github.com/randy3k/SendREPL 9 | tell application "Google Chrome" 10 | if shouldActivate then 11 | activate 12 | -- having trouble figuring out how to raise the proper tab 13 | -- tell window whichWindow to set current tab to whichTab 14 | -- set current tab to whichTab 15 | end if 16 | 17 | set cmd to "javascript:{" & " 18 | var input = document.getElementById('rstudio_console_input'); 19 | var textarea = input.getElementsByTagName('textarea')[0]; 20 | textarea.value += \"" & code & "\"; 21 | var e = document.createEvent('KeyboardEvent'); 22 | e.initKeyboardEvent('input'); 23 | textarea.dispatchEvent(e); 24 | var e = document.createEvent('KeyboardEvent'); 25 | e.initKeyboardEvent('keydown'); 26 | Object.defineProperty(e, 'keyCode', {'value' : 13}); 27 | input.dispatchEvent(e); 28 | " & "}" 29 | 30 | set URL of tab whichTab of window whichWindow to cmd 31 | end tell 32 | end run 33 | 34 | tell application "Google Chrome" 35 | set currentTabName to title of active tab of front window 36 | if (currentTabName is "RStudio") then 37 | my runGoogleChrome(incoming, 1, active tab index of front window) 38 | return 1 39 | end if 40 | 41 | set potentialWindows to {} 42 | set windowCount to number of windows 43 | repeat with w from 1 to windowCount 44 | try 45 | set tabCount to number of tabs in window w 46 | repeat with t from 1 to tabCount 47 | set tabName to name of tab t of window w 48 | if (tabName is "RStudio") then 49 | set potentialWindows to potentialWindows & {{w, t, URL of tab t of window w}} 50 | end if 51 | end repeat 52 | on error msg 53 | -- intentionally empty 54 | end try 55 | end repeat 56 | 57 | if (count of potentialWindows) is 1 then 58 | set executeContext to item 1 of potentialWindows 59 | my runGoogleChrome(incoming, item 1 of executeContext, item 2 of executeContext) 60 | return 1 61 | else if (count of potentialWindows) is 0 then 62 | return 0 63 | end if 64 | 65 | return 2 66 | end tell 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.0 - Complete overhaul of Browser commands ([@pimentel](https://github.com/pimentel)) 2 | - Browser commands no longer use copy/paste 3 | - Browser commands can now send in the background (without raising the application) 4 | - In browser mode, if the tab is not selected, r-exec will attempt to find the correct tab if only one session exists. 5 | 6 | ## 0.4.2 - Add support for RStudio ([@pimentel](https://github.com/pimentel)) 7 | - Add support for RStudio 8 | 9 | ## 0.4.1 - Fix multiple bugs, skipping comments as option ([@pimentel](https://github.com/pimentel)) 10 | - Additional documentation for 'smart' insert 11 | - Fixed bug when sending to browser and console not selected 12 | - Fixed bug when skipping lines with comments 13 | 14 | ## 0.4.0 - Fix multiple bugs, send previous command, smart insert operators ([@pimentel](https://github.com/pimentel)) 15 | - Fixed bug when trying to send 1 line knitr blocks 16 | - Can now send previous command 17 | - Now has a 'smart' insert assignment operator ('<\-') and 'smart' insert pipe operator ('%>%') 18 | 19 | ## 0.3.5 - Support for iTerm2 3.0.0 ([@pimentel](https://github.com/pimentel)) 20 | - iTerm2 3.0.0 now supported under mode `iTerm2`. Older versions of iTerm2 are supported under mode `iTerm` due to a recent API change. 21 | 22 | ## 0.3.4 - Fix multiple bugs, re-factor ([@pimentel](https://github.com/pimentel)) 23 | - Fix bug when trying to send current line at the end of the file 24 | - Skip comments when advancing lines with `cmd-enter` 25 | - Skip comments when advancing paragraphs 26 | 27 | ## 0.3.3 - Send knitr block ([@pimentel](https://github.com/pimentel)) 28 | - Allow user to send a RMarkdown block 29 | - When sending to server, if the title does not contain 'RStudio', print a error 30 | - When using `advancePosition`, if sending the current line, advance to the next non-empty line (rather than simply the next line) 31 | 32 | ## 0.3.1 - Send function or paragraph ([@pimentel](https://github.com/pimentel)) 33 | - Allow user to send function or paragraph 34 | - Add configuration allowing notifications 35 | - Add menu items allowing user to change destination application 36 | 37 | ## 0.3.0 - Major re-factoring ([@pimentel](https://github.com/pimentel)) 38 | - Allow user to send code by typing `cmd-enter` 39 | - Allow user to configure which application to send code to 40 | - Add support for Google Chrome and iTerm 41 | - Change behavior of `r-exec:setwd` 42 | - Fix bug when sending code with single quotes 43 | 44 | ## 0.1.0 - First Release ([@hafen](https://github.com/hafen)) 45 | - Simple support for sending R code to be executed in R.app, Terminal, or a web browser running RStudio Server 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r-exec 2 | 3 | Send R code from Atom to be executed in R.app, Terminal, iTerm, or a web browser running RStudio Server on Mac OS X. The current selection is sent or in the case of no selection the current line is sent. 4 | 5 | ## Installation 6 | 7 | `apm install r-exec` 8 | 9 | or 10 | 11 | Search for `r-exec` within package search in the Settings View. 12 | 13 | ## Configuration 14 | 15 | ### Keybindings 16 | 17 | While `cmd-enter` is bound to sending code in the package, it is also annoyingly bound to entering a new line by default in atom. 18 | In order to make it work, you must add the following binding in `~/.atom/keymap.cson`: 19 | 20 | ```javascript 21 | 'atom-workspace atom-text-editor:not([mini])': 22 | 'cmd-enter': 'r-exec:send-command' 23 | ``` 24 | 25 | ### Behavior 26 | 27 | All configuration can be done in the settings panel. Alternatively, you can edit your configuration file as noted below. 28 | 29 | In your global configuration file (`~/.atom/init.coffee`), you may set the following variables: 30 | 31 | - `r-exec.whichApp` which R application to use. Valid applications are: 32 | - `R.app`: the default (the R GUI). 33 | - `RStudio`: the RStudio console. 34 | - `iTerm` or `Terminal`: Assumes the currently active terminal has R running. 35 | - `Safari` or `Google Chrome`: assumes the currently active tab has an active RStudio session running or only one session is open. If the session is not in the active tab, `r-exec` should be able to find it and still send the code. This is helpful if you are viewing plots full screen. 36 | - `r-exec.advancePosition` 37 | - if `true`, go to the next line/paragraph after running the current line/paragraph. 38 | - if `false`, leave the cursor where it currently is 39 | - `r-exec.focusWindow`. 40 | - if `true`, focus the window before sending code. 41 | - if `false`, send the code in the background and stay focused on Atom. This is not possible when sending code to a browser. 42 | - `r-exec.notifications` 43 | - if `true`, notifications via `NotificationManager` when a paragraph or function is not identified. 44 | - `r-exec.smartInsertOperator` 45 | - if `true` when inserting operators, only insert whitespace to the left or right of the operator if there is no existing whitespace. 46 | - `r-exec.skipComments` 47 | - if `true` along with `r-exec.advancePosition`, skip comments after a command is run. 48 | 49 | The default configuration looks like this: 50 | 51 | ```javascript 52 | atom.config.set('r-exec.whichApp', 'R.app') 53 | atom.config.set('r-exec.advancePosition', false) 54 | atom.config.set('r-exec.skipComments', true) 55 | atom.config.set('r-exec.focusWindow', true) 56 | atom.config.set('r-exec.notifications', true) 57 | atom.config.set('r-exec.smartInsertOperator', true) 58 | ``` 59 | 60 | #### Inserting operators 61 | 62 | `r-exec` currently supports inserting the assignment (`<-`) and pipe (`%>%`) operators. 63 | It tries to be smart by looking if there is whitespace to the left or the right of the cursor. 64 | If there is already whitespace it will not insert additional whitespace. 65 | Otherwise, it will insert whitespace. 66 | This can be disabled in the settings tab (`Smart Insert Operator`). 67 | 68 | ### Notes about iTerm 69 | 70 | The iTerm2 Applescript API recently changed as of version 3.0.0. 71 | Older versions of iTerm2 (< 3.0.0) are supported using mode `iTerm`. 72 | Newer versions of iTerm2 (>= 3.0.0) are supported using mode `iTerm2`. 73 | 74 | ## Usage 75 | 76 | ### Sending code 77 | 78 | - `cmd-enter`: send code to configured application (`r-exec:whichApp`). 79 | - `shift-cmd-e`: change to current working directory of current file. 80 | - `shift-cmd-k`: send code between a knitr block (currently only RMarkdown supported). 81 | - `shift-cmd-u`: send function under current cursor. Currently, only functions that begin of the first column in and on the first column of a line are sent. An example: 82 | ```r 83 | myFunction <- function(x) { 84 | # my code goes here 85 | } 86 | ``` 87 | - `shift-cmd-m`: send paragraph under current cursor. A paragraph is a region enclosed by whitespace. 88 | - `shift-alt-p`: send the previous command. 89 | 90 | ### Inserting operators 91 | 92 | - `alt--`: insert the assignment operator ` <- ` 93 | - `shift-alt-m`: insert the pipe operator ` %>% ` 94 | 95 | ## Notes 96 | 97 | It is currently Mac-only because these things are easy to do with AppleScript. Any help on the Windows or Linux side would be great. 98 | 99 | ## TODO 100 | 101 | - Error reporting. 102 | - Support for Windows and Linux. 103 | -------------------------------------------------------------------------------- /lib/r-exec.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Point, Range} = require 'atom' 2 | 3 | String::addSlashes = -> 4 | @replace(/[\\"]/g, "\\$&").replace /\u0000/g, "\\0" 5 | 6 | apps = 7 | chrome: 'Google Chrome' 8 | iterm: 'iTerm' 9 | iterm2: 'iTerm2' 10 | rapp: 'R.app' 11 | rstudio: 'RStudio' 12 | safari: 'Safari' 13 | terminal: 'Terminal' 14 | 15 | module.exports = 16 | config: 17 | whichApp: 18 | type: 'string' 19 | enum: [apps.chrome, apps.iterm, apps.iterm2, apps.rapp, apps.rstudio, 20 | apps.safari, apps.terminal] 21 | default: apps.rapp 22 | description: 'Which application to send code to' 23 | advancePosition: 24 | type: 'boolean' 25 | default: false 26 | description: 'Cursor advances to the next line after ' + 27 | 'sending the current line when there is no selection' 28 | skipComments: 29 | type: 'boolean' 30 | default: true 31 | description: 'When "advancePosition" is true, skip lines that contain ' + 32 | 'only comments' 33 | focusWindow: 34 | type: 'boolean' 35 | default: true 36 | description: 'After code is sent, bring focus to where it was sent' 37 | notifications: 38 | type: 'boolean' 39 | default: true 40 | description: 'Send notifications if there is an error sending code' 41 | smartInsertOperator: 42 | type: 'boolean' 43 | default: true 44 | description: 'Try to be "smart" when inserting operators (see README)' 45 | 46 | subscriptions: null 47 | 48 | previousCommand: '' 49 | 50 | activate: (state) -> 51 | @subscriptions = new CompositeDisposable 52 | 53 | @subscriptions.add atom.commands.add 'atom-workspace', 54 | 'r-exec:send-command', => @sendCommand() 55 | @subscriptions.add atom.commands.add 'atom-workspace', 56 | 'r-exec:send-previous-command', => @sendPreviousCommand() 57 | @subscriptions.add atom.commands.add 'atom-workspace', 58 | 'r-exec:send-paragraph': => @sendParagraph() 59 | @subscriptions.add atom.commands.add 'atom-workspace', 60 | 'r-exec:send-function': => @sendFunction() 61 | @subscriptions.add atom.commands.add 'atom-workspace', 62 | 'r-exec:setwd', => @setWorkingDirectory() 63 | @subscriptions.add atom.commands.add 'atom-workspace', 64 | 'r-exec:send-knitr': => @sendKnitr() 65 | 66 | # # this is for testing 67 | # @subscriptions.add atom.commands.add 'atom-workspace', 68 | # 'r-exec:test', => @getCurrentParagraphRange() 69 | 70 | @subscriptions.add atom.commands.add 'atom-workspace', 71 | 'r-exec:set-chrome', => @setChrome() 72 | @subscriptions.add atom.commands.add 'atom-workspace', 73 | 'r-exec:set-iterm', => @setIterm() 74 | @subscriptions.add atom.commands.add 'atom-workspace', 75 | 'r-exec:set-iterm2', => @setIterm2() 76 | @subscriptions.add atom.commands.add 'atom-workspace', 77 | 'r-exec:set-rapp', => @setRApp() 78 | @subscriptions.add atom.commands.add 'atom-workspace', 79 | 'r-exec:set-rstudio', => @setRStudio() 80 | @subscriptions.add atom.commands.add 'atom-workspace', 81 | 'r-exec:set-safari', => @setSafari() 82 | @subscriptions.add atom.commands.add 'atom-workspace', 83 | 'r-exec:set-terminal', => @setTerminal() 84 | 85 | @subscriptions.add atom.commands.add 'atom-workspace', 86 | 'r-exec:insert-assignment', => @insertAssignment() 87 | 88 | @subscriptions.add atom.commands.add 'atom-workspace', 89 | 'r-exec:insert-pipe', => @insertPipe() 90 | 91 | deactivate: -> 92 | @subscriptions.dispose() 93 | 94 | setChrome: -> 95 | atom.config.set('r-exec.whichApp', apps.chrome) 96 | setIterm: -> 97 | atom.config.set('r-exec.whichApp', apps.iterm) 98 | setIterm2: -> 99 | atom.config.set('r-exec.whichApp', apps.iterm2) 100 | setRApp: -> 101 | atom.config.set('r-exec.whichApp', apps.rapp) 102 | setRStudio: -> 103 | atom.config.set('r-exec.whichApp', apps.rstudio) 104 | setSafari: -> 105 | atom.config.set('r-exec.whichApp', apps.safari) 106 | setTerminal: -> 107 | atom.config.set('r-exec.whichApp', apps.terminal) 108 | 109 | _getEditorAndBuffer: -> 110 | editor = atom.workspace.getActiveTextEditor() 111 | buffer = editor.getBuffer() 112 | return [editor, buffer] 113 | 114 | sendCommand: -> 115 | whichApp = atom.config.get 'r-exec.whichApp' 116 | [editor, buffer] = @_getEditorAndBuffer() 117 | # we store the current position so that we can jump back to it later 118 | # (if the user wants to) 119 | currentPosition = editor.getLastSelection().getScreenRange().end 120 | selection = @getSelection(whichApp) 121 | @sendCode(selection.selection, whichApp) 122 | 123 | advancePosition = atom.config.get 'r-exec.advancePosition' 124 | if advancePosition and not selection.anySelection 125 | nextPosition = @_findForward(@nonEmptyLine, currentPosition.row + 1) 126 | if nextPosition? 127 | nextPosition ?= [currentPosition + 1, 0] 128 | editor.setCursorScreenPosition(nextPosition) 129 | editor.moveToFirstCharacterOfLine() 130 | else 131 | if not selection.anySelection 132 | editor.setCursorScreenPosition(currentPosition) 133 | 134 | sendPreviousCommand: -> 135 | whichApp = atom.config.get 'r-exec.whichApp' 136 | @sendCode(@previousCommand, whichApp) 137 | 138 | sendCode: (code, whichApp) -> 139 | @previousCommand = code 140 | switch whichApp 141 | when apps.iterm then @iterm(code) 142 | when apps.iterm2 then @iterm2(code) 143 | when apps.rapp then @rapp(code) 144 | when apps.rstudio then @rstudio(code) 145 | # when apps.safari then @safari(code) 146 | when apps.chrome, apps.safari then @browser(code, whichApp) 147 | when apps.terminal then @terminal(code) 148 | else console.error 'r-exec.whichApp "' + whichApp + '" is not supported.' 149 | 150 | getFunctionRange: -> 151 | # gets the range of the closest function above the cursor. 152 | # if there is no (proper) function, return false 153 | [editor, buffer] = @_getEditorAndBuffer() 154 | currentPosition = editor.getCursorBufferPosition() 155 | # search for the simple function that looks something like: 156 | # label <- function(...) { 157 | # in case the current function definition is on the current line 158 | currentPosition.row += 1 159 | backwardRange = [0, currentPosition] 160 | funRegex = new 161 | RegExp(/[a-zA-Z]+[a-zA-Z0-9_\.]*[\s]*(<-|=)[\s]*(function)[\s]*\(/g) 162 | foundStart = null 163 | editor.backwardsScanInBufferRange funRegex, backwardRange, (result) -> 164 | if result.range.start.column == 0 165 | foundStart = result.range 166 | result.stop() 167 | 168 | if not foundStart? 169 | console.error "Couldn't find the beginning of the function." 170 | return null 171 | 172 | # now look for the end 173 | numberOfLines = editor.getLineCount() 174 | forwardRange = [foundStart.start, new Point(numberOfLines + 1, 0)] 175 | 176 | foundEnd = null 177 | editor.scanInBufferRange /}/g, forwardRange, (result) -> 178 | if result.range.start.column == 0 179 | foundEnd = result.range 180 | result.stop() 181 | 182 | if not foundEnd? 183 | console.error "Couldn't find the end of the function." 184 | return null 185 | 186 | # check if cursor is contained in range 187 | currentPosition.row -= 1 188 | if foundStart.start.row <= currentPosition.row and 189 | currentPosition.row <= foundEnd.start.row 190 | return new Range(foundStart.start, foundEnd.end) 191 | else 192 | console.error "Couldn't find a function surrounding the current line." 193 | console.error "start: ", foundStart 194 | console.error "end: ", foundEnd 195 | console.error "currentPosition: ", currentPosition 196 | return null 197 | 198 | sendFunction: -> 199 | [editor, buffer] = @_getEditorAndBuffer() 200 | whichApp = atom.config.get 'r-exec.whichApp' 201 | 202 | range = @getFunctionRange() 203 | if range? 204 | code = editor.getTextInBufferRange(range) 205 | code = code.addSlashes() 206 | @sendCode(code, whichApp) 207 | else 208 | @conditionalWarning("Couldn't find function.") 209 | 210 | getSelection: (whichApp) -> 211 | # returns an object with keys: 212 | # selection: the selection or line at which the cursor is present 213 | # anySelection: if true, the user made a selection. 214 | [editor, buffer] = @_getEditorAndBuffer() 215 | 216 | selection = editor.getLastSelection() 217 | anySelection = true 218 | 219 | if selection.getText().addSlashes() == "" 220 | anySelection = false 221 | # editor.selectLinesContainingCursors() 222 | # selection = editor.getLastSelection() 223 | currentPosition = editor.getCursorBufferPosition().row 224 | selection = editor.lineTextForBufferRow(currentPosition) 225 | else 226 | selection = selection.getText() 227 | selection = selection.addSlashes() 228 | 229 | {selection: selection, anySelection: anySelection} 230 | 231 | conditionalWarning: (message) -> 232 | notifications = atom.config.get 'r-exec.notifications' 233 | if notifications 234 | atom.notifications.addWarning(message) 235 | 236 | onlyWhitespace: (str) -> 237 | # returns true if string is only whitespace 238 | return str.replace(/\s/g, '').length is 0 239 | 240 | getCurrentParagraphRange: -> 241 | [editor, buffer] = @_getEditorAndBuffer() 242 | currentPosition = editor.getCursorBufferPosition().row 243 | 244 | currentLine = buffer.lineForRow(currentPosition) 245 | 246 | if @onlyWhitespace(currentLine) 247 | return null 248 | 249 | startIndex = -1 250 | # if we exhaust loop, then this paragraph begins at the first line 251 | if currentPosition > 0 252 | for lineIndex in [(currentPosition - 1)..0] 253 | currentLine = buffer.lineForRow(lineIndex) 254 | if @onlyWhitespace(currentLine) 255 | startIndex = lineIndex 256 | break 257 | startIndex += 1 258 | 259 | endIndex = editor.getLineCount() 260 | numberOfLines = editor.getLineCount() - 1 261 | if currentPosition < endIndex - 1 262 | for lineIndex in [(currentPosition + 1)..numberOfLines] 263 | currentLine = buffer.lineForRow(lineIndex) 264 | if @onlyWhitespace(currentLine) 265 | endIndex = lineIndex 266 | break 267 | endIndex -= 1 268 | 269 | paragraphRange = new Range([startIndex, 0], 270 | [endIndex, buffer.lineLengthForRow(endIndex)]) 271 | 272 | return paragraphRange 273 | 274 | sendParagraph: -> 275 | whichApp = atom.config.get 'r-exec.whichApp' 276 | [editor, buffer] = @_getEditorAndBuffer() 277 | paragraphRange = @getCurrentParagraphRange() 278 | 279 | if paragraphRange 280 | code = editor.getTextInBufferRange(paragraphRange) 281 | code = code.addSlashes() 282 | @sendCode(code, whichApp) 283 | advancePosition = atom.config.get 'r-exec.advancePosition' 284 | if advancePosition 285 | currentPosition = editor.getLastSelection().getScreenRange().end 286 | nextPosition = @_findForward(@nonEmptyLine, paragraphRange.end.row + 1) 287 | if nextPosition? 288 | nextPosition ?= [currentPosition + 1, 0] 289 | editor.setCursorScreenPosition(nextPosition) 290 | editor.moveToFirstCharacterOfLine() 291 | else 292 | console.error 'No paragraph at cursor.' 293 | @conditionalWarning("No paragraph at cursor.") 294 | 295 | isWhitespaceLine: (line) -> 296 | return line.replace(/\s/g, '').length is 0 297 | 298 | nonEmptyLine: (line) -> 299 | skipComments = atom.config.get 'r-exec.skipComments' 300 | ret = true 301 | if skipComments 302 | ret = not /^\s*#/.test(line) 303 | # a non empty line is a line that doesn't contain only a comment 304 | # and at least 1 character 305 | return ret and /\S/.test(line) 306 | 307 | _findBackward: (searchFun, startPosition = null) -> 308 | [editor, buffer] = @_getEditorAndBuffer() 309 | 310 | if not startPosition? 311 | startPosition = editor.getCursorBufferPosition().row 312 | 313 | index = null 314 | for lineIndex in [startPosition..0] 315 | currentLine = buffer.lineForRow(lineIndex) 316 | if searchFun(currentLine) 317 | index = lineIndex 318 | break 319 | 320 | position = null 321 | if index? 322 | position = [index, 0] 323 | 324 | return position 325 | 326 | _findForward: (searchFun, startPosition = null) -> 327 | editor = atom.workspace.getActiveTextEditor() 328 | buffer = editor.getBuffer() 329 | 330 | if not startPosition? 331 | startPosition = editor.getCursorBufferPosition().row 332 | 333 | index = null 334 | numberOfLines = editor.getLineCount() - 1 335 | if startPosition >= numberOfLines 336 | return null 337 | for lineIndex in [startPosition..numberOfLines] 338 | currentLine = buffer.lineForRow(lineIndex) 339 | if searchFun(currentLine) 340 | index = lineIndex 341 | break 342 | 343 | if index? 344 | return [index, buffer.lineLengthForRow(index)] 345 | 346 | return null 347 | 348 | getGeneralRange: (startFun, endFun) -> 349 | # returns a Range with the beginning being the first line of the 350 | # knitr block and the end of the knitr block. 351 | startPosition = @_findBackward(startFun) 352 | endPosition = null 353 | if startPosition? 354 | endPosition = @_findForward(endFun, startPosition[0] + 1) 355 | if startPosition? and endPosition? 356 | return new Range(startPosition, endPosition) 357 | 358 | return null 359 | 360 | rMarkdownStart: (line) -> 361 | return /^\s*([`]{3})\s*([\{]{0,1})([rR])([^\}]*)([\}]{0,1})\s*$/.test(line) 362 | 363 | rMarkdownEnd: (line) -> 364 | return /^\s*([`]{3})\s*$/.test(line) 365 | 366 | sendKnitr: -> 367 | [editor, buffer] = @_getEditorAndBuffer() 368 | codeRange = null 369 | fileName = editor.getPath() 370 | extension = fileName.split('.').pop() 371 | 372 | if /rmd/i.test(extension) 373 | codeRange = @getGeneralRange(@rMarkdownStart, @rMarkdownEnd) 374 | else if /rnw/i.test(extension) 375 | @conditionalWarning("Rnw not implemented yet") 376 | return 377 | else 378 | @conditionalWarning("File not recognized as knitr.") 379 | return 380 | 381 | whichApp = atom.config.get 'r-exec.whichApp' 382 | currentPosition = editor.getCursorBufferPosition().row 383 | 384 | if not codeRange? 385 | msg = "Couldn't find the beginning or end of a knitr block." 386 | atom.notifications.addError(msg) 387 | console.error msg 388 | return 389 | 390 | # ensure that current line is contained in codeRange 391 | if not(codeRange.start.row <= currentPosition and 392 | currentPosition <= codeRange.end.row) 393 | msg = "Couldn't find a knitr block at the current position." 394 | atom.notifications.addError(msg) 395 | console.error msg 396 | return 397 | 398 | codeRange.start.row += 1 399 | codeRange.end.row -= 1 400 | codeRange.end.column = buffer.lineLengthForRow(codeRange.end.row) 401 | 402 | if (codeRange.end.row - codeRange.start.row) < 0 403 | msg = "Empty knitr block." 404 | atom.notifications.addError(msg) 405 | console.error msg 406 | return 407 | 408 | code = editor.getTextInBufferRange(codeRange) 409 | code = code.addSlashes() 410 | @sendCode(code, whichApp) 411 | 412 | advancePosition = atom.config.get 'r-exec.advancePosition' 413 | if advancePosition 414 | nextBlockStart = @_findForward(@rMarkdownStart, codeRange.end.row + 1) 415 | if not nextBlockStart? 416 | nextBlockStart = [editor.getLineCount() - 1, 0] 417 | editor.setCursorScreenPosition(nextBlockStart) 418 | editor.moveToFirstCharacterOfLine() 419 | 420 | return null 421 | 422 | setWorkingDirectory: -> 423 | # set the current working directory to the directory of 424 | # where the current file is 425 | [editor, buffer] = @_getEditorAndBuffer() 426 | whichApp = atom.config.get 'r-exec.whichApp' 427 | # TODO: add warning if connected to server 428 | 429 | cwd = editor.getPath() 430 | if not cwd 431 | console.error 'No current working directory (save the file first).' 432 | @conditionalWarning('No current working directory (save the file first).') 433 | return 434 | cwd = cwd.substring(0, cwd.lastIndexOf('/')) 435 | cwd = "setwd(\"" + cwd + "\")" 436 | 437 | @sendCode(cwd.addSlashes(), whichApp) 438 | 439 | iterm: (selection) -> 440 | # This assumes the active pane item is an console 441 | osascript = require 'node-osascript' 442 | command = [] 443 | focusWindow = atom.config.get 'r-exec.focusWindow' 444 | if focusWindow 445 | command.push 'tell application "iTerm" to activate' 446 | command.push 'tell application "iTerm"' 447 | command.push ' tell the current terminal' 448 | # if focusWindow 449 | # command.push ' activate current session' 450 | command.push ' tell the last session' 451 | command.push ' write text code' 452 | command.push ' end tell' 453 | command.push ' end tell' 454 | command.push 'end tell' 455 | command = command.join('\n') 456 | osascript.execute command, {code: selection}, (error, result, raw) -> 457 | if error 458 | console.error(error) 459 | 460 | iterm2: (selection) -> 461 | # This assumes the active pane item is an console 462 | osascript = require 'node-osascript' 463 | command = [] 464 | focusWindow = atom.config.get 'r-exec.focusWindow' 465 | if focusWindow 466 | command.push 'tell application "iTerm" to activate' 467 | command.push 'tell application "iTerm"' 468 | command.push ' tell the current window' 469 | command.push ' tell current session' 470 | command.push ' write text code' 471 | command.push ' end tell' 472 | command.push ' end tell' 473 | command.push 'end tell' 474 | command = command.join('\n') 475 | console.log command 476 | console.log selection 477 | osascript.execute command, {code: selection}, (error, result, raw) -> 478 | if error 479 | console.error(error) 480 | 481 | rapp: (selection) -> 482 | osascript = require 'node-osascript' 483 | command = [] 484 | focusWindow = atom.config.get 'r-exec.focusWindow' 485 | if focusWindow 486 | command.push 'tell application "R" to activate' 487 | command.push 'tell application "R" to cmd code' 488 | command = command.join('\n') 489 | 490 | osascript.execute command, {code: selection}, 491 | (error, result, raw) -> 492 | if error 493 | console.error error 494 | console.error 'code: ', selection 495 | console.error 'Applescript: ', command 496 | 497 | rstudio: (selection) -> 498 | osascript = require 'node-osascript' 499 | command = [] 500 | focusWindow = atom.config.get 'r-exec.focusWindow' 501 | if focusWindow 502 | command.push 'tell application "RStudio" to activate' 503 | command.push 'tell application "RStudio" to cmd code' 504 | command = command.join('\n') 505 | 506 | osascript.execute command, {code: selection}, 507 | (error, result, raw) -> 508 | if error 509 | console.error error 510 | console.error 'code: ', selection 511 | console.error 'Applescript: ', command 512 | 513 | terminal: (selection) -> 514 | # This assumes the active pane item is an console 515 | osascript = require 'node-osascript' 516 | command = [] 517 | focusWindow = atom.config.get 'r-exec.focusWindow' 518 | if focusWindow 519 | command.push 'tell application "Terminal" to activate' 520 | command.push 'tell application "Terminal"' 521 | command.push 'do script code in window 1' 522 | command.push 'end tell' 523 | command = command.join('\n') 524 | 525 | osascript.execute command, {code: selection}, (error, result, raw) -> 526 | if error 527 | console.error(error) 528 | 529 | getWhichApp: -> 530 | return atom.config.get 'r-exec.whichApp' 531 | 532 | browser: (selection, whichApp) -> 533 | selection = selection.addSlashes() 534 | selection = selection.replace /\n$/, '' 535 | selection = selection.replace /\n/g, '\\\\n' 536 | 537 | focusWindow = atom.config.get 'r-exec.focusWindow' 538 | osascript = require 'node-osascript' 539 | resolve = require('path').resolve 540 | 541 | script = '' 542 | if whichApp == apps.chrome 543 | script = resolve(__dirname, 'applescript', 'chrome-rstudio.applescript') 544 | else if whichApp == apps.safari 545 | script = resolve(__dirname, 'applescript', 'safari-rstudio.applescript') 546 | else 547 | atom.notifications.addError('Invalid whichApp for function browser()') 548 | return 549 | 550 | osascript.executeFile( 551 | script, 552 | {shouldActivate: focusWindow, incoming: selection}, 553 | (err, result, raw) -> 554 | if err 555 | atom.notifications.addError('AppleScript error. Check developer log.') 556 | console.error err 557 | return 558 | if result == 1 559 | # intentionally empty 560 | else if result == 0 561 | atom.notifications.addError('Could not find any RStudio windows.') 562 | else 563 | atom.notifications.addError('More than one RStudio window open.' + 564 | ' Either close all except one or focus the appropriate one.') 565 | ) 566 | 567 | _getSurroundingCharacters: -> 568 | [editor, buffer] = @_getEditorAndBuffer() 569 | # get the character to the left and to the right. 570 | # if there is whitespace to the left do not insert whitespace 571 | currentPosition = editor.getCursorBufferPosition() 572 | leftPosition = Point(currentPosition.row, currentPosition.column - 1) 573 | rightPosition = Point(currentPosition.row, currentPosition.column + 1) 574 | 575 | return [ 576 | editor.getTextInBufferRange(Range(leftPosition, currentPosition)), 577 | editor.getTextInBufferRange(Range(currentPosition, rightPosition)) 578 | ] 579 | 580 | # behavior: 581 | # if there is a whitespace to the left or right, do not insert additional whitespace. 582 | # otherwise, insert whitespace. 583 | # XXX: behavior is a bit weird when text is selected. 584 | # unfortunately, it is unclear how to deal with selections because Atom does not report if there is currently a selection, but only the last selection. 585 | _smartInsert: (text) -> 586 | beSmart = atom.config.get 'r-exec.smartInsertOperator' 587 | 588 | [editor, buffer] = @_getEditorAndBuffer() 589 | # get the character to the left and to the right. 590 | # if there is whitespace to the left do not insert whitespace 591 | [left, right] = @_getSurroundingCharacters() 592 | textInsert = text 593 | 594 | if !beSmart or left != ' ' 595 | textInsert = ' ' + textInsert 596 | 597 | if !beSmart or right != ' ' 598 | textInsert = textInsert + ' ' 599 | 600 | editor.insertText(textInsert) 601 | 602 | insertAssignment: -> 603 | @_smartInsert('<-') 604 | 605 | insertPipe: -> 606 | @_smartInsert('%>%') 607 | --------------------------------------------------------------------------------