├── .gitignore ├── docs ├── static │ ├── main_modulename.png │ └── scratch_modulename.png ├── README.md ├── communication.md └── workflow.md ├── CHANGELOG.md ├── lib ├── frontend.coffee ├── ui │ ├── selector.coffee │ ├── notifications.coffee │ └── console.coffee ├── completions.coffee ├── connection │ ├── terminal.coffee │ ├── tcp.coffee │ ├── process.coffee │ ├── spawnInterruptibleJulia.ps1 │ └── client.coffee ├── eval.coffee ├── utils.coffee ├── julia-client.coffee └── modules.coffee ├── README.md ├── menus └── julia-client.cson ├── styles ├── julia-client.less └── spinner.css ├── LICENSE.md ├── package.json ├── manual └── README.md ├── keymaps └── julia-client.cson └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /docs/static/main_modulename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpsanders/atom-julia-client/master/docs/static/main_modulename.png -------------------------------------------------------------------------------- /docs/static/scratch_modulename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpsanders/atom-julia-client/master/docs/static/scratch_modulename.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | * Improved scrolling for inline results 3 | * Inter-file links in errors and `methods` output 4 | * Interrupts also working on Windows 5 | 6 | ## 0.1.0 - First Release 7 | * Every feature added 8 | * Every bug fixed 9 | -------------------------------------------------------------------------------- /lib/frontend.coffee: -------------------------------------------------------------------------------- 1 | client = require './connection/client' 2 | selector = require './ui/selector' 3 | 4 | module.exports = 5 | activate: -> 6 | client.handle 'select', ({items}, resolve) => 7 | selector.show items, (item) => 8 | resolve item: item 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Julia Client 2 | 3 | Julia client provides interactive IDE-like features for the Atom editor. You can spin up 4 | a Julia process to run in the background, evaluate code inline, get live autocompletion 5 | results, work with the inline console, and more. 6 | 7 | For set up instructions, please see [the manual](manual/). This software is very much in 8 | alpha stage so expect to get your hands a little dirty. 9 | 10 | [Chat](https://gitter.im/JunoLab/Juno) for informal discussion. 11 | -------------------------------------------------------------------------------- /menus/julia-client.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/hacking-atom-package-word-count#menus for more details 2 | # 'context-menu': 3 | # 'atom-text-editor': [ 4 | # { 5 | # 'label': 'Toggle julia-client' 6 | # 'command': 'julia-client:toggle' 7 | # } 8 | # ] 9 | # 'menu': [ 10 | # { 11 | # 'label': 'Packages' 12 | # 'submenu': [ 13 | # 'label': 'julia-client' 14 | # 'submenu': [ 15 | # { 16 | # 'label': 'Toggle' 17 | # 'command': 'julia-client:toggle' 18 | # } 19 | # ] 20 | # ] 21 | # } 22 | # ] 23 | -------------------------------------------------------------------------------- /lib/ui/selector.coffee: -------------------------------------------------------------------------------- 1 | {SelectListView} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | show: (xs, f) -> 5 | @selector ?= new SelectListView 6 | @selector.setItems [] 7 | @selector.storeFocusedElement() 8 | @selector.viewForItem = (item) => 9 | "
  • #{item}
  • " 10 | 11 | if xs.constructor == Promise 12 | @selector.setLoading "Loading..." 13 | xs.then (xs) => 14 | @selector.setItems xs 15 | else 16 | @selector.setItems xs 17 | 18 | panel = atom.workspace.addModalPanel(item: @selector) 19 | @selector.focusFilterEditor() 20 | 21 | confirmed = false 22 | @selector.confirmed = (item) => 23 | f(item) 24 | confirmed = true 25 | @selector.cancel() 26 | @selector.cancelled = => 27 | panel.destroy() 28 | f() unless confirmed 29 | -------------------------------------------------------------------------------- /styles/julia-client.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 | .julia-client { 8 | .fade { 9 | color: @text-color-subtle; 10 | } 11 | } 12 | 13 | .julia { 14 | table.methods { 15 | td { 16 | padding-left: 5px; 17 | } 18 | } 19 | &.error, .error { 20 | color: @text-color-error; 21 | .fade { 22 | color: fade(@text-color-error, 75%); 23 | } 24 | a { 25 | color: @text-color-error; 26 | } 27 | } 28 | } 29 | 30 | atom-text-editor::shadow { 31 | div.julia-client.loading { 32 | position: absolute; 33 | right: 10px; 34 | bottom: 10px; 35 | z-index: 1; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/ui/notifications.coffee: -------------------------------------------------------------------------------- 1 | remote = require('remote') 2 | client = require '../connection/client' 3 | 4 | module.exports = 5 | notes: [] 6 | window: remote.getCurrentWindow() 7 | 8 | activate: -> 9 | document.addEventListener 'focusin', => 10 | @clear() 11 | @msgHandlers() 12 | 13 | enabled: () -> atom.config.get('julia-client.notifications') 14 | 15 | show: (msg) -> 16 | return unless @enabled() and not document.hasFocus() 17 | n = new Notification "Julia Client", 18 | body: msg 19 | icon: @iconPath 20 | n.onclick = => 21 | @window.focus() 22 | @notes.push(n) 23 | 24 | clear: -> 25 | for note in @notes 26 | note.close() 27 | @notes = [] 28 | 29 | msgHandlers: -> 30 | client.handle 'error', (options) => 31 | atom.notifications.addError options.msg, options 32 | -------------------------------------------------------------------------------- /lib/completions.coffee: -------------------------------------------------------------------------------- 1 | client = require './connection/client' 2 | run = require './eval' 3 | 4 | module.exports = 5 | selector: '.source.julia' 6 | inclusionPriority: 0 7 | filterSuggestions: true 8 | excludeLowerPriority: false 9 | 10 | completionsData: (ed, pos) -> 11 | module: ed.juliaModule 12 | cursor: run.cursor pos 13 | code: ed.getText() 14 | path: ed.getPath() 15 | 16 | getCompletions: (ed, pos, f) -> 17 | client.msg 'completions', @completionsData(ed, pos), (data) -> 18 | f data 19 | 20 | toCompletion: (c) -> 21 | if c.constructor == String 22 | text: c 23 | else 24 | c 25 | 26 | getSuggestions: ({editor, bufferPosition}) -> 27 | return [] unless client.isConnected() 28 | new Promise (resolve) => 29 | @getCompletions editor, bufferPosition, (completions) => 30 | resolve completions?.map(@toCompletion) or [] 31 | -------------------------------------------------------------------------------- /lib/connection/terminal.coffee: -------------------------------------------------------------------------------- 1 | child_process = require 'child_process' 2 | client = require './client' 3 | 4 | module.exports = 5 | 6 | escpath: (p) -> '"' + p + '"' 7 | escape: (sh) -> sh.replace(/"/g, '\\"') 8 | 9 | exec: (sh) -> 10 | child_process.exec sh, (err, stdout, stderr) -> 11 | if err? 12 | console.log err 13 | 14 | term: (sh) -> 15 | switch process.platform 16 | when "darwin" 17 | @exec "osascript -e 'tell application \"Terminal\" to activate'" 18 | @exec "osascript -e 'tell application \"Terminal\" to do script \"#{@escape(sh)}\"'" 19 | else 20 | @exec "#{@terminal()} \"#{@escape(sh)}\"" 21 | 22 | terminal: -> atom.config.get("julia-client.terminal") 23 | 24 | jlpath: () -> atom.config.get("julia-client.juliaPath") 25 | jlargs: () -> atom.config.get("julia-client.juliaArguments") 26 | 27 | repl: -> @term "#{@escpath @jlpath()} #{@jlargs()}" 28 | 29 | client: (port) -> 30 | client.booting() 31 | @term "#{@escpath @jlpath()} #{@jlargs()} -P \"import Atom; Atom.connect(#{port})\"" 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Mike Innes 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 | -------------------------------------------------------------------------------- /lib/connection/tcp.coffee: -------------------------------------------------------------------------------- 1 | net = require 'net' 2 | client = require './client' 3 | 4 | module.exports = 5 | server: null 6 | port: null 7 | sock: null 8 | 9 | buffer: (f) -> 10 | buffer = [''] 11 | (data) -> 12 | str = data.toString() 13 | lines = str.split '\n' 14 | buffer[0] += lines.shift() 15 | buffer.push lines... 16 | while buffer.length > 1 17 | f buffer.shift() 18 | 19 | listen: (f) -> 20 | return f?(@port) if @port? 21 | client.isConnected = => @sock? 22 | client.output = (data) => @sock.write JSON.stringify data 23 | @server = net.createServer (c) => 24 | if @sock then return c.end() 25 | @sock = c 26 | client.connected() 27 | c.on 'end', => 28 | @sock = null 29 | client.disconnected() 30 | c.on 'error', (e) => 31 | console.error 'Julia Client: TCP connection error:' 32 | console.error e 33 | @sock = null 34 | client.disconnected() 35 | c.on 'data', @buffer (s) => 36 | client.input JSON.parse s 37 | 38 | @server.listen 0, => 39 | @port = @server.address().port 40 | f?(@port) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "julia-client", 3 | "main": "./lib/julia-client", 4 | "version": "0.1.2", 5 | "description": "Julia Evaluation", 6 | "keywords": [], 7 | "activationCommands": { 8 | "atom-text-editor": [ 9 | "julia-client:evaluate", 10 | "julia-client:evaluate-all" 11 | ], 12 | "atom-workspace": [ 13 | "julia-client:open-a-repl", 14 | "julia-client:start-repl-client", 15 | "julia-client:start-julia", 16 | "julia-client:toggle-console", 17 | "julia:open-startup-file", 18 | "julia:open-julia-home", 19 | "julia:open-package-in-new-window" 20 | ] 21 | }, 22 | "repository": "https://github.com/JunoLab/atom-julia-client", 23 | "license": "MIT", 24 | "engines": { 25 | "atom": ">=1.0.0 <2.0.0" 26 | }, 27 | "dependencies": { 28 | "atom-space-pen-views": "^2.0.0" 29 | }, 30 | "consumedServices": { 31 | "status-bar": { 32 | "versions": { 33 | "^1.0.0": "consumeStatusBar" 34 | } 35 | }, 36 | "ink": { 37 | "versions": { 38 | "*": "consumeInk" 39 | } 40 | } 41 | }, 42 | "providedServices": { 43 | "autocomplete.provider": { 44 | "versions": { 45 | "2.0.0": "completions" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ui/console.coffee: -------------------------------------------------------------------------------- 1 | # TODO: modules, history 2 | 3 | client = require '../connection/client' 4 | notifications = require '../ui/notifications' 5 | 6 | module.exports = 7 | activate: -> 8 | @create() 9 | 10 | @cmd = atom.commands.add 'atom-workspace', 11 | "julia-client:clear-console": => 12 | @c.reset() 13 | 14 | client.handle 'info', ({msg}) => 15 | @c.info msg 16 | 17 | client.handle 'result', ({result}) => 18 | view = if result.type then result.view else result 19 | view = @ink.tree.fromJson view 20 | @ink.links.linkify view[0] 21 | error = result.type == 'error' 22 | @ink.tree.toggle view unless error 23 | @c.result view, 24 | error: error 25 | 26 | deactivate: -> 27 | @cmd.dispose() 28 | 29 | create: -> 30 | @c = new @ink.Console 31 | @c.setGrammar atom.grammars.grammarForScopeName('source.julia') 32 | @c.view[0].classList.add 'julia' 33 | @c.view.getTitle = -> "Julia" 34 | @c.modes = => @replModes 35 | @c.onEval (ed) => @eval ed 36 | @c.input() 37 | @loading.onWorking => @c.view.loading true 38 | @loading.onDone => @c.view.loading false 39 | 40 | toggle: -> @c.toggle() 41 | 42 | eval: (ed) -> 43 | if ed.getText() 44 | client.start() 45 | @c.done() 46 | client.msg 'eval-repl', {code: ed.getText(), mode: ed.inkConsoleMode?.name}, (result) => 47 | @c.input() 48 | notifications.show "Evaluation Finished" 49 | 50 | replModes: 51 | ';': 52 | name: 'shell' 53 | icon: 'terminal' 54 | grammar: atom.grammars.grammarForScopeName('source.shell') 55 | '?': 56 | name: 'help' 57 | icon: 'question' 58 | -------------------------------------------------------------------------------- /manual/README.md: -------------------------------------------------------------------------------- 1 | # Julia Client Manual 2 | 3 | This is the user manual for the atom-julia-client plugin and its dependencies. It's a little 4 | sparse at the moment – reflecting the fact that the plugin isn't ready for all users – but 5 | basic setup is included. If you'd like to get involved you can check out the [dev 6 | setup](../docs) instead. 7 | 8 | ## Installation 9 | 10 | In order to get Julia-Client working you need to install its dependencies in both Julia and 11 | Atom. To start with, you'll need to work with Julia v0.4, if you're not already. You can 12 | [build Julia from source](https://github.com/JuliaLang/julia) or [download the nightly 13 | build](http://julialang.org/downloads/) – the download is easier to get started with, 14 | particularly on Windows. From Julia, install the `Atom` package. You currently also need to 15 | run `Pkg.checkout("JuliaParser")` in order to avoid errors from the latest release. 16 | 17 | Next, go to the Atom settings pane and install the packages `language-julia`, `ink` and 18 | `julia-client`. If you open the command palette and type `Julia` you should see that 19 | Julia-related commands are now available. The last step is to make sure Atom can find Julia – 20 | if the `julia` command is not on your path, you need to go into the julia-client settings 21 | and set the path to the Julia binary, which is `[wherever you installed 22 | Julia]/bin/julia` (or `[same]\bin\julia.exe` on Windows). 23 | 24 | Finally, run the `Julia Client: Toggle Console` command from Atom. When the console opens, 25 | type a Julia expression (e.g. `2+2`) and press `Enter` to evaluate it. This may take a while 26 | the first time as precompilation runs, but booting Julia next time around will be much 27 | quicker. 28 | -------------------------------------------------------------------------------- /keymaps/julia-client.cson: -------------------------------------------------------------------------------- 1 | # TODO: don't apply all these commands to the console 2 | 3 | '.platform-darwin .item-views > atom-text-editor[data-grammar="source julia"]:not([mini])': 4 | 'cmd-enter': 'julia-client:evaluate' 5 | 'cmd-shift-enter': 'julia-client:evaluate-all' 6 | 'cmd-j cmd-m': 'julia-client:set-working-module' 7 | 'cmd-shift-j cmd-shift-m': 'julia-client:reset-working-module' 8 | 'ctrl-`': 'julia-client:toggle-console' 9 | 10 | '.platform-darwin .console': 11 | 'ctrl-c': 'julia-client:interrupt-julia' 12 | 'ctrl-`': 'julia-client:toggle-console' 13 | 14 | '.platform-darwin atom-workspace': 15 | 'cmd-j cmd-r': 'julia-client:open-a-repl' 16 | 'cmd-shift-j cmd-shift-r': 'julia-client:start-repl-client' 17 | 'cmd-j cmd-s': 'julia-client:start-julia' 18 | 'cmd-j cmd-c': 'julia-client:clear-console' 19 | 'cmd-j cmd-l': 'julia-client:reset-loading-indicator' 20 | 'cmd-j cmd-k': 'julia-client:kill-julia' 21 | 22 | '.platform-win32 .item-views > atom-text-editor[data-grammar="source julia"]:not([mini]), 23 | .platform-linux .item-views > atom-text-editor[data-grammar="source julia"]:not([mini])': 24 | 'ctrl-enter': 'julia-client:evaluate' 25 | 'ctrl-shift-enter': 'julia-client:evaluate-all' 26 | 'ctrl-j ctrl-m': 'julia-client:set-working-module' 27 | 'ctrl-alt-j ctrl-alt-m': 'julia-client:reset-working-module' 28 | 'ctrl-`': 'julia-client:toggle-console' 29 | 30 | '.platform-win32 .console, 31 | .platform-linux .console': 32 | 'ctrl-shift-c': 'julia-client:interrupt-julia' 33 | 'ctrl-`': 'julia-client:toggle-console' 34 | 35 | '.platform-win32 atom-workspace, 36 | .platform-linux atom-workspace': 37 | 'ctrl-j ctrl-r': 'julia-client:open-a-repl' 38 | 'ctrl-alt-j ctrl-alt-r': 'julia-client:start-repl-client' 39 | 'ctrl-j ctrl-s': 'julia-client:start-julia' 40 | 'ctrl-j ctrl-c': 'julia-client:clear-console' 41 | 'ctrl-j ctrl-c': 'julia-client:reset-loading-indicator' 42 | 'ctrl-j ctrl-k': 'julia-client:kill-julia' 43 | -------------------------------------------------------------------------------- /lib/connection/process.coffee: -------------------------------------------------------------------------------- 1 | process = require 'child_process' 2 | client = require './client' 3 | net = require 'net' 4 | 5 | module.exports = 6 | jlpath: () -> atom.config.get("julia-client.juliaPath") 7 | # TODO: this is very naïve. 8 | jlargs: () -> atom.config.get("julia-client.juliaArguments").split ' ' 9 | 10 | start: (port, cons) -> 11 | return if @proc? 12 | client.booting() 13 | @spawnJulia(port) 14 | @onStart() 15 | @proc.on 'exit', (code, signal) => 16 | cons.c.err "Julia has stopped: #{code}, #{signal}" 17 | cons.c.input() unless cons.c.isInput 18 | @onStop() 19 | @proc = null 20 | client.cancelBoot() 21 | @proc.stdout.on 'data', (data) => 22 | text = data.toString() 23 | if text then cons.c.out text 24 | @proc.stderr.on 'data', (data) => 25 | text = data.toString() 26 | if text then cons.c.err text 27 | 28 | onStart: -> 29 | @cmds = atom.commands.add 'atom-workspace', 30 | 'julia-client:kill-julia': => @killJulia() 31 | 'julia-client:interrupt-julia': => @interruptJulia() 32 | 33 | onStop: -> 34 | @cmds?.dispose() 35 | 36 | spawnJulia: (port) -> 37 | switch process.platform 38 | when 'win32' 39 | @proc = process.spawn("powershell", ["-ExecutionPolicy", "bypass", "& \"#{__dirname}\\spawnInterruptibleJulia.ps1\" -port #{port} -jlpath \"#{@jlpath()}\" -jloptions \"#{@jlargs().join(' ')}\""]) 40 | else 41 | @proc = process.spawn(@jlpath(), [@jlargs()..., '-e', "import Atom; @sync Atom.connect(#{port})"]) 42 | 43 | interruptJulia: -> 44 | switch process.platform 45 | when 'win32' 46 | @sendSignalToWrapper('SIGINT') 47 | else 48 | @proc.kill('SIGINT') 49 | 50 | killJulia: -> 51 | switch process.platform 52 | when 'win32' 53 | @proc.kill() 54 | else 55 | @sendSignalToWrapper('KILL') 56 | 57 | sendSignalToWrapper: (signal) -> 58 | wrapper = net.connect(port: 26992) 59 | wrapper.setNoDelay() 60 | wrapper.write(signal) 61 | wrapper.end() 62 | -------------------------------------------------------------------------------- /lib/connection/spawnInterruptibleJulia.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Int32] $port, 3 | [string] $jlpath, 4 | [string] $jloptions 5 | ) 6 | 7 | # start Julia 8 | $proc = Start-Process $jlpath "$jloptions -e `"import Atom; @sync Atom.connect($port)`"" -NoNewWindow -PassThru 9 | 10 | # import GenerateConsoleCtrlEvent: 11 | $MethodDefinition = @' 12 | [DllImport("Kernel32.dll", CharSet = CharSet.Unicode)] 13 | public static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); 14 | '@ 15 | 16 | $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -Namespace 'Win32' -PassThru 17 | 18 | function Receive-TCPMessage { 19 | param ( [ValidateNotNullOrEmpty()] 20 | [int] $Port 21 | ) 22 | try { 23 | $endpoint = new-object System.Net.IPEndPoint([ipaddress]::any, $port) 24 | $listener = new-object System.Net.Sockets.TcpListener $endpoint 25 | $listener.start() 26 | 27 | $data = $listener.AcceptTcpClient() # will block here until connection 28 | $bytes = New-Object System.Byte[] 6 29 | $stream = $data.GetStream() 30 | 31 | while (($i = $stream.Read($bytes,0,$bytes.Length)) -ne 0){ 32 | $EncodedText = New-Object System.Text.ASCIIEncoding 33 | $data = $EncodedText.GetString($bytes,0, $i) 34 | Write-Output $data 35 | } 36 | 37 | $stream.close() 38 | $listener.stop() 39 | } 40 | catch [exception]{ 41 | echo "julia-client: Internal Error:" 42 | echo "$exception" 43 | } 44 | } 45 | 46 | # the port should probably be determined dynamically (by nodejs): 47 | while ($true){ 48 | $msg = Receive-TCPMessage -Port 26992 # wait for interrupts 49 | if ($msg -match "SIGINT"){ 50 | $status = $Kernel32::GenerateConsoleCtrlEvent(0, $proc.Id) 51 | # this is necessary for GenerateConsoleCtrlEvent to actually do something: 52 | echo "Interrupting Julia..." 53 | if (!$status) { 54 | echo "julia-client: Internal Error: Interrupting Julia failed." 55 | } 56 | } 57 | if ($msg -match "KILL"){ 58 | $proc.Kill() 59 | Exit 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/eval.coffee: -------------------------------------------------------------------------------- 1 | client = require './connection/client' 2 | notifications = require './ui/notifications' 3 | 4 | module.exports = 5 | cursor: ({row, column}) -> 6 | row: row+1 7 | column: column+1 8 | 9 | evalData: (editor, selection) -> 10 | start = selection.getHeadBufferPosition() 11 | end = selection.getTailBufferPosition() 12 | if not start.isLessThan end then [start, end] = [end, start] 13 | 14 | code: editor.getText() 15 | module: editor.juliaModule 16 | path: editor.getPath() || 'untitled-' + editor.getBuffer().inkId 17 | start: @cursor start 18 | end: @cursor end 19 | 20 | # TODO: get block bounds as a seperate step 21 | # TODO: implement block finding in Atom 22 | eval: -> 23 | editor = atom.workspace.getActiveTextEditor() 24 | for sel in editor.getSelections() 25 | client.msg 'eval', @evalData(editor, sel), ({start, end, result}) => 26 | view = if result.type then result.view else result 27 | view = @ink.tree.fromJson(view)[0] 28 | @ink.links.linkify view 29 | r = @ink?.results.showForLines editor, start-1, end-1, 30 | content: view 31 | error: result.type == 'error' 32 | clas: 'julia' 33 | if result.type == 'error' and result.highlights 34 | @showError r, result.highlights 35 | notifications.show "Evaluation Finished" 36 | 37 | evalAll: -> 38 | editor = atom.workspace.getActiveTextEditor() 39 | client.msg 'eval-all', { 40 | path: editor.getPath() 41 | module: editor.juliaModule 42 | code: editor.getText() 43 | }, 44 | (result) => 45 | notifications.show "Evaluation Finished" 46 | 47 | showError: (r, lines) -> 48 | @errorLines?.lights.destroy() 49 | lights = @ink.highlights.errorLines (file: file, line: line-1 for {file, line} in lines) 50 | @errorLines = {r, lights} 51 | 52 | destroyResult = r.destroy.bind r 53 | r.destroy = => 54 | if @errorLines?.r == r 55 | @errorLines.lights.destroy() 56 | destroyResult() 57 | -------------------------------------------------------------------------------- /lib/utils.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | shell = require 'shell' 3 | fs = require 'fs' 4 | client = require './connection/client' 5 | selector = require './ui/selector' 6 | 7 | module.exports = 8 | homedir: -> 9 | if process.platform == 'win32' 10 | process.env.USERPROFILE 11 | else 12 | process.env.HOME 13 | 14 | home: (p...) -> path.join @homedir(), p... 15 | 16 | juliaHome: -> process.env.JULIA_HOME or @home '.julia' 17 | 18 | pkgDir: (f) -> 19 | fs.readdir @juliaHome(), (err, data) => 20 | dir = data?.filter((path)=>path.startsWith('v')).sort().pop() 21 | dir? and f path.join(@juliaHome(), dir) 22 | 23 | packages: (f) -> 24 | @pkgDir (dir) => 25 | fs.readdir dir, (err, data) => 26 | ps = data?.filter((path)=>!path.startsWith('.') and 27 | ["METADATA","REQUIRE","META_BRANCH"].indexOf(path) < 0) 28 | ps? and f(ps, dir) 29 | 30 | openPackage: -> 31 | dir = '' 32 | ps = new Promise (resolve) => 33 | @packages (ps, d) => 34 | dir = d 35 | resolve ps 36 | selector.show ps, (pkg) => 37 | return unless pkg? 38 | atom.open pathsToOpen: [path.join dir, pkg] 39 | 40 | cdHere: -> 41 | file = atom.workspace.getActiveTextEditor()?.getPath() 42 | file? or atom.notifications.addError 'This file has no path.' 43 | client.msg 'cd', {path: path.dirname(file)} 44 | 45 | cdProject: -> 46 | dirs = atom.project.getPaths() 47 | if dirs.length < 1 48 | atom.notifications.addError 'This project has no folders.' 49 | else if dirs.length == 1 50 | client.msg 'cd', {path: dirs[0]} 51 | else 52 | selector.show dirs, (dir) => 53 | return unless dir? 54 | client.msg 'cd', {path: dir} 55 | 56 | cdHome: -> 57 | client.msg 'cd', {path: @home()} 58 | 59 | commands: (subs) -> 60 | subs.add atom.commands.add 'atom-workspace', 61 | 'julia:open-startup-file': => atom.workspace.open @home '.juliarc.jl' 62 | 'julia:open-julia-home': => shell.openItem @juliaHome() 63 | 'julia:open-package-in-new-window': => @openPackage() 64 | 65 | subs.add atom.commands.add 'atom-text-editor', 66 | 'julia-client:work-in-file-folder': => 67 | client.require => @cdHere() 68 | 'julia-client:work-in-project-folder': => 69 | client.require => @cdProject() 70 | 'julia-client:work-in-home-folder': => 71 | client.require => @cdHome() 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Atom-Julia 2 | 3 | Julia support in Atom is in a very, very early state – we love to have people kicking the 4 | tires, but be ready to get your hands dirty! 5 | 6 | ## Help & Bug Reporting 7 | 8 | This project is composed of many sub-projects, and it can be hard to know the appropriate 9 | place to file issues. For that reason we prefer that non-developers report issues on the 10 | [Juno discussion forum](http://discuss.junolab.org/) under the "issue" category, and we can 11 | funnel things through to the right places from there. 12 | 13 | ## Contributing 14 | 15 | If you have feature ideas you'd like to implement, or bugs you'd like to fix, feel free to 16 | open a [discussion](http://discuss.junolab.org/) under the "dev" category – we're always happy 17 | to help people flesh out their ideas or get unstuck on problems. 18 | 19 | If you look over the GitHub issues for the various packages, you may notice some labelled 20 | [up for 21 | grabs](https://github.com/JunoLab/atom-julia-client/issues?q=is%3Aopen+is%3Aissue+label%3A%22up+for+grabs%22). 22 | These are features or bugs for which the implementation or fix is reasonably straightforward – 23 | they might take a few hours of effort or more, but they won't involve enormous expert-level 24 | challenges. As above, feel free to open up a discussion on these and we'll help you get 25 | going. 26 | 27 | Please read the [developer set up guidelines](docs/) to get started. There's a basic 28 | development tutorial there, but as you get deeper you'll want some more general resources: 29 | 30 | * [Julia Documentation](http://docs.julialang.org/en/latest/) – for learning about the Julia 31 | language. 32 | * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript) – by far the best reference 33 | on the JavaScript language and browser window APIs. 34 | * [CoffeeScript](http://coffeescript.org/) – Atom and its packages use this to make working 35 | with JS a little more convenient. 36 | * [The Atom Docs](https://atom.io/docs) – the Atom Flight Manual is a very readable introduction 37 | to Atom's internals, and the API docs are a useful reference. 38 | * [julia-client developer docs](docs/) – These docs go into more detail about the internals 39 | of this project and the development workflow. 40 | 41 | Most open source projects, including ours, use [git](http://git-scm.org) to help work 42 | together. There are plenty of git tutorials around, and the various GUI clients (e.g. GitHub 43 | for Windows/Mac, SourceTree) are very helpful for learning the ropes. 44 | -------------------------------------------------------------------------------- /lib/connection/client.coffee: -------------------------------------------------------------------------------- 1 | {Emitter} = require 'atom' 2 | 3 | module.exports = 4 | 5 | # Messaging 6 | 7 | handlers: {} 8 | callbacks: {} 9 | queue: [] 10 | id: 0 11 | 12 | input: ([type, data]) -> 13 | if @handlers.hasOwnProperty type 14 | if data.callback? 15 | @handlers[type] data, (result) => 16 | @msg data.callback, result 17 | else 18 | @handlers[type] data 19 | else if @callbacks.hasOwnProperty type 20 | try 21 | @callbacks[type] data 22 | finally 23 | delete @callbacks[type] 24 | @loading.done() 25 | else 26 | console.log "julia-client: unrecognised message #{type}" 27 | console.log data 28 | 29 | output: (data) -> 30 | 31 | msg: (type, data, f) -> 32 | if f? 33 | data.callback = @id = @id+1 34 | @callbacks[@id] = f 35 | @loading.working() 36 | if @isConnected() 37 | @output [type, data] 38 | else 39 | @queue.push [type, data] 40 | 41 | handle: (type, f) -> 42 | @handlers[type] = f 43 | 44 | # Connecting & Booting 45 | 46 | emitter: new Emitter() 47 | 48 | onConnected: (cb) -> @emitter.on('connected', cb) 49 | onDisconnected: (cb) -> @emitter.on('disconnected', cb) 50 | 51 | isConnected: -> false 52 | 53 | connected: -> 54 | @emitter.emit 'connected' 55 | if @isBooting 56 | @isBooting = false 57 | @loading.done() 58 | @output msg for msg in @queue 59 | @queue = [] 60 | 61 | disconnected: -> 62 | @emitter.emit 'disconnected' 63 | @reset() 64 | 65 | booting: -> 66 | @isBooting = true 67 | @loading.working() 68 | 69 | cancelBoot: -> 70 | if @isBooting 71 | @isBooting = false 72 | @reset() 73 | 74 | reset: -> 75 | @cancelBoot() 76 | @loading.reset() 77 | @queue = [] 78 | @callbacks = {} 79 | 80 | # Management & UI 81 | 82 | connectedError: -> 83 | if @isConnected() 84 | atom.notifications.addError "Can't create a new client.", 85 | detail: "There is already a Julia client running." 86 | true 87 | else 88 | false 89 | 90 | notConnectedError: -> 91 | if not @isConnected() 92 | atom.notifications.addError "Can't do that without a Julia client.", 93 | detail: "Try connecting a client by evaluating something." 94 | true 95 | else 96 | false 97 | 98 | require: (f) -> @notConnectedError() or f() 99 | disrequire: (f) -> @connectedError() or f() 100 | 101 | start: -> 102 | if not @isConnected() and not @isBooting 103 | atom.commands.dispatch atom.views.getView(atom.workspace), 104 | 'julia-client:start-julia' 105 | -------------------------------------------------------------------------------- /styles/spinner.css: -------------------------------------------------------------------------------- 1 | /* Thanks to https://github.com/tobiasahlin/SpinKit/ */ 2 | 3 | /* 4 | * Usage: 5 | * 6 | *
    7 | *
    8 | *
    9 | *
    10 | *
    11 | *
    12 | * 13 | */ 14 | .sk-folding-cube { 15 | margin: 0; 16 | width: 20px; 17 | height: 20px; 18 | position: relative; 19 | -webkit-transform: rotateZ(45deg); 20 | transform: rotateZ(45deg); } 21 | .sk-folding-cube .sk-cube { 22 | float: left; 23 | width: 50%; 24 | height: 50%; 25 | position: relative; 26 | -webkit-transform: scale(1.1); 27 | -ms-transform: scale(1.1); 28 | transform: scale(1.1); } 29 | .sk-folding-cube .sk-cube:before { 30 | content: ''; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 100%; 36 | background-color: #fff; 37 | -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both; 38 | animation: sk-foldCubeAngle 2.4s infinite linear both; 39 | -webkit-transform-origin: 100% 100%; 40 | -ms-transform-origin: 100% 100%; 41 | transform-origin: 100% 100%; } 42 | .sk-folding-cube .sk-cube2 { 43 | -webkit-transform: scale(1.1) rotateZ(90deg); 44 | transform: scale(1.1) rotateZ(90deg); } 45 | .sk-folding-cube .sk-cube3 { 46 | -webkit-transform: scale(1.1) rotateZ(180deg); 47 | transform: scale(1.1) rotateZ(180deg); } 48 | .sk-folding-cube .sk-cube4 { 49 | -webkit-transform: scale(1.1) rotateZ(270deg); 50 | transform: scale(1.1) rotateZ(270deg); } 51 | .sk-folding-cube .sk-cube2:before { 52 | -webkit-animation-delay: 0.3s; 53 | animation-delay: 0.3s; } 54 | .sk-folding-cube .sk-cube3:before { 55 | -webkit-animation-delay: 0.6s; 56 | animation-delay: 0.6s; } 57 | .sk-folding-cube .sk-cube4:before { 58 | -webkit-animation-delay: 0.9s; 59 | animation-delay: 0.9s; } 60 | 61 | @-webkit-keyframes sk-foldCubeAngle { 62 | 0%, 10% { 63 | -webkit-transform: perspective(140px) rotateX(-180deg); 64 | transform: perspective(140px) rotateX(-180deg); 65 | opacity: 0; } 66 | 25%, 75% { 67 | -webkit-transform: perspective(140px) rotateX(0deg); 68 | transform: perspective(140px) rotateX(0deg); 69 | opacity: 1; } 70 | 90%, 100% { 71 | -webkit-transform: perspective(140px) rotateY(180deg); 72 | transform: perspective(140px) rotateY(180deg); 73 | opacity: 0; } } 74 | 75 | @keyframes sk-foldCubeAngle { 76 | 0%, 10% { 77 | -webkit-transform: perspective(140px) rotateX(-180deg); 78 | transform: perspective(140px) rotateX(-180deg); 79 | opacity: 0; } 80 | 25%, 75% { 81 | -webkit-transform: perspective(140px) rotateX(0deg); 82 | transform: perspective(140px) rotateX(0deg); 83 | opacity: 1; } 84 | 90%, 100% { 85 | -webkit-transform: perspective(140px) rotateY(180deg); 86 | transform: perspective(140px) rotateY(180deg); 87 | opacity: 0; } } 88 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Contents 2 | 3 | * Basics 4 | * [Developer Install](#developer-install) – get going with Atom and Julia package 5 | development. 6 | * [Communication](communication.md) – Details how Atom and Julia both communicate with 7 | each other. 8 | 9 | These docs are evolving as we figure out the best way to get people involved. If it's hard 10 | to get things working, or anything seems out of date, broken, or just plain confusing, 11 | please do let us know so we can fix it. Even better, file a PR! 12 | 13 | # Developer Install 14 | 15 | Firstly, you need to be working in Julia v0.4 for Atom. Currently, you can either build Julia 16 | from source or download a [nightly build](http://julialang.org/downloads/). 17 | 18 | Julia support in Atom consists of a number of packages for both Julia and Atom: 19 | 20 | * [language-julia](https://github.com/JuliaLang/atom-language-julia) – Provides basic 21 | language support for Atom, e.g. syntax highlighting. 22 | * [ink](https://github.com/JunoLab/atom-ink) – Provides generic UI components for building 23 | IDEs in Atom (e.g. the console lives here). 24 | * [CodeTools.jl](http://github.com/JunoLab/CodeTools.jl) – provides backend editor support 25 | for Julia, e.g. autocompletion and evaluation. 26 | * [Atom.jl](http://github.com/JunoLab/Atom.jl) and 27 | [julia-client](http://github.com/JunoLab/atom-julia-client) – these packages tie everything 28 | together. julia-client boots Julia from inside Atom, then communicates with the Atom.jl 29 | package to provide e.g. autocompletion and evaluation within the editor. 30 | 31 | You can install *language-julia* by using Atom's `Install Packages And Themes` command and 32 | searching for it. The Julia packages, *Atom.jl* and *CodeTools.jl*, can be installed via 33 | 34 | ```julia 35 | Pkg.clone("http://github.com/JunoLab/Atom.jl") 36 | Pkg.clone("http://github.com/JunoLab/CodeTools.jl") 37 | ``` 38 | 39 | If you already have these packages change `clone` to `checkout` here. 40 | 41 | To install the Atom packages, start by uninstalling them completely if you have them 42 | already, then run the following commands in a folder of your choice (it's conventional, 43 | though not required, to do this in `~/github`): 44 | 45 | ```shell 46 | git clone http://github.com/JunoLab/atom-ink ink 47 | cd ink 48 | apm install 49 | apm link . 50 | cd .. 51 | 52 | git clone http://github.com/JunoLab/atom-julia-client julia-client 53 | cd julia-client 54 | apm install 55 | apm link . 56 | cd .. 57 | ``` 58 | 59 | It's a good idea to keep these up to date by running `Pkg.update()` in Julia and syncing the 60 | package repos every now and then. 61 | 62 | Atom will need to be reloaded, either by closing and reopening it or by running the `Window: 63 | Reload` command. At this point, you should find that there are a bunch of new Julia commands 64 | available in Atom – type "Julia" into the command palette to see what's available. If the 65 | `julia` command isn't on your path already, set the Julia path in the julia-client settings 66 | panel. 67 | 68 | Get started by going into a buffer set to Julia syntax, typing `2+2`, and pressing 69 | `C-Enter` (where `C` stands for `Ctrl`, or `Cmd` on OS X). After the client boots you 70 | should see the result pop up next to the text. You can also work in the Atom REPL by pressing 71 | `` Ctrl-` `` – just type in the input box and `Shift-Enter` to evaluate. 72 | 73 | On OS X you may an error because the Julia command cannot be found, even though it's on your 74 | path. To solve this you can either set the full path in the package settings, or start Atom 75 | from the terminal with the `atom` command. 76 | -------------------------------------------------------------------------------- /lib/julia-client.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | http = require 'http' 3 | terminal = require './connection/terminal' 4 | client = require './connection/client' 5 | process = require './connection/process' 6 | tcp = require './connection/tcp' 7 | modules = require './modules' 8 | evaluation = require './eval' 9 | notifications = require './ui/notifications' 10 | utils = require './utils' 11 | completions = require './completions' 12 | frontend = require './frontend' 13 | cons = require './ui/console' 14 | 15 | defaultTerminal = 16 | switch process.platform 17 | when 'darwin' 18 | 'Terminal.app' 19 | when 'win32' 20 | 'cmd /C start cmd /C' 21 | else 22 | 'x-terminal-emulator -e' 23 | 24 | module.exports = JuliaClient = 25 | config: 26 | juliaPath: 27 | type: 'string' 28 | default: 'julia' 29 | description: 'The location of the Julia binary' 30 | juliaArguments: 31 | type: 'string' 32 | default: '-q' 33 | description: 'Command-line arguments to pass to Julia' 34 | notifications: 35 | type: 'boolean' 36 | default: true 37 | description: 'Enable notifications for evaluation' 38 | terminal: 39 | type: 'string' 40 | default: defaultTerminal 41 | description: 'Command used to open a terminal. (Windows/Linux only)' 42 | 43 | activate: (state) -> 44 | @subscriptions = new CompositeDisposable 45 | @commands @subscriptions 46 | modules.activate() 47 | notifications.activate() 48 | frontend.activate() 49 | client.onConnected => 50 | notifications.show("Client Connected") 51 | @withInk => 52 | cons.activate() 53 | 54 | try 55 | if id = localStorage.getItem 'metrics.userId' 56 | http.get "http://mikeinn.es/hit?id=#{id}&app=atom-julia" 57 | 58 | deactivate: -> 59 | @subscriptions.dispose() 60 | modules.deactivate() 61 | cons.deactivate() 62 | @spinner.dispose() 63 | 64 | commands: (subs) -> 65 | subs.add atom.commands.add 'atom-text-editor', 66 | 'julia-client:evaluate': (event) => 67 | @withInk => 68 | client.start() 69 | evaluation.eval() 70 | 'julia-client:evaluate-all': (event) => 71 | @withInk => 72 | client.start() 73 | evaluation.evalAll() 74 | 75 | subs.add atom.commands.add 'atom-workspace', 76 | 'julia-client:open-a-repl': => terminal.repl() 77 | 'julia-client:start-repl-client': => 78 | client.disrequire => 79 | tcp.listen (port) => terminal.client port 80 | 'julia-client:start-julia': => 81 | client.disrequire => 82 | tcp.listen (port) => process.start port, cons 83 | 'julia-client:toggle-console': => @withInk => cons.toggle() 84 | 'julia-client:reset-loading-indicator': => client.reset() 85 | 86 | subs.add atom.commands.add 'atom-text-editor[data-grammar="source julia"]:not([mini])', 87 | 'julia-client:set-working-module': => modules.chooseModule() 88 | 'julia-client:reset-working-module': => modules.resetModule() 89 | 90 | utils.commands subs 91 | 92 | consumeInk: (ink) -> 93 | @ink = ink 94 | evaluation.ink = ink 95 | cons.ink = ink 96 | @loading = new ink.Loading 97 | client.loading = @loading 98 | cons.loading = @loading 99 | @spinner = new ink.Spinner client.loading 100 | client.handle 'show-block', ({start, end}) => 101 | if ed = atom.workspace.getActiveTextEditor() 102 | ink.highlight ed, start-1, end-1 103 | 104 | withInk: (f, err) -> 105 | if @ink? 106 | f() 107 | else if err 108 | atom.notifications.addError 'Please install the Ink package.', 109 | detail: 'Julia Client requires the Ink package to run. 110 | You can install it from the settings view.' 111 | dismissable: true 112 | else 113 | setTimeout (=> @withInk f, true), 100 114 | 115 | consumeStatusBar: (bar) -> modules.consumeStatusBar(bar) 116 | 117 | completions: -> completions 118 | -------------------------------------------------------------------------------- /lib/modules.coffee: -------------------------------------------------------------------------------- 1 | client = require './connection/client' 2 | selector = require './ui/selector' 3 | {CompositeDisposable} = require 'atom' 4 | 5 | module.exports = 6 | activate: -> 7 | @subscriptions = new CompositeDisposable 8 | @createStatusUI() 9 | 10 | # configure all the events that persist until we're deactivated 11 | @subscriptions.add atom.workspace.onDidChangeActivePaneItem => @activePaneChanged() 12 | @subscriptions.add client.onConnected => @editorStateChanged() 13 | @subscriptions.add client.onDisconnected => @editorStateChanged() 14 | @activePaneChanged() 15 | 16 | deactivate: -> 17 | @tile?.destroy() 18 | @tile = null 19 | @subscriptions.dispose() 20 | @grammarChangeSubscription?.dispose() 21 | @moveSubscription?.dispose() 22 | if @pendingUpdate then clearTimeout @pendingUpdate 23 | 24 | 25 | activePaneChanged: -> 26 | @grammarChangeSubscription?.dispose() 27 | ed = atom.workspace.getActivePaneItem() 28 | @grammarChangeSubscription = ed?.onDidChangeGrammar? => @editorStateChanged() 29 | @editorStateChanged() 30 | 31 | # sets or clears the callback on cursor change based on the editor state 32 | editorStateChanged: -> 33 | # first clear the display and remove any old subscription 34 | @clear() 35 | @moveSubscription?.dispose() 36 | 37 | # now see if we need to resubscribe 38 | ed = atom.workspace.getActivePaneItem() 39 | if ed?.getGrammar?().scopeName == 'source.julia' && client.isConnected() 40 | @moveSubscription = ed.onDidChangeCursorPosition => 41 | if @pendingUpdate then clearTimeout @pendingUpdate 42 | @pendingUpdate = setTimeout (=> @update()), 300 43 | @update() 44 | 45 | # Status Bar 46 | 47 | createStatusUI: -> 48 | @dom = document.createElement 'span' 49 | @dom.classList.add 'julia-client' 50 | @main = document.createElement 'a' 51 | @sub = document.createElement 'span' 52 | @divider = document.createElement 'span' 53 | @divider.innerText = '/' 54 | 55 | @main.onclick = => 56 | atom.commands.dispatch atom.views.getView(atom.workspace.getActiveTextEditor()), 57 | 'julia-client:set-working-module' 58 | 59 | atom.tooltips.add @dom, 60 | title: => "Currently working with module #{@currentModule()}" 61 | 62 | currentModule: -> 63 | if @isInactive then "Main" 64 | else if @isSubInactive || @sub.innerText == "" then @main.innerText 65 | else "#{@main.innerText}.#{@sub.innerText}" 66 | 67 | consumeStatusBar: (bar) -> 68 | @tile = bar.addRightTile {item: @dom} 69 | 70 | clear: -> @dom.innerHTML = "" 71 | 72 | reset: (main, sub) -> 73 | @clear() 74 | @main.innerText = main 75 | @sub.innerText = sub 76 | @dom.appendChild @main 77 | if sub 78 | @dom.appendChild @divider 79 | @dom.appendChild @sub 80 | @active() 81 | 82 | isSubInactive: false 83 | isInactive: false 84 | 85 | active: -> 86 | @isInactive = false 87 | @isSubInactive = false 88 | @main.classList.remove 'fade' 89 | @divider.classList.remove 'fade' 90 | @sub.classList.remove 'fade' 91 | subInactive: -> 92 | @active() 93 | @isSubInactive = true 94 | @sub.classList.add 'fade' 95 | inactive: -> 96 | @active() 97 | @isInactive = true 98 | @main.classList.add 'fade' 99 | @divider.classList.add 'fade' 100 | @sub.classList.add 'fade' 101 | 102 | # gets the current module from the Julia process and updates the display. 103 | # assumes that we're connected and in a julia file 104 | update: -> 105 | ed = atom.workspace.getActivePaneItem() 106 | {row, column} = ed.getCursors()[0].getScreenPosition() 107 | data = 108 | path: ed.getPath() 109 | code: ed.getText() 110 | row: row+1, column: column+1 111 | module: ed.juliaModule 112 | 113 | client.msg 'module', data, ({main, sub, inactive, subInactive}) => 114 | @reset main, sub 115 | subInactive && @subInactive() 116 | inactive && @inactive() 117 | 118 | # TODO: auto detect option, remove reset command 119 | chooseModule: -> 120 | client.require => 121 | mods = new Promise (resolve) => 122 | client.msg 'all-modules', {}, (mods) => 123 | resolve mods 124 | selector.show mods, (mod) => 125 | return unless mod? 126 | atom.workspace.getActiveTextEditor().juliaModule = mod 127 | @update() 128 | 129 | resetModule: -> 130 | client.require => 131 | delete atom.workspace.getActiveTextEditor().juliaModule 132 | @update() 133 | -------------------------------------------------------------------------------- /docs/communication.md: -------------------------------------------------------------------------------- 1 | # Communication 2 | 3 | Juno works by booting a Julia client from Atom. When Julia starts it connects to Atom over a 4 | TCP port, and the upshot is that from that point on Julia and Atom can each send messages to 5 | each other. Messages are JSON objects, with a type header to tell the receiver how the 6 | message should be handled. 7 | 8 | The code handling low-level communication is kept in 9 | [client.coffee](https://github.com/JunoLab/atom-julia-client/blob/master/lib/connection/client.coffee) 10 | and [comm.jl](https://github.com/JunoLab/Atom.jl/blob/master/src/comm.jl). However, the 11 | details of those files aren't particularly important – you only need to understand the 12 | communication API, which we'll go over here. 13 | 14 | ## Sending messages from Atom 15 | 16 | Communication works by sending messages with an appropriate type on one side and registering 17 | handlers for that type on the other. The handler then takes some action and returns data to 18 | the original sender. For example, on the Atom side messages are sent in CoffeeScript as 19 | follows: 20 | 21 | ```coffeescript 22 | client.msg 'eval', {code: '2+2'} 23 | ``` 24 | 25 | On the Julia side, we need to set up a handler for this message, which happens as follows: 26 | 27 | ```julia 28 | handle("eval") do data 29 | Dict(:result => eval(parse(data["code"]))) 30 | end 31 | ``` 32 | 33 | This is a very simplified version of the `eval` handler that you can find in the Atom.jl 34 | source code. It simply evaluates whatever it's given and returns the result – in this case, 35 | `4` – inside a dictionary. 36 | 37 | Often we want to do something with that return result in Atom – in this case, we'd like to 38 | display the result. We don't need to change anything on the Julia side to accomplish this; 39 | it's as simple as adding a callback to the original `msg` call: 40 | 41 | ```coffeescript 42 | client.msg 'eval', {code: '2+2'}, ({result}) => 43 | console.log data 44 | ``` 45 | 46 | This call sends the `eval` message, pulls the `result` field out of the returned JSON, and 47 | displays the result, `4`, in the console. 48 | 49 | This approach is exactly how Atom gets evaluation results, autocompletion and more from 50 | Julia – so it's easy to find more examples spread throughout the 51 | [julia-client](https://github.com/JunoLab/atom-julia-client/tree/master/lib) and 52 | [Atom.jl](https://github.com/JunoLab/Atom.jl/tree/master/src) source code. 53 | 54 | As a first project, try implementing an Atom command (see the Atom docs) which sends this 55 | message to Julia, as well as adding the Julia handler above to Atom.jl. (You'll want to use 56 | a type other than `eval` to avoid clashes with actual evaluation.) 57 | 58 | ## Sending messages from Julia 59 | 60 | Julia has a similar mechanism to talk to Atom via the function 61 | 62 | ```julia 63 | msg("type", data) 64 | ``` 65 | 66 | Handlers are defined on the Atom side as follows: 67 | 68 | ```coffeescript 69 | client.handle 'type', (data) => 70 | console.log data 71 | ``` 72 | 73 | It's also possible for Julia to wait for a response from Atom, using the `rpc` function. 74 | An extra argument is provided to the JS handler which can be called with the data to send 75 | back to Julia. To clarify this a little, say you have a handler in Atom which looks like 76 | this: 77 | 78 | ```coffeescript 79 | client.handle 'echo', (data, resolve) => 80 | resolve data 81 | ``` 82 | 83 | (It's very easy to add this code to `julia-client`'s [`activate` 84 | function](https://github.com/JunoLab/atom-julia-client/blob/master/lib/julia-client.coffee) 85 | if you want to try this out.) 86 | 87 | Calling the following from the REPL: 88 | 89 | ```julia 90 | Atom.rpc("echo", Dict(:a=>1, :b=>2)) 91 | ``` 92 | 93 | will return `Dict("a"=>1, "b"=>2)`. The data was passed to Atom and simply returned as-is. 94 | Try changing the handler to modify the data before returning it. 95 | 96 | This mechanism is how Julia commands like `Atom.select()` are implemented, and in general it 97 | makes it very simple for Julia to control the Atom frontend – see 98 | [frontend.jl](https://github.com/JunoLab/Atom.jl/blob/master/src/frontend.jl) and 99 | [frontend.coffee](https://github.com/JunoLab/atom-julia-client/blob/master/lib/frontend.coffee) 100 | 101 | ## Debugging and Learning 102 | 103 | A good way to get a handle on this stuff is just to use `console.log` and `@show`, on the 104 | Atom and Julia sides respectively, to take a peek at what's going over the wire. For example, 105 | it's easy to change the above Julia handler to 106 | 107 | ```julia 108 | handle("eval") do data 109 | @show data 110 | @show Dict(:result => eval(parse(data["code"]))) 111 | end 112 | ``` 113 | 114 | This will show you both the data being sent to Julia (in the example above, 115 | `Dict("code"=>"2+2")`) and the data being sent back to Atom (`Dict(:result => 4)`). 116 | Modifying say, the completions handler in a similar way will show you what completion data 117 | Julia sends back to Atom (there will probably be a lot, so try looking at specific 118 | keys, for example). 119 | 120 | You don't need to reload Atom or restart the Julia client every time you make a change like 121 | this. If you open a file from the `Atom.jl` source code, you should see from the status bar 122 | that Juno knows you're working with the `Atom` module (try evaluating `current_module()` if 123 | you're not sure). Evaluating `handlers` from within the `Atom` module will show you what 124 | message types are currently defined. If you change a handler, just press `C-Enter` to update 125 | it in place; you should see the effect of your update immediately next time the handler is 126 | triggered. For example, if you modify the 127 | [`eval`](https://github.com/JunoLab/Atom.jl/blob/master/src/eval.jl) handler as follows: 128 | 129 | ```julia 130 | handle("eval") do data 131 | println(data["code"]) # <- insert this line 132 | # ... 133 | ``` 134 | 135 | and update it, you should find that the *next* time you evaluate you see the contents of the 136 | current editor dumped into the console. Thus, most features or fixes you'd want to add to 137 | Juno can be made without a long edit – compile – run cycle. 138 | -------------------------------------------------------------------------------- /docs/workflow.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Workflow 7 | 8 | One of the goals of this project is to help users adopt a more efficient 9 | workflow when editing Julia code. To understand how to gain this productivity 10 | boost, we will consider a common workflow for many Julia developers and a Juno 11 | based workflow. 12 | 13 | ## Status quo 14 | 15 | Suppose you are working on a file `foo.jl` that contains the definition of a 16 | type `Bar`. As you are hacking away on the fields and functionality of the `Bar` 17 | type you want to experiment with your work in the REPL. Here's what you might do: 18 | 19 | - Edit `foo.jl` 20 | - Fire up Julia and `include("foo.jl")` to load up your new types and methods 21 | - Return to the editor to make more changes to `foo.jl` 22 | - Go back to your REPL session and `include("foo.jl")` again, only to be greeted 23 | by the following error message: 24 | 25 | ``` 26 | julia> include("foo.jl") 27 | ERROR: LoadError: invalid redefinition of constant Bar 28 | in include at ./boot.jl:260 29 | in include_from_node1 at ./loading.jl:271 30 | while loading /Users/sglyon/Desktop/temp/julia/junk/foo.jl, in expression starting on line 3 31 | ``` 32 | 33 | At this point you have two options: 34 | 35 | 1. Quit Julia completely and start over. Then we will be able to 36 | `include("foo.jl")` one time before seeing that error message again. 37 | 2. Follow the [workflow tips](http://docs.julialang.org/en/latest/manual/workflow-tips/#repl-based-workflow) 38 | section of the manual and wrap the code in a module. 39 | 40 | Option 1 is painful for obvious reasons: every time we make a change to anything 41 | in `foo.jl` we have to restart Julia and reload everything associated with our 42 | code (including other packages we are `using`). 43 | 44 | Option 2 seems better: it allows us to continue working with the same REPL 45 | instance and reload code as we need to. However, it also comes with some costs: 46 | 47 | - We now have to prefix all REPL access to anything in `foo.jl` with the name 48 | of our module: i.e. we use `ModuleName.Bar` instead of just `Bar`. 49 | - Although we can re`include` our module, we will be having Julia reload the 50 | entire module, rather than just the parts that we have changed. 51 | 52 | Wouldn't it be great if we had a solution that allowed: 53 | 54 | - Reloading code at will 55 | - Not prefixing everything with a module name 56 | - Only reloading the code that actually changes 57 | - Avoiding the need to switch back and forth from editor to REPL (a bonus :+1:) 58 | 59 | ## Enter Juno 60 | 61 | Juno provides a way to do just that. I will first describe how the Juno workflow 62 | looks and then explain a few of the details that make it possible: 63 | 64 | - Start by putting the code from `foo.jl` in a module (as in option 2 above). 65 | Here we use the name `Tmp`. 66 | - Launch a Julia client by executing the command `Julia client: Start Julia` 67 | (`cmd-j cmd-s` on OSX `ctrl-j ctrl-s` otherwise). 68 | - Now, while editing `foo.jl` you should see `Main/Tmp` on the status 69 | bar as shown in the screenshot below: 70 | 71 | ![](static/main_modulename.png) 72 | 73 | - If `foo.jl` is not on the Julia `LOAD_PATH` you should evaluate the 74 | whole file by executing the `Julia client: evaluate all` command 75 | (`cmd-shift-enter` or `ctrl-shift-enter`) 76 | - Now open a different buffer and set the syntax to Julia (`ctrl+shift+l` on all 77 | platforms and select `Julia`). While this buffer is active run the 78 | `Julia Client: Set working module` command (`cmd-j cmd-m` or `ctrl-j ctrl-m`) 79 | and select the name of the module defined in `foo.jl`. After doing this you 80 | should see `Tmp` on the status bar where `Main/Tmp` was: 81 | 82 | ![](static/scratch_modulename.png) 83 | 84 | - Now you are prepared to treat this buffer like the Julia REPL by writing code 85 | and evaluating it using `cmd-enter` (or `ctrl-enter` on Linux or Windows). 86 | - The key difference is that this "REPL" will have _unqualifed_ access to 87 | all members of `Tmp`: i.e. you can use `Bar` instead of `ModuleName.Bar` 88 | as was required in option 2 above. 89 | - What's more is that you can go back to `foo.jl`, edit and evaluate method 90 | definitions (again using `cmd-enter` or `ctrl-enter`) and the new methods 91 | are automatically ready to go when you return to the second buffer 92 | 93 | ### How it works 94 | 95 | So, how does this magic work? The secret sauce used here is clever use of an 96 | optional argument to the `Base.eval` method. Let's look at the `?help` entry for 97 | `Base.eval`: 98 | 99 | ``` 100 | help?> Base.eval 101 | eval([m::Module], expr::Expr) 102 | 103 | Evaluate an expression in the given module and return the result. Every Module (except those defined with baremodule) has its own 1-argument definition of eval, which evaluates expressions in that 104 | module. 105 | ``` 106 | 107 | Notice that if one argument is given, `Expr` is evaluated in the current working 108 | module. However, if two arguments are given the first argument gives the name of 109 | the module in which the `Expr` should be evaluated. So, when you run `Julia 110 | Client: Set working module` what you are doing is telling Atom to evaluate all 111 | the code from the current buffer directly into the chosen module. In our example 112 | above, this means that all the code we evaluate from `scratch.jl` is evaluated 113 | within the `Tmp` module _as if_ we had put the code in the module to begin with. 114 | 115 | ### Pro Tips 116 | 117 | This workflow can take some getting used to, but can yield massive productivity 118 | boosts once you master it. Here are some tips to help as you learn: 119 | 120 | - Don't ever set the current working module to the module defined in the buffer 121 | you are working on. 122 | - In the example above this means that if we wouldn't set the module to `Tmp` 123 | from within `foo.jl`. 124 | - Doing so would show `Tmp/Tmp` in the status bar and would make it awkward 125 | to re-evaluate the entire buffer later on. 126 | - If you do get in this situation, set the current working module to `Main` 127 | to restore `Main/Tmp` in the status bar 128 | - You can't change a type definition and re-evaluate with `cmd-enter` (or 129 | `ctrl-enter`). Instead, whenever you modify a type definition you should 130 | re-evaluate whole buffer using `cmd-shift-enter` (`ctrl-shift-enter`). 131 | - In the description of the workflow we mentioned opening up throwaway buffer 132 | and simply setting the grammar (active syntax) to Julia. Here are two 133 | alternatives: 134 | 1. You could treat the main file itself (`foo.jl` in the example) as the 135 | REPL. This saves you from having to set the working module in a 136 | different buffer, but also means you probably need to do some cleanup 137 | when you are done. 138 | 2. Instead of throwing away the second buffer, you could save it as a 139 | scratch file (I create files named `scratch.jl`). This file then becomes 140 | like a persistent REPL where test code can survive across coding sessions. 141 | - This can be helpful when you are working on the same code over 142 | multiple work sessions as it means you do not have to construct 143 | intermediate test objects from nothing each time. 144 | - Using this workflow is especially helpful when working on `Base` Julia. As you 145 | make changes to the files you don't need to re-build Julia to interact with 146 | them. The same logic applies to other large modules and packages. 147 | - While Juno understands how to deal with dependence on the current working 148 | directory in calls to functions like `include`, sometimes it is still useful 149 | to have the Julia instance work from the directory of your main file (e.g. 150 | when you are writing output files to disk) 151 | - The commands `Julia Client: Work in File Folder` will set the working 152 | directory for the Julia process to be the directory where your file is 153 | contained 154 | - I bind this function to a keybinding in my `~/.atom/keymaps.cson` to make 155 | this more convenient: 156 | ```coffeescript 157 | 'atom-text-editor[data-grammar="source julia"]': 158 | 'cmd-j cmd-f': 'julia-client:work-in-file-folder' 159 | ``` 160 | - There are also commands `Julia Client: Work in Project Folder` and 161 | `Julia Client: Work in Home Folder` that can set the directory for the 162 | Julia process. 163 | --------------------------------------------------------------------------------