├── .travis.yml ├── .gitignore ├── atom-iex.gif ├── CHANGELOG.md ├── spec ├── iex-view-spec.coffee └── iex-spec.coffee ├── package.json ├── styles └── iex.less ├── lib ├── pty.coffee ├── TermView.coffee └── iex.coffee ├── keymaps └── iex.cson ├── LICENSE.md ├── menus └── iex.cson ├── elixir_src └── iex.exs └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /atom-iex.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indiejames/atom-iex/HEAD/atom-iex.gif -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Every feature added 3 | * Every bug fixed 4 | -------------------------------------------------------------------------------- /spec/iex-view-spec.coffee: -------------------------------------------------------------------------------- 1 | IexView = require '../lib/iex-view' 2 | 3 | describe "IexView", -> 4 | it "has one valid test", -> 5 | expect("life").toBe "easy" 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iex", 3 | "main": "./lib/iex", 4 | "version": "0.10.0", 5 | "description": "Run an Elixir IEx (REPL) session in an Atom window.", 6 | "repository": "https://github.com/indiejames/atom-iex", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">=1.0.0" 10 | }, 11 | "dependencies": { 12 | "atom-iex-term.js": "0.0.58", 13 | "fs-plus": "^2.2.3", 14 | "keypather": "^1.3.2", 15 | "pty.js": "^0.3.0", 16 | "atom-space-pen-views": "^2.0.3", 17 | "uuid": "^2.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /styles/iex.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/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .iex { 8 | .terminal { 9 | padding: 0; 10 | 11 | > div { 12 | white-space: pre; 13 | > span { 14 | display: inline-block; 15 | line-height: inherit; 16 | height: inherit; 17 | font-weight: normal !important; 18 | } 19 | } 20 | 21 | .terminal-cursor { 22 | background-color: #fff; 23 | } 24 | 25 | span { 26 | white-space: pre; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/pty.coffee: -------------------------------------------------------------------------------- 1 | # from atom/terminal to reduce cpu usage 2 | pty = require 'pty.js' 3 | 4 | module.exports = (ptyCwd, args) -> 5 | callback = @async() 6 | # if sh 7 | # shell = sh 8 | # else 9 | # if process.platform is 'win32' 10 | # path = require 'path' 11 | # shell = path.resolve(process.env.SystemRoot, 'WindowsPowerShell', 'v1.0', 'powershell.exe') 12 | # else 13 | shell = process.env.SHELL 14 | 15 | cols = 80 16 | rows = 30 17 | 18 | ptyProcess = pty.fork shell, args, 19 | name: 'xterm-256color' 20 | cols: cols 21 | rows: rows 22 | cwd: ptyCwd 23 | env: process.env 24 | 25 | ptyProcess.on 'data', (data) -> emit('iex:data', data) 26 | ptyProcess.on 'exit', -> 27 | emit('iex:exit') 28 | callback() 29 | 30 | process.on 'message', ({event, cols, rows, text}={}) -> 31 | switch event 32 | when 'resize' then ptyProcess.resize(cols, rows) 33 | when 'input' then ptyProcess.write(text) 34 | -------------------------------------------------------------------------------- /keymaps/iex.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 | 'atom-workspace': 12 | 'cmd-alt-l': 'iex:open' 13 | 'cmd-alt-l down': 'iex:open-split-down' 14 | 'cmd-alt-l up': 'iex:open-split-up' 15 | 'cmd-alt-l left': 'iex:open-split-left' 16 | 'cmd-alt-l right': 'iex:open-split-right' 17 | 'cmd-alt-e': 'iex:reset' 18 | 'cmd-alt-a': 'iex:run-all-tests' 19 | 'cmd-alt-p': 'iex:pretty-print' 20 | 21 | 'atom-text-editor': 22 | 'cmd-alt-h': 'iex:help' 23 | 'cmd-alt-o': 'iex:gotoDefinition' 24 | 'cmd-alt-x': 'iex:run-tests' 25 | 'cmd-alt-j': 'iex:run-test' 26 | 'cmd-alt-b': 'iex:pipe' 27 | 'cmd-alt-y': 'iex:say-yes' 28 | 29 | '.iex': 30 | 'cmd-v': 'iex:paste' 31 | 'ctrl-v': 'iex:paste' 32 | 'cmd-c': 'iex:copy' 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 James Norton 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/iex.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'context-menu': 3 | '.iex': [ 4 | {'label': 'Copy', 'command': 'iex:copy'} 5 | {'label': 'Paste', 'command': 'iex:paste'} 6 | ] 7 | 'menu': [ 8 | { 9 | 'label': 'Packages' 10 | 'submenu': [ 11 | 'label': 'iex' 12 | 'submenu': [ 13 | 14 | {'label': 'Open IEx session in New Tab', 'command': 'iex:open'} 15 | {'label': 'Open IEx session in Bottom Pane', 'command': 'iex:open-split-down'} 16 | {'label': 'Open IEx session in Top Pane', 'command': 'iex:open-split-down'} 17 | {'label': 'Open IEx session in Right Pane', 'command': 'iex:open-split-down'} 18 | {'label': 'Open IEx session in Left Pane', 'command': 'iex:open-split-down'} 19 | {'label': 'Jump to definition', 'command': 'iex:gotoDefinition'} 20 | {'label': 'Print help for function', 'command': 'iex:help'} 21 | {'label': 'Run all tests', 'command': 'iex:run-all-tests'} 22 | {'label': 'Run all tests in pane', 'command': 'iex:run-tests'} 23 | {'label': 'Run test', 'command': 'iex:run-test'} 24 | {'label': 'Reset', 'command': 'iex:reset'} 25 | {'label': 'Execute selected in IEx', 'command': 'iex:pipe'} 26 | ] 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /elixir_src/iex.exs: -------------------------------------------------------------------------------- 1 | defmodule AtomIEx do 2 | @moduledoc "Helper functions to support interaction with IEx using the iex 3 | package for the Atom editor" 4 | 5 | @doc "Reset the application" 6 | def reset do 7 | Mix.Task.reenable "compile.elixir" 8 | try do 9 | Application.stop(Mix.Project.config[:app]); 10 | Mix.Task.run "compile.elixir"; 11 | Application.start(Mix.Project.config[:app], :permanent) 12 | catch; 13 | :exit, _ -> "Application failed to start" 14 | end 15 | :ok 16 | end 17 | 18 | @doc "Run all the tests defined in the application" 19 | def run_all_tests do 20 | {rval, _} = System.cmd("mix", ["test", "--color"], []) 21 | IO.puts rval 22 | end 23 | 24 | @doc "Run the currently open test file" 25 | def run_test(file) do 26 | {rval, _} = System.cmd("mix", ["test", "--color", file]) 27 | IO.puts rval 28 | end 29 | 30 | @doc "Run the currently selected test" 31 | def run_test(file, line_num) do 32 | {rval, _} = System.cmd("mix", ["test", "--color", "#{file}:#{line_num}"]) 33 | IO.puts rval 34 | end 35 | 36 | @doc "Get file and line of a module definition" 37 | def get_file_and_line(module) do 38 | file = to_string(module.__info__(:compile)[:source]) 39 | code_docs = Code.get_docs(module, :moduledoc) 40 | line_num = elem(code_docs, 0) 41 | "#{module} - #{file}:#{line_num}" 42 | end 43 | 44 | @doc "Get file and line of function definition" 45 | def get_file_and_line(module, func) do 46 | file = to_string(module.__info__(:compile)[:source]) 47 | code_docs = Code.get_docs(module, :all)[:docs] 48 | #line_num = List.key_find(code_docs, ) 49 | entry = Enum.find(code_docs, fn(x) -> elem(x, 0) |> elem(0) == func end) 50 | line_num = elem(entry, 1) 51 | "#{module}.#{func} - #{file}:#{line_num}" 52 | end 53 | 54 | defmodule Comment do 55 | @moduledoc "Provides a 'comment' macro to allow blocks of code to be ignored 56 | to facilitate running them as small tests in IEx during interactive 57 | development. 58 | 59 | Usage: 60 | ``` 61 | comment do 62 | some code 63 | end 64 | ```" 65 | defmacro comment(_expr) do 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/iex-spec.coffee: -------------------------------------------------------------------------------- 1 | Iex = require '../lib/iex' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "Iex", -> 9 | [workspaceElement, activationPromise] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('iex') 14 | 15 | describe "when the iex:toggle event is triggered", -> 16 | it "hides and shows the modal panel", -> 17 | # Before the activation event the view is not on the DOM, and no panel 18 | # has been created 19 | expect(workspaceElement.querySelector('.iex')).not.toExist() 20 | 21 | # This is an activation event, triggering it will cause the package to be 22 | # activated. 23 | atom.commands.dispatch workspaceElement, 'iex:toggle' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(workspaceElement.querySelector('.iex')).toExist() 30 | 31 | iexElement = workspaceElement.querySelector('.iex') 32 | expect(iexElement).toExist() 33 | 34 | iexPanel = atom.workspace.panelForItem(iexElement) 35 | expect(iexPanel.isVisible()).toBe true 36 | atom.commands.dispatch workspaceElement, 'iex:toggle' 37 | expect(iexPanel.isVisible()).toBe false 38 | 39 | it "hides and shows the view", -> 40 | # This test shows you an integration test testing at the view level. 41 | 42 | # Attaching the workspaceElement to the DOM is required to allow the 43 | # `toBeVisible()` matchers to work. Anything testing visibility or focus 44 | # requires that the workspaceElement is on the DOM. Tests that attach the 45 | # workspaceElement to the DOM are generally slower than those off DOM. 46 | jasmine.attachToDOM(workspaceElement) 47 | 48 | expect(workspaceElement.querySelector('.iex')).not.toExist() 49 | 50 | # This is an activation event, triggering it causes the package to be 51 | # activated. 52 | atom.commands.dispatch workspaceElement, 'iex:toggle' 53 | 54 | waitsForPromise -> 55 | activationPromise 56 | 57 | runs -> 58 | # Now we can test for view visibility 59 | iexElement = workspaceElement.querySelector('.iex') 60 | expect(iexElement).toBeVisible() 61 | atom.commands.dispatch workspaceElement, 'iex:toggle' 62 | expect(iexElement).not.toBeVisible() 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iex package 2 | 3 | This package provides Elixir developers with the ability to run an Elixir IEx 4 | (REPL) session in an Atom window. It has only been tested on OS X and is 5 | unlikely to work properly (or at all) on other platforms. 6 | 7 | ![iex Screenshot](https://github.com/indiejames/atom-iex/raw/master/atom-iex.gif) 8 | 9 | 10 | ### Installation 11 | 12 | ``` 13 | apm install iex 14 | ``` 15 | 16 | It is _highly recommended_ that you add the key bindings below. These can be 17 | customized as desired. They are not set by default to avoid conflicts with 18 | other packages. 19 | 20 | ### Features 21 | 22 | Aside from typing directly in the IEx session, the plugin provides actions 23 | to improve workflow: 24 | 25 | * Reset the project, restarting the application and compiling any files that 26 | have changed since the last restart 27 | * Run all tests in the project 28 | * Run all tests in the currently open editor 29 | * Run the test in the open editor in which the cursor resides 30 | * Execute the currently selected text 31 | 32 | These actions depend on `mix`, so they only work for `mix` generated projects 33 | and require a `mix.exs` file at the top level. 34 | 35 | ### Key Bindings 36 | 37 | Customizing Key Bindings: 38 | 39 | ```cson 40 | 'atom-workspace': 41 | 'cmd-alt-l': 'iex:open' 42 | 'cmd-alt-l down': 'iex:open-split-down' 43 | 'cmd-alt-l up': 'iex:open-split-up' 44 | 'cmd-alt-l left': 'iex:open-split-left' 45 | 'cmd-alt-l right': 'iex:open-split-right' 46 | 'cmd-alt-e': 'iex:reset' 47 | 'cmd-alt-a': 'iex:run-all-tests' 48 | 'cmd-alt-p': 'iex:pretty-print' 49 | 50 | 'atom-text-editor': 51 | 'cmd-alt-h': 'iex:help' 52 | 'cmd-alt-o': 'iex:gotoDefinition' 53 | 'cmd-alt-x': 'iex:run-tests' 54 | 'cmd-alt-j': 'iex:run-test' 55 | 'cmd-alt-b': 'iex:pipe' 56 | ``` 57 | 58 | Adding these will provide the following: 59 | 60 | #### Key Bindings and Events 61 | 62 | | key binding | event | action | 63 | | ----------- | ----- | ------ | 64 | | `cmd + alt + l` | `iex:open` | Opens new IEx in new tab pane | 65 | | `cmd + alt + l down` | `iex:open-split-up` | Opens new IEx tab pane in up split | 66 | | `cmd + alt + l right` | `iex:open-split-right` | Opens new IEx tab pane in right split | 67 | | `cmd + alt + l down` | `iex:open-split-down` | Opens new IEx tab pane in down split | 68 | | `cmd + alt + l left` | `iex:open-split-left` | Opens new IEx tab pane in left split | 69 | | `cmd + alt + e` | `iex:reset` | Stops the application, compiles any changed files with mix, then restarts the application. | 70 | | `cmd + alt + a` | `iex:run-all-tests` | Run all the test in the project | 71 | | `cmd + alt + x` | `iex:run-tests` | Run all the tests in the active editor | 72 | | `cmd + alt + j` | `iex:run-test` | Run the test in which the cursor lies | 73 | | `cmd + alt + h` | `iex:help` | Print the docstring for the function or module under the cursor | 74 | | `cmd + alt + o` | `iex:gotoDefinition`| Jump to the definition of the function or module under the cursor | 75 | | `cmd + alt + b` | `iex:pipe` | Pipe the currently selected text to the REPL and execute it | 76 | | `cmd + alt + p` | `iex:pretty-print` | Pretty print the last evaluated expression | 77 | 78 | ### Fonts 79 | The REPL defaults to using the same font family/size as Atom. Independent settings for the REPL will be available in the next release. 80 | 81 | ### Contributions 82 | 83 | This package is originally based on the [Term2 Atom package](https://atom.io/packages/term2) with heavy modifications. Feel free to submit bugs or issue pull requests. 84 | -------------------------------------------------------------------------------- /lib/TermView.coffee: -------------------------------------------------------------------------------- 1 | util = require 'util' 2 | path = require 'path' 3 | os = require 'os' 4 | fs = require 'fs-plus' 5 | uuid = require 'uuid' 6 | 7 | Terminal = require 'atom-iex-term.js' 8 | 9 | keypather = do require 'keypather' 10 | 11 | {Task, CompositeDisposable} = require 'atom' 12 | {$, View, ScrollView} = require 'atom-space-pen-views' 13 | 14 | uuids = [] 15 | 16 | last = (str)-> str[str.length-1] 17 | 18 | generateUUID = ()-> 19 | new_id = uuid.v1().substring(0,4) 20 | while new_id in uuids 21 | new_id = uuid.v1().substring(0,4) 22 | uuids.push new_id 23 | new_id 24 | 25 | getMixFilePath = ()-> 26 | mixPath = null 27 | for projectPath in atom.project.getPaths() 28 | do (projectPath) -> 29 | if projectPath && fs.existsSync(path.join(projectPath, 'mix.exs')) 30 | mixPath = path.join(projectPath, 'mix.exs') 31 | return 32 | mixPath 33 | 34 | renderTemplate = (template, data)-> 35 | vars = Object.keys data 36 | vars.reduce (_template, key)-> 37 | _template.split(///\{\{\s*#{key}\s*\}\}///) 38 | .join data[key] 39 | , template.toString() 40 | 41 | class TermView extends View 42 | 43 | tabindex: -1 44 | 45 | @content: -> 46 | @div class: 'iex', click: 'click' 47 | 48 | constructor: (@opts={})-> 49 | @opts.shell = process.env.SHELL or 'bash' 50 | @opts.shellArguments or= '' 51 | 52 | editorPath = keypather.get atom, 'workspace.getEditorViews[0].getEditor().getPath()' 53 | @opts.cwd = @opts.cwd or atom.project.getPaths()[0] or editorPath or process.env.HOME 54 | super 55 | 56 | applyStyle: -> 57 | # remove background color in favor of the atom background 58 | @term.element.style.background = null 59 | @term.element.style.fontFamily = ( 60 | @opts.fontFamily or 61 | atom.config.get('editor.fontFamily') or 62 | # (Atom doesn't return a default value if there is none) 63 | # so we use a poor fallback 64 | "monospace" 65 | ) 66 | # Atom returns a default for fontSize 67 | @term.element.style.fontSize = ( 68 | @opts.fontSize or 69 | atom.config.get('editor.fontSize') 70 | ) + "px" 71 | 72 | forkPtyProcess: (args=[])-> 73 | processPath = require.resolve './pty' 74 | projectPath = atom.project.getPaths()[0] ? '~' 75 | Task.once processPath, fs.absolute(projectPath), args 76 | # TODO - try switching back to pty.js to see if it fixes the backspace issue 77 | 78 | initialize: (@state)-> 79 | @shell_stdout_history = [] 80 | iexSrcPath = atom.packages.resolvePackagePath("iex") + "/elixir_src/iex.exs" 81 | {cols, rows} = @getDimensions() 82 | {cwd, shell, shellArguments, runCommand, colors, cursorBlink, scrollback} = @opts 83 | new_id = generateUUID() 84 | iexPath = atom.config.get('iex.iexExecutablePath') 85 | args = ["-l", "-c", iexPath + " --sname IEX-" + new_id + " -r " + iexSrcPath] 86 | mixPath = getMixFilePath() 87 | # assume mix file is at top level 88 | if mixPath 89 | file_str = fs.readFileSync(mixPath, {"encoding": "utf-8"}) 90 | phoenix_str = "" 91 | if atom.config.get('iex.startPhoenixServer') && file_str.match(/applications.*:phoenix/g) 92 | phoenix_str = " phoenix.server" 93 | args = ["-l", "-c", iexPath + " --sname IEX-" + new_id + " -r " + iexSrcPath + " -S mix" + phoenix_str] 94 | 95 | @term = term = new Terminal { 96 | useStyle: no 97 | screenKeys: no 98 | colors: colorsArray 99 | cursorBlink, scrollback, cols, rows 100 | } 101 | 102 | @ptyProcess = @forkPtyProcess args 103 | # capture output from the child process (shell) 104 | @ptyProcess.on 'iex:data', (data) => 105 | @shell_stdout_history.push data 106 | if @shell_stdout_history.length > 10 107 | @shell_stdout_history = @shell_stdout_history.slice(-10) 108 | @term.write data 109 | @ptyProcess.on 'iex:exit', (data) => @destroy() 110 | 111 | colorsArray = (colorCode for colorName, colorCode of colors) 112 | 113 | term.end = => @destroy() 114 | 115 | term.on "copy", (text)=> @copy(text) 116 | 117 | term.on "data", (data)=> @input data 118 | term.open this.get(0) 119 | 120 | @input "#{runCommand}#{os.EOL}" if runCommand 121 | term.focus() 122 | @applyStyle() 123 | @attachEvents() 124 | @resizeToPane() 125 | 126 | 127 | focus: -> 128 | @resizeToPane() 129 | @focusTerm() 130 | #super 131 | 132 | focusTerm: -> 133 | @term.element.focus() 134 | @term.focus() 135 | 136 | onActivePaneItemChanged: (activeItem) => 137 | if (activeItem && activeItem.items.length == 1 && activeItem.items[0] == this) 138 | @focus() 139 | 140 | input: (data) -> 141 | @ptyProcess.send event: 'input', text: data 142 | 143 | resize: (cols, rows) -> 144 | try 145 | @ptyProcess.send {event: 'resize', rows, cols} 146 | catch error 147 | console.log error 148 | 149 | titleVars: -> 150 | bashName: last @opts.shell.split '/' 151 | hostName: os.hostname() 152 | platform: process.platform 153 | home : process.env.HOME 154 | 155 | getTitle: -> 156 | @vars = @titleVars() 157 | titleTemplate = @opts.titleTemplate or "({{ bashName }})" 158 | renderTemplate titleTemplate, @vars 159 | 160 | getIconName: -> 161 | "terminal" 162 | 163 | attachEvents: -> 164 | @resizeToPane = @resizeToPane.bind this 165 | @attachResizeEvents() 166 | # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 167 | @subscriptions = new CompositeDisposable 168 | 169 | # Register commands 170 | @subscriptions.add atom.commands.add '.iex', 'iex:paste': => @paste() 171 | @subscriptions.add atom.commands.add '.iex', 'iex:copy': => @copy() 172 | @subscriptions.add atom.workspace.onDidChangeActivePane(@onActivePaneItemChanged) 173 | #atom.workspace.onDidChangeActivePaneItem (item)=> @onActivePaneItemChanged(item) 174 | 175 | click: (evt, element) -> 176 | @focus() 177 | 178 | paste: -> 179 | try 180 | @input atom.clipboard.read() 181 | catch error 182 | 183 | copy: -> 184 | if @term._selected # term.js visual mode selections 185 | textarea = @term.getCopyTextarea() 186 | text = @term.grabText( 187 | @term._selected.x1, @term._selected.x2, 188 | @term._selected.y1, @term._selected.y2) 189 | else # fallback to DOM-based selections 190 | text = @term.context.getSelection().toString() 191 | rawText = @term.context.getSelection().toString() 192 | rawLines = rawText.split(/\r?\n/g) 193 | lines = rawLines.map (line) -> 194 | line.replace(/\s/g, " ").trimRight() 195 | text = lines.join("\n") 196 | atom.clipboard.write text 197 | 198 | attachResizeEvents: -> 199 | setTimeout (=> @resizeToPane()), 10 200 | @on 'focus', @focus 201 | $(window).on 'resize', => @resizeToPane() 202 | 203 | detachResizeEvents: -> 204 | @off 'focus', @focus 205 | $(window).off 'resize' 206 | 207 | resizeToPane: -> 208 | {cols, rows} = @getDimensions() 209 | return unless cols > 0 and rows > 0 210 | return unless @term 211 | return if @term.rows is rows and @term.cols is cols 212 | 213 | @resize cols, rows 214 | @term.resize cols, rows 215 | atom.views.getView(atom.workspace).style.overflow = 'visible' 216 | 217 | getDimensions: -> 218 | fakeRow = $("
 
").css visibility: 'hidden' 219 | if @term 220 | @find('.terminal').append fakeRow 221 | fakeCol = fakeRow.children().first() 222 | cols = Math.floor (@width() / fakeCol.width()) or 9 223 | rows = Math.floor (@height() / fakeCol.height()) or 16 224 | fakeCol.remove() 225 | else 226 | cols = Math.floor @width() / 7 227 | rows = Math.floor @height() / 14 228 | 229 | cols = cols - 2 230 | {cols, rows} 231 | 232 | activate: -> 233 | @focus 234 | 235 | deactivate: -> 236 | @subscriptions.dispose() 237 | 238 | destroy: -> 239 | console.log "Destroying TermView" 240 | @input "\nSystem.halt\n\n" 241 | console.log "System halted" 242 | # this is cheesy and a race condition, but apparently I need a delay 243 | # before continuing so the IEx system can halt 244 | # FIXME - race condition 245 | count = 10000000 246 | while count -= 1 247 | "" 248 | 249 | @detachResizeEvents() 250 | 251 | @ptyProcess.send("exit") 252 | @ptyProcess.terminate() 253 | @term.destroy() 254 | parentPane = atom.workspace.getActivePane() 255 | if parentPane.activeItem is this 256 | parentPane.removeItem parentPane.activeItem 257 | @detach() 258 | 259 | module.exports = TermView 260 | -------------------------------------------------------------------------------- /lib/iex.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Point, Task} = require 'atom' 2 | path = require 'path' 3 | keypather = do require 'keypather' 4 | TermView = require './TermView' 5 | os = require 'os' 6 | spawn = require('child_process').spawn 7 | 8 | {SHELL, HOME}=process.env 9 | 10 | capitalize = (str)-> str[0].toUpperCase() + str[1..].toLowerCase() 11 | 12 | paneChanged = (pane)-> console.log("Pane changed") 13 | 14 | module.exports = Iex = 15 | subscriptions: null 16 | termViews: [] 17 | focusedTerminal: off 18 | config: 19 | iexExecutablePath: 20 | type: 'string' 21 | default: 'iex' 22 | scrollback: 23 | type: 'integer' 24 | default: 1000 25 | startPhoenixServer: 26 | type: 'boolean' 27 | default: no 28 | cursorBlink: 29 | type: 'boolean' 30 | default: yes 31 | openPanesInSameSplit: 32 | type: 'boolean' 33 | default: no 34 | colors: 35 | type: 'object' 36 | properties: 37 | normalBlack : 38 | type: 'string' 39 | default: '#2e3436' 40 | normalRed : 41 | type: 'string' 42 | default: '#cc0000' 43 | normalGreen : 44 | type: 'string' 45 | default: '#4e9a06' 46 | normalYellow: 47 | type: 'string' 48 | default: '#c4a000' 49 | normalBlue : 50 | type: 'string' 51 | default: '#3465a4' 52 | normalPurple: 53 | type: 'string' 54 | default: '#75507b' 55 | normalCyan : 56 | type: 'string' 57 | default: '#06989a' 58 | normalWhite : 59 | type: 'string' 60 | default: '#d3d7cf' 61 | brightBlack : 62 | type: 'string' 63 | default: '#555753' 64 | brightRed : 65 | type: 'string' 66 | default: '#ef2929' 67 | brightGreen : 68 | type: 'string' 69 | default: '#8ae234' 70 | brightYellow: 71 | type: 'string' 72 | default: '#fce94f' 73 | brightBlue : 74 | type: 'string' 75 | default: '#729fcf' 76 | brightPurple: 77 | type: 'string' 78 | default: '#ad7fa8' 79 | brightCyan : 80 | type: 'string' 81 | default: '#34e2e2' 82 | brightWhite : 83 | type: 'string' 84 | default: '#eeeeec' 85 | 86 | activate: (state) -> 87 | # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 88 | @subscriptions = new CompositeDisposable 89 | 90 | # Register commands 91 | ['up', 'right', 'down', 'left'].forEach (direction)=> 92 | @subscriptions.add atom.commands.add 'atom-workspace',"iex:open-split-#{direction}", @splitTerm.bind(this, direction) 93 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:open': => @newIEx() 94 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:pipe': => @pipeIEx() 95 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:help': => @printHelp() 96 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:run-all-tests': => @runAllTests() 97 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:run-tests': => @runTests() 98 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:run-test': => @runTest() 99 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:reset': => @resetIEx() 100 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:pretty-print': => @prettyPrint() 101 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:goto-definition': => @gotoDefinition() 102 | @subscriptions.add atom.commands.add 'atom-workspace', 'iex:say-yes': => @sayYes() 103 | @subscriptions.add atom.workspace.onDidChangeActivePane(paneChanged) 104 | 105 | deactivate: -> 106 | @termViews.forEach (view)-> view.deactivate() 107 | @subscriptions.dispose() 108 | 109 | getColors: -> 110 | { 111 | normalBlack, normalRed, normalGreen, normalYellow 112 | normalBlue, normalPurple, normalCyan, normalWhite 113 | brightBlack, brightRed, brightGreen, brightYellow 114 | brightBlue, brightPurple, brightCyan, brightWhite 115 | } = atom.config.get('iex.colors') 116 | [ 117 | normalBlack, normalRed, normalGreen, normalYellow 118 | normalBlue, normalPurple, normalCyan, normalWhite 119 | brightBlack, brightRed, brightGreen, brightYellow 120 | brightBlue, brightPurple, brightCyan, brightWhite 121 | ] 122 | 123 | createTermView:-> 124 | opts = 125 | runCommand : null 126 | shellArguments: null 127 | titleTemplate : 'IEx' 128 | cursorBlink : atom.config.get('iex.cursorBlink') 129 | fontFamily : atom.config.get 'iex.fontFamily' 130 | fontSize : atom.config.get 'iex.fontSize' 131 | colors : @getColors() 132 | 133 | termView = new TermView opts 134 | termView.on 'remove', @handleRemoveTerm.bind this 135 | termView.on "click", => @focusedTerminal = termView 136 | @focusedTerminal = termView 137 | 138 | @termViews.push? termView 139 | termView 140 | 141 | runCommand: (cmd) -> 142 | if @focusedTerminal 143 | if Array.isArray @focusedTerminal 144 | [pane, item] = @focusedTerminal 145 | pane.activateItem item 146 | else 147 | item = @focusedTerminal 148 | item.term.send(cmd) 149 | item.term.focus() 150 | 151 | readTerminalText: -> 152 | if @focusedTerminal 153 | if Array.isArray @focusedTerminal 154 | [pane, item] = @focusedTerminal 155 | pane.activateItem item 156 | else 157 | item = @focusedTerminal 158 | text = item.shell_stdout_history.join " " 159 | console.log text 160 | text 161 | 162 | resetIEx: -> 163 | text = 'AtomIEx.reset\n' 164 | @runCommand(text) 165 | 166 | runAllTests: -> 167 | text = "AtomIEx.run_all_tests\n" 168 | @runCommand(text) 169 | 170 | runTests: -> 171 | editor = atom.workspace.getActiveTextEditor() 172 | if editor 173 | path = editor.getBuffer().file.path 174 | text = "AtomIEx.run_test(\"" 175 | text = text.concat(path).concat("\")\n") 176 | @runCommand(text) 177 | 178 | runTest: -> 179 | editor = atom.workspace.getActiveTextEditor() 180 | if editor 181 | path = editor.getBuffer().file.path 182 | line_num = editor.getCursorBufferPosition().toArray()[0] + 1 183 | text = "AtomIEx.run_test(\"" 184 | text = text.concat(path).concat("\",").concat(line_num).concat(")\n") 185 | @runCommand(text) 186 | 187 | getSelectedSymbol: -> 188 | editor = atom.workspace.getActiveTextEditor() 189 | if editor 190 | cursorPosition = editor.getCursorBufferPosition() 191 | [row, col] = cursorPosition.toArray() 192 | # begRegex = new RegExp("[\\n\\(,\\s]") 193 | begRegex = /(\n|\s|,|\(|^.)/ 194 | endRegex = /.*?[\(,\s\.$]/ 195 | endRange = [[row, col + 1], [row, col + 10000]] 196 | begRange = [[row, 0], [row, col]] 197 | tailIndex = -1 198 | headIndex = -1 199 | 200 | editor.scanInBufferRange(endRegex, endRange, 201 | (match, matchText, range, stop, replace) -> 202 | tailIndex = match.match.index + match.match[0].length - 1 203 | ) 204 | 205 | editor.backwardsScanInBufferRange(begRegex, begRange, 206 | (match, matchText, range, stop, replace) => 207 | headIndex = match.match.index 208 | ) 209 | 210 | editor.getText().substring(headIndex, tailIndex) 211 | 212 | gotoDefinition: -> 213 | text = @getSelectedSymbol() 214 | editorPath = keypather.get atom, 'workspace.getEditorViews[0].getEditor().getPath()' 215 | cwd = atom.project.getPaths()[0] or editorPath 216 | 217 | moduleFuncRegex = /^(.*?)\.(.*?)$/ 218 | moduleMatch = moduleFuncRegex.exec text 219 | if moduleMatch 220 | module = moduleMatch[1] 221 | func = moduleMatch[2] 222 | cmd = "AtomIEx.get_file_and_line(" + module.trim() + ", :" + func + ")\n" 223 | else 224 | cmd = "AtomIEx.get_file_and_line(" + text.trim() + ")\n" 225 | 226 | env = process.env 227 | env.path = env.path + "" 228 | done = false 229 | file = null 230 | lineNum = null 231 | fileLineRegex = /".*? - (.*?):(.*)"/ 232 | iexSrcPath = atom.packages.resolvePackagePath("iex") + "/elixir_src/iex.exs" 233 | iexp = spawn('iex', ['-r', iexSrcPath, '-S', 'mix'], {cwd: cwd}) 234 | outCount = 0 235 | iexp.stdout.on 'data', (data) => 236 | console.log('stdout: ' + data) 237 | outCount += 1 238 | if outCount == 3 239 | iexp.stdin.write cmd 240 | if outCount == 4 241 | match = fileLineRegex.exec data 242 | if match 243 | console.log("MATCH") 244 | console.log(match[1]) 245 | file = match[1] 246 | lineNum = parseInt(match[2], 10) - 1 247 | # workaround for apparent Atom bug in opening file to line 1 248 | if lineNum == 0 249 | lineNum = 1 250 | console.log("LINENUM...") 251 | console.log(lineNum) 252 | options = {} 253 | options.initialLine = lineNum 254 | iexp.kill('SIGKILL') 255 | atom.workspace.open(file, options) 256 | else 257 | console.log("NO MATCH") 258 | console.log(data) 259 | 260 | iexp.stderr.on 'data', (data) => 261 | console.log('stderr: ' + data); 262 | iexp.on 'close', (code) => 263 | console.log('child process exited with code ' + code); 264 | 265 | printHelp: -> 266 | text = @getSelectedSymbol() 267 | @runCommand("h " + text + "\n") 268 | 269 | prettyPrint: -> 270 | @runCommand("IO.puts(v(-1))\n") 271 | 272 | sayYes: -> 273 | @runCommand("Y\n") 274 | 275 | splitTerm: (direction)-> 276 | openPanesInSameSplit = atom.config.get 'iex.openPanesInSameSplit' 277 | termView = @createTermView() 278 | termView.on "click", => @focusedTerminal = termView 279 | direction = capitalize direction 280 | 281 | splitter = => 282 | pane = activePane["split#{direction}"] items: [termView] 283 | activePane.termSplits[direction] = pane 284 | @focusedTerminal = [pane, pane.items[0]] 285 | 286 | activePane = atom.workspace.getActivePane() 287 | activePane.termSplits or= {} 288 | if openPanesInSameSplit 289 | if activePane.termSplits[direction] and activePane.termSplits[direction].items.length > 0 290 | pane = activePane.termSplits[direction] 291 | item = pane.addItem termView 292 | pane.activateItem item 293 | @focusedTerminal = [pane, item] 294 | else 295 | splitter() 296 | else 297 | splitter() 298 | 299 | newIEx: -> 300 | termView = @createTermView() 301 | pane = atom.workspace.getActivePane() 302 | item = pane.addItem termView 303 | pane.activateItem item 304 | 305 | pipeIEx: -> 306 | editor = atom.workspace.getActiveTextEditor() 307 | if editor 308 | action = 'selection' 309 | stream = switch action 310 | when 'path' 311 | editor.getBuffer().file.path 312 | when 'selection' 313 | editor.getSelectedText() 314 | 315 | if stream and @focusedTerminal 316 | if Array.isArray @focusedTerminal 317 | [pane, item] = @focusedTerminal 318 | pane.activateItem item 319 | else 320 | item = @focusedTerminal 321 | 322 | text = stream.trim().concat("\n") 323 | item.term.send(text) 324 | item.term.focus() 325 | 326 | handleRemoveTerm: (termView)-> 327 | @termViews.splice @termViews.indexOf(termView), 1 328 | --------------------------------------------------------------------------------