├── keymaps ├── .gitignore ├── julia-client.cson.cmd └── julia-client.cson.ctrl ├── script ├── .gitignore ├── postinstall.js └── boot_repl.jl ├── .gitignore ├── manual ├── static │ ├── main_modulename.png │ └── scratch_modulename.png └── README.md ├── ci └── packages.jl ├── .github └── ISSUE_TEMPLATE │ └── bugfeature.md ├── lib ├── connection │ ├── process │ │ ├── boot.js │ │ ├── tcp.coffee │ │ ├── cycler.coffee │ │ ├── basic.js │ │ └── server.coffee │ ├── terminal.coffee │ ├── local.coffee │ ├── ipc.coffee │ ├── messages.coffee │ └── client.coffee ├── ui │ ├── notifications.coffee │ ├── progress.coffee │ ├── highlighter.coffee │ ├── focusutils.js │ ├── docs.js │ ├── selector.js │ ├── cellhighlighter.js │ ├── views.coffee │ └── layout.js ├── runtime │ ├── packages.js │ ├── frontend.js │ ├── linter.js │ ├── urihandler.js │ ├── profiler.js │ ├── debuginfo.js │ ├── workspace.coffee │ ├── environments.js │ ├── formatter.js │ ├── datatip.js │ ├── outline.js │ ├── plots.js │ ├── completions.js │ ├── modules.coffee │ ├── goto.js │ └── evaluation.coffee ├── misc │ ├── colors.js │ ├── weave.js │ ├── cells.js │ ├── scopes.js │ ├── words.js │ ├── paths.js │ └── blocks.js ├── package │ ├── release-note.js │ ├── settings.js │ ├── toolbar.coffee │ └── menu.coffee ├── ui.coffee ├── connection.coffee ├── misc.coffee ├── runtime.coffee └── julia-client.coffee ├── appveyor.yml ├── release-notes ├── README.md ├── 0.12.6.md └── 0.12.5.md ├── README.md ├── .travis.yml ├── LICENSE.md ├── spec ├── juno-spec.coffee ├── juno-spec.js.bak ├── eval.coffee └── client.coffee ├── CHANGELOG.md ├── package.json ├── CONTRIBUTING.md ├── menus └── julia-client.cson ├── docs ├── README.md └── communication.md └── styles └── julia-client.less /keymaps/.gitignore: -------------------------------------------------------------------------------- 1 | julia-client.cson 2 | -------------------------------------------------------------------------------- /script/.gitignore: -------------------------------------------------------------------------------- 1 | Manifest.toml 2 | Project.toml 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /manual/static/main_modulename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoLab/atom-julia-client/HEAD/manual/static/main_modulename.png -------------------------------------------------------------------------------- /manual/static/scratch_modulename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoLab/atom-julia-client/HEAD/manual/static/scratch_modulename.png -------------------------------------------------------------------------------- /manual/README.md: -------------------------------------------------------------------------------- 1 | # Juno Manual 2 | 3 | This is the (work in progress) user manual for Juno. 4 | 5 | * [Installation Instructions](https://github.com/JunoLab/uber-juno/blob/master/setup.md) 6 | * [Workflow](workflow.md) – tips and tricks on using Juno/Julia productively 7 | -------------------------------------------------------------------------------- /ci/packages.jl: -------------------------------------------------------------------------------- 1 | jlpath = readchomp(`which julia`) 2 | 3 | mkpath("../julia") 4 | run(`ln -s $jlpath ../julia/julia`) 5 | 6 | using Pkg 7 | pkg"add Juno Atom" 8 | 9 | if get(ENV, "ATOMJL", "") == "master" 10 | pkg"add Atom#master" 11 | end 12 | 13 | using Juno 14 | import Atom 15 | -------------------------------------------------------------------------------- /script/postinstall.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | function copyKeymaps () { 4 | let suffix = process.platform === 'darwin' ? '.cmd' : '.ctrl' 5 | fs.copyFileSync(__dirname + '/../keymaps/julia-client.cson' + suffix, __dirname + '/../keymaps/julia-client.cson') 6 | } 7 | 8 | copyKeymaps() 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bugfeature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report/Feature request 3 | about: Please open your issue at https://github.com/JunoLab/Juno.jl/. 4 | --- 5 | 6 | Please open your issue at https://github.com/JunoLab/Juno.jl/issues/new unless you've been explicitly pointed here by a Juno developer. 7 | -------------------------------------------------------------------------------- /lib/connection/process/boot.js: -------------------------------------------------------------------------------- 1 | process.on('uncaughtException', function (err) { 2 | if (process.connected) { 3 | process.send({type: 'error', message: err.message, stack: err.stack}) 4 | } 5 | process.exit(1) 6 | }) 7 | 8 | process.on('unhandledRejection', function (err) { 9 | if (process.connected) { 10 | if (err instanceof Error) { 11 | process.send({type: 'rejection', message: err.message, stack: err.stack}) 12 | } else { 13 | process.send({type: 'rejection', err}) 14 | } 15 | } 16 | }) 17 | 18 | const server = require('./server') 19 | 20 | server.serve() 21 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | environment: 3 | APM_TEST_PACKAGES: "ink language-julia" 4 | ATOM_LINT_WITH_BUNDLED_NODE: "true" 5 | 6 | matrix: 7 | - ATOM_CHANNEL: stable 8 | - ATOM_CHANNEL: beta 9 | 10 | ### Generic setup follows ### 11 | build_script: 12 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) 13 | - julia: ci/packages.jl 14 | 15 | branches: 16 | only: 17 | - master 18 | 19 | version: "{build}" 20 | platform: x64 21 | clone_depth: 10 22 | skip_tags: true 23 | test: off 24 | deploy: off 25 | -------------------------------------------------------------------------------- /lib/ui/notifications.coffee: -------------------------------------------------------------------------------- 1 | remote = require 'remote' 2 | 3 | module.exports = 4 | # notes: [] 5 | # window: remote.getCurrentWindow() 6 | 7 | activate: -> 8 | # document.addEventListener 'focusin', => 9 | # @clear() 10 | 11 | enabled: -> atom.config.get('julia-client.uiOptions.notifications') 12 | 13 | show: (msg, force) -> 14 | # return unless force or (@enabled() and not document.hasFocus()) 15 | # n = new Notification "Julia Client", 16 | # body: msg 17 | # n.onclick = => 18 | # @window.focus() 19 | # @notes.push(n) 20 | 21 | # clear: -> 22 | # for note in @notes 23 | # note.close() 24 | # @notes = [] 25 | -------------------------------------------------------------------------------- /release-notes/README.md: -------------------------------------------------------------------------------- 1 | # Juno – release notes 2 | 3 | This folder contains release notes of Juno. 4 | Those notes are also posted as [Julia Discourse](https://discourse.julialang.org/c/tools/juno). 5 | 6 | The release notes will be automatically opened when an user has updated this package 7 | and the updated version has a release note. 8 | Also we can manually view a release note via `julia-client: open-release-note` command. 9 | 10 | 11 | ## Developer notes 12 | 13 | - in order for those features to work properly, release notes must be named under the rule: `major.minor.patch.md` 14 | - markdowns are parsed and transformed into HTML by [marked](https://marked.js.org/#/README.md) npm package 15 | - URLs should already exist somewhere; e.g. we should replace "upload://" when we use Discourse's image upload feature 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Julia Client 2 | 3 | 4 | **Attention**: We have decided to join forces with the [Julia extension for VSCode](https://github.com/julia-vscode/julia-vscode). As such, this Atom-based plugin is effectively in “maintenance-only mode” and we expect to only work on bug fixes in the future. 5 | 6 | ---- 7 | 8 | [![Developer Chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/JunoLab/Juno) [![Build Status](https://travis-ci.org/JunoLab/atom-julia-client.svg?branch=master)](https://travis-ci.org/JunoLab/atom-julia-client) [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://JunoLab.github.io/JunoDocs.jl/latest) 9 | 10 | This is the main repo for [Juno](http://junolab.org), the Julia IDE. Please see [here](http://docs.junolab.org/latest/man/installation) for installation instructions and report problems at [the discussion board](http://discourse.julialang.org/). 11 | -------------------------------------------------------------------------------- /lib/runtime/packages.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { client } from '../connection' 4 | import { selector } from '../ui' 5 | 6 | var { packages } = client.import({ rpc: ['packages'] }) 7 | 8 | export function openPackage (newWindow = true) { 9 | const pkgs = packages() 10 | pkgs.then(pkgs => { 11 | const ps = [] 12 | for (const pkg in pkgs) { 13 | ps.push({ primary: pkg, secondary: pkgs[pkg] }) 14 | } 15 | selector.show(ps, { infoMessage: 'Select package to open' }).then( pkg => { 16 | if (pkg) { 17 | if (newWindow) { 18 | atom.open({ pathsToOpen: [pkgs[pkg.primary]]}) 19 | } else { 20 | atom.project.addPath(pkgs[pkg.primary], { 21 | mustExist: true, 22 | exact: true 23 | }) 24 | } 25 | } 26 | }) 27 | }).catch(() => { 28 | atom.notifications.addError("Couldn't find your Julia packages.") 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: julia 2 | 3 | julia: 4 | - 1 5 | - nightly 6 | 7 | env: 8 | global: 9 | - APM_TEST_PACKAGES="ink language-julia" 10 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 11 | 12 | matrix: 13 | - "" 14 | - ATOMJL=master 15 | 16 | os: 17 | - linux 18 | 19 | matrix: 20 | include: 21 | # # Sanity check for OS X 22 | # - os: osx 23 | # julia: 1 24 | # env: ATOMJL=master 25 | # Sanity check for Atom Beta 26 | - os: linux 27 | julia: 1 28 | env: ATOM_CHANNEL=beta 29 | allow_failures: 30 | - julia: nightly 31 | - env: ATOM_CHANNEL=beta 32 | 33 | script: 34 | - julia ci/packages.jl 35 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 36 | - chmod u+x build-package.sh 37 | - ./build-package.sh 38 | 39 | dist: xenial 40 | addons: 41 | apt: 42 | sources: 43 | - ubuntu-toolchain-r-test 44 | packages: 45 | - g++-6 46 | - build-essential 47 | - fakeroot 48 | - git 49 | - libsecret-1-dev 50 | 51 | notifications: 52 | email: false 53 | -------------------------------------------------------------------------------- /lib/runtime/frontend.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { client } from '../connection' 4 | import { selector, notifications } from '../ui' 5 | import { colors } from '../misc' 6 | 7 | export function activate (ink) { 8 | client.handle({ 9 | select: (items) => selector.show(items), 10 | input: () => selector.show([], { allowCustom: true }), 11 | syntaxcolors: (selectors) => colors.getColors(selectors), 12 | openFile: (file, line) => { 13 | ink.Opener.open(file, line, { 14 | pending: atom.config.get('core.allowPendingPaneItems') 15 | }) 16 | }, 17 | versionwarning: (msg) => { 18 | atom.notifications.addWarning("Outdated version of Atom.jl detected.", { 19 | description: msg, 20 | dismissable: true 21 | }) 22 | }, 23 | notify: (msg) => notifications.show(msg, true), 24 | notification: (message, kind = 'Info', options = {}) => { 25 | try { 26 | atom.notifications[`add${kind}`](message, options) 27 | } catch (err) { 28 | console.log(err) 29 | } 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019: Contributors 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/runtime/linter.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { CompositeDisposable } from 'atom' 4 | import { client } from '../connection' 5 | 6 | let subs, lintPane 7 | 8 | export function activate (ink) { 9 | const linter = ink.Linter 10 | lintPane = linter.lintPane 11 | 12 | client.handle({ 13 | staticLint: (warnings) => { 14 | lintPane.ensureVisible({ 15 | split: atom.config.get('julia-client.uiOptions.layouts.linter.split') 16 | }) 17 | linter.setItems(warnings) 18 | } 19 | }) 20 | 21 | subs = new CompositeDisposable() 22 | 23 | subs.add(atom.commands.add('.workspace', { 24 | 'julia-client:clear-linter': () => linter.clearItems() 25 | })) 26 | subs.add(atom.config.observe('julia-client.uiOptions.layouts.linter.defaultLocation', (defaultLocation) => { 27 | lintPane.setDefaultLocation(defaultLocation) 28 | })) 29 | } 30 | 31 | export function open () { 32 | return lintPane.open({ 33 | split: atom.config.get('julia-client.uiOptions.layouts.linter.split') 34 | }) 35 | } 36 | 37 | export function close () { 38 | return lintPane.close() 39 | } 40 | 41 | export function deactivate () { 42 | if (subs) { 43 | subs.dispose() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/misc/colors.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | export function getColors(selectors) { 4 | let grammar = atom.grammars.grammarForScopeName("source.julia") 5 | 6 | let styled = {} 7 | let color = {} 8 | let div = document.createElement('div') 9 | div.classList.add('editor', 'editor-colors', 'julia-syntax-color-selector') 10 | 11 | for (let style in selectors) { 12 | let child = document.createElement('span') 13 | child.innerText = 'foo' 14 | child.classList.add(...selectors[style]) 15 | div.appendChild(child) 16 | styled[style] = child 17 | } 18 | 19 | document.body.appendChild(div) 20 | // wait till rendered? 21 | for (let style in selectors) { 22 | try { 23 | color[style] = rgb2hex(window.getComputedStyle(styled[style])['color']) 24 | } catch (e) { 25 | console.error(e) 26 | } 27 | } 28 | color['background'] = rgb2hex(window.getComputedStyle(div)['backgroundColor']) 29 | document.body.removeChild(div) 30 | 31 | return color 32 | } 33 | 34 | function rgb2hex(rgb) { 35 | if (rgb.search("rgb") == -1) { 36 | return rgb 37 | } else { 38 | rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/) 39 | function hex(x) { 40 | return ("0" + parseInt(x).toString(16)).slice(-2); 41 | } 42 | return hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/runtime/urihandler.js: -------------------------------------------------------------------------------- 1 | "use babel" 2 | 3 | import { client } from '../connection' 4 | import { docpane, views } from '../ui' 5 | 6 | const { moduleinfo } = client.import({ rpc: ['moduleinfo'] }) 7 | const docs = client.import('docs') 8 | 9 | export default function handleURI (parsedURI) { 10 | const { query } = parsedURI 11 | 12 | if (query.open) { // open a file 13 | atom.workspace.open(query.file, { 14 | initialLine: Number(query.line), 15 | pending: atom.config.get('core.allowPendingPaneItems') 16 | }) 17 | } else if (query.docs) { // show docs 18 | const { word, mod } = query 19 | docs({ word, mod }).then(result => { 20 | if (result.error) return 21 | const view = views.render(result) 22 | docpane.processLinks(view.getElementsByTagName('a')) 23 | docpane.ensureVisible() 24 | docpane.showDocument(view, []) 25 | }).catch(err => { 26 | console.log(err) 27 | }) 28 | } else if (query.moduleinfo){ // show module info 29 | const { mod } = query 30 | moduleinfo({ mod }).then(({ doc, items }) => { 31 | items.map(item => { 32 | docpane.processItem(item) 33 | }) 34 | const view = views.render(doc) 35 | docpane.ensureVisible() 36 | docpane.showDocument(view, items) 37 | }).catch(err => { 38 | console.log(err) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/package/release-note.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { CompositeDisposable, Disposable } from 'atom' 4 | import path from 'path' 5 | import fs from 'fs' 6 | import { show } from '../ui/selector' 7 | import { readCode } from '../misc/paths' 8 | 9 | let subs 10 | const RELEASE_NOTE_DIR = path.join(__dirname, '..', '..', 'release-notes') 11 | 12 | export function activate (ink, startupNoteVersion) { 13 | const pane = ink.NotePane.fromId('Note') 14 | subs = new CompositeDisposable() 15 | 16 | const showNote = (version) => { 17 | const p = path.join(RELEASE_NOTE_DIR, version + '.md') 18 | const markdown = readCode(p) 19 | pane.setNote(markdown) 20 | pane.setTitle(`Juno release note – v${version}`) 21 | pane.ensureVisible({ 22 | split: 'right' 23 | }) 24 | } 25 | 26 | subs.add( 27 | atom.commands.add('atom-workspace', 'julia-client:open-release-note', () => { 28 | const versions = fs.readdirSync(RELEASE_NOTE_DIR) 29 | .filter(path => path !== 'README.md') 30 | .map(path => path.replace(/(.+)\.md/, 'v $1')) 31 | show(versions) 32 | .then(version => showNote(version.replace(/v\s(.+)/, '$1'))) 33 | .catch(err => console.log(err)) 34 | }) 35 | ) 36 | if (startupNoteVersion) showNote(startupNoteVersion) 37 | } 38 | 39 | export function deactivate () { 40 | if (subs) subs.dispose() 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Disposable} = require 'atom' 2 | 3 | module.exports = 4 | notifications: require './ui/notifications' 5 | selector: require './ui/selector' 6 | views: require './ui/views' 7 | progress: require './ui/progress' 8 | layout: require './ui/layout' 9 | docpane: require './ui/docs' 10 | focusutils: require './ui/focusutils' 11 | cellhighlighter: require './ui/cellhighlighter' 12 | 13 | activate: (@client) -> 14 | @subs = new CompositeDisposable 15 | 16 | @notifications.activate() 17 | @subs.add atom.config.observe 'julia-client.uiOptions.highlightCells', (val) => 18 | if val 19 | @cellhighlighter.activate() 20 | else 21 | @cellhighlighter.deactivate() 22 | @subs.add new Disposable => 23 | @cellhighlighter.deactivate() 24 | 25 | @subs.add @client.onAttached => 26 | @notifications.show("Client Connected") 27 | @subs.add @client.onDetached => 28 | @ink?.Result.invalidateAll() 29 | 30 | deactivate: -> 31 | @subs.dispose() 32 | 33 | consumeInk: (@ink) -> 34 | @views.ink = @ink 35 | @selector.activate(@ink) 36 | @docpane.activate(@ink) 37 | @progress.activate(@ink) 38 | @focusutils.activate(@ink) 39 | @subs.add(new Disposable(=> 40 | @docpane.deactivate() 41 | @progress.deactivate() 42 | @focusutils.deactivate())) 43 | -------------------------------------------------------------------------------- /lib/connection.coffee: -------------------------------------------------------------------------------- 1 | {time} = require './misc' 2 | externalTerminal = require './connection/terminal' 3 | 4 | module.exports = 5 | IPC: require './connection/ipc' 6 | messages: require './connection/messages' 7 | client: require './connection/client' 8 | local: require './connection/local' 9 | terminal: require './connection/terminal' 10 | 11 | activate: -> 12 | @messages.activate() 13 | @client.activate() 14 | @client.boot = => @boot() 15 | @local.activate() 16 | @booting = false 17 | 18 | deactivate: -> 19 | @client.deactivate() 20 | 21 | consumeInk: (ink) -> 22 | @IPC.consumeInk ink 23 | @ink = ink 24 | 25 | consumeGetServerConfig: (getconf) -> 26 | @local.consumeGetServerConfig(getconf) 27 | 28 | consumeGetServerName: (name) -> 29 | @local.consumeGetServerName(name) 30 | 31 | _boot: (provider) -> 32 | if not @client.isActive() and not @booting 33 | @booting = true 34 | @client.setBootMode(provider) 35 | if provider is 'External Terminal' 36 | p = externalTerminal.connectedRepl() 37 | else 38 | p = @local.start(provider) 39 | 40 | if @ink? 41 | @ink.Opener.allowRemoteFiles(provider == 'Remote') 42 | p.then => 43 | @booting = false 44 | p.catch => 45 | @booting = false 46 | time "Julia Boot", @client.import('ping')() 47 | 48 | bootRemote: -> 49 | @_boot('Remote') 50 | 51 | boot: -> 52 | @_boot(atom.config.get('julia-client.juliaOptions.bootMode')) 53 | -------------------------------------------------------------------------------- /lib/connection/terminal.coffee: -------------------------------------------------------------------------------- 1 | child_process = require 'child_process' 2 | net = require 'net' 3 | 4 | tcp = require './process/tcp' 5 | client = require './client' 6 | {paths} = require '../misc' 7 | 8 | disrequireClient = (a, f) -> client.disrequire a, f 9 | 10 | module.exports = 11 | 12 | escpath: (p) -> '"' + p + '"' 13 | escape: (sh) -> sh.replace(/"/g, '\\"') 14 | 15 | exec: (sh) -> 16 | child_process.exec sh, (err, stdout, stderr) -> 17 | if err? 18 | console.log err 19 | 20 | term: (sh) -> 21 | switch process.platform 22 | when "darwin" 23 | @exec "osascript -e 'tell application \"Terminal\" to activate'" 24 | @exec "osascript -e 'tell application \"Terminal\" to do script \"#{@escape(sh)}\"'" 25 | when "win32" 26 | @exec "#{@terminal()} \"#{sh}\"" 27 | else 28 | @exec "#{@terminal()} \"#{@escape(sh)}\"" 29 | 30 | terminal: -> atom.config.get("julia-client.consoleOptions.terminal") 31 | 32 | defaultShell: -> 33 | sh = process.env["SHELL"] 34 | if sh? 35 | sh 36 | else if process.platform == 'win32' 37 | 'powershell.exe' 38 | else 39 | 'bash' 40 | 41 | defaultTerminal: -> 42 | if process.platform == 'win32' 43 | 'cmd /C start cmd /C' 44 | else 45 | 'x-terminal-emulator -e' 46 | 47 | repl: -> @term "#{@escpath paths.jlpath()}" 48 | 49 | connectCommand: -> 50 | tcp.listen().then (port) => 51 | "#{@escpath paths.jlpath()} #{client.clargs().join(' ')} #{paths.script('boot_repl.jl')} #{port}" 52 | 53 | connectedRepl: -> @connectCommand().then (cmd) => @term cmd 54 | -------------------------------------------------------------------------------- /lib/ui/progress.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {client} = require '../connection' 3 | {formatTimePeriod} = require '../misc' 4 | 5 | module.exports = 6 | progs: {} 7 | 8 | activate: (ink) -> 9 | @subs = new CompositeDisposable 10 | @ink = ink 11 | client.handle 'progress': (t, id, m) => @[t] id, m 12 | status = [] 13 | @subs.add( 14 | client.onWorking => 15 | status = @ink.progress.add(null, description: 'Julia') 16 | client.onDone => status?.destroy() 17 | client.onAttached => @ink.progress.show() 18 | client.onDetached => @clear() 19 | ) 20 | 21 | deactivate: -> 22 | @clear() 23 | @subs.dispose() 24 | 25 | add: (id) -> 26 | pr = @ink.progress.add() 27 | pr.t0 = Date.now() 28 | pr.showTime = true 29 | @progs[id] = pr 30 | 31 | progress: (id, prog) -> 32 | pr = @progs[id] 33 | return unless pr? 34 | pr.level = prog 35 | if pr.showTime then @rightText id, null 36 | 37 | message: (id, m) -> @progs[id]?.message = m 38 | 39 | leftText: (id, m) -> @progs[id]?.description = m 40 | 41 | rightText: (id, m) -> 42 | pr = @progs[id] 43 | return unless pr? 44 | if m?.length 45 | pr.rightText = m 46 | pr.showTime = false 47 | else 48 | dt = (Date.now() - pr.t0)*(1/pr.level - 1)/1000 49 | pr.showTime = true 50 | pr.rightText = formatTimePeriod dt 51 | 52 | delete: (id) -> 53 | pr = @progs[id] 54 | return unless pr? 55 | pr.destroy() 56 | delete @progs[id] 57 | 58 | clear: -> 59 | for _, p of @progs 60 | p?.destroy() 61 | @progs = {} 62 | @ink.progress.hide() 63 | -------------------------------------------------------------------------------- /lib/runtime/profiler.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { client } from '../connection' 4 | import { CompositeDisposable } from 'atom' 5 | import { remote } from 'electron' 6 | 7 | let pane, subs 8 | var {loadProfileTrace, saveProfileTrace} = client.import({msg: ['loadProfileTrace', 'saveProfileTrace']}) 9 | 10 | export function activate (ink) { 11 | pane = ink.PlotPane.fromId('Profile') 12 | pane.getTitle = () => {return 'Profiler'} 13 | subs = new CompositeDisposable() 14 | 15 | subs.add(client.onDetached(() => clear())) 16 | subs.add(atom.config.observe('julia-client.uiOptions.layouts.profiler.defaultLocation', (defaultLocation) => { 17 | pane.setDefaultLocation(defaultLocation) 18 | })) 19 | 20 | client.handle({ 21 | profile(data) { 22 | const save = (path) => saveProfileTrace(path, data) 23 | const profile = new ink.Profiler.ProfileViewer({data, save, customClass: 'julia-profile'}) 24 | pane.ensureVisible({ 25 | split: atom.config.get('julia-client.uiOptions.layouts.profiler.split') 26 | }) 27 | pane.show(new ink.Pannable(profile, {zoomstrategy: 'width', minScale: 0.5})) 28 | } 29 | }) 30 | 31 | subs.add(atom.commands.add('atom-workspace', 'julia-client:clear-profile', () => { 32 | clear() 33 | pane.close() 34 | })) 35 | 36 | subs.add(atom.commands.add('atom-workspace', 'julia-client:load-profile-trace', () => { 37 | const path = remote.dialog.showOpenDialog({title: 'Load Profile Trace', properties: ['openFile']}) 38 | loadProfileTrace(path) 39 | })) 40 | } 41 | 42 | function clear () { 43 | pane.teardown() 44 | } 45 | 46 | export function deactivate () { 47 | subs.dispose() 48 | } 49 | -------------------------------------------------------------------------------- /spec/juno-spec.coffee: -------------------------------------------------------------------------------- 1 | juno = require '../lib/julia-client' 2 | {client} = juno.connection 3 | 4 | if process.platform is 'darwin' 5 | process.env.PATH += ':/usr/local/bin' 6 | 7 | basicSetup = -> 8 | jasmine.attachToDOM atom.views.getView atom.workspace 9 | waitsForPromise -> atom.packages.activatePackage 'language-julia' 10 | waitsForPromise -> atom.packages.activatePackage 'ink' 11 | waitsForPromise -> atom.packages.activatePackage 'julia-client' 12 | runs -> 13 | atom.config.set 'julia-client', 14 | juliaPath: 'julia' 15 | juliaOptions: 16 | bootMode: 'Basic' 17 | optimisationLevel: 2 18 | deprecationWarnings: false 19 | consoleOptions: 20 | rendererType: true 21 | 22 | cyclerSetup = -> 23 | basicSetup() 24 | runs -> atom.config.set 'julia-client.juliaOptions.bootMode', 'Cycler' 25 | 26 | conn = null 27 | 28 | withClient = -> 29 | beforeEach -> 30 | if conn? 31 | client.attach conn 32 | 33 | testClient = require './client' 34 | testEval = require './eval' 35 | 36 | describe "managing a basic client", -> 37 | beforeEach basicSetup 38 | testClient() 39 | 40 | describe "interaction with client cycler", -> 41 | beforeEach cyclerSetup 42 | testClient() 43 | 44 | describe "before use", -> 45 | beforeEach basicSetup 46 | it 'boots the client', -> 47 | waitsFor 5*60*1000, (done) -> 48 | juno.connection.boot().then -> done() 49 | runs -> 50 | conn = client.conn 51 | 52 | describe "in an editor", -> 53 | beforeEach basicSetup 54 | withClient() 55 | testEval() 56 | 57 | describe "after use", -> 58 | beforeEach basicSetup 59 | withClient() 60 | it "kills the client", -> 61 | client.kill() 62 | -------------------------------------------------------------------------------- /lib/runtime/debuginfo.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { client } from '../connection' 4 | 5 | const { reportinfo } = client.import(['reportinfo']) 6 | 7 | export default function debuginfo () { 8 | let atomReport = `# Atom: 9 | Version: ${atom.getVersion()} 10 | Dev Mode: ${atom.inDevMode()} 11 | Official Release: ${atom.isReleasedVersion()} 12 | ${JSON.stringify(process.versions, null, 2)} 13 | ` 14 | const atomPkgs = ['julia-client', 'ink', 'uber-juno', 'language-julia', 'language-weave', 15 | 'indent-detective', 'latex-completions'] 16 | atomPkgs.forEach((pkg, ind) => { 17 | atomReport += '# ' + atomPkgs[ind] + ':' 18 | let activePkg = atom.packages.getActivePackage(pkg) 19 | if (activePkg) { 20 | atomReport += 21 | ` 22 | Version: ${activePkg.metadata.version} 23 | Config: 24 | ${JSON.stringify(activePkg.config.settings[pkg], null, 2)} 25 | ` 26 | } else { 27 | atomReport += 'not installed\n' 28 | } 29 | atomReport += '\n\n' 30 | }) 31 | 32 | reportinfo().then(info => { 33 | atomReport += "# versioninfo():\n" 34 | atomReport += info 35 | showNotification(atomReport) 36 | }).catch(err => { 37 | atomReport += 'Could not connect to Julia.' 38 | showNotification(atomReport) 39 | }) 40 | } 41 | 42 | function showNotification (atomReport) { 43 | atom.notifications.addInfo('Juno Debug Info', { 44 | description: 'Please provide the info above when you report an issue. ' + 45 | 'Make sure to strip it of any kind of sensitive info you might ' + 46 | 'not want to share.', 47 | detail: atomReport, 48 | dismissable: true, 49 | buttons: [ 50 | { 51 | text: 'Copy to Clipboard', 52 | onDidClick: () => { 53 | atom.clipboard.write(atomReport) 54 | } 55 | } 56 | ] 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /lib/connection/process/tcp.coffee: -------------------------------------------------------------------------------- 1 | net = require 'net' 2 | client = require '../client' 3 | 4 | module.exports = 5 | server: null 6 | port: null 7 | 8 | listeners: [] 9 | 10 | next: -> 11 | conn = new Promise (resolve) => 12 | @listeners.push resolve 13 | conn.dispose = => 14 | @listeners = @listeners.filter (x) -> x is conn 15 | conn 16 | 17 | connect: (sock) -> 18 | message = (m) -> sock.write JSON.stringify m 19 | client.readStream sock 20 | sock.on 'end', -> client.detach() 21 | sock.on 'error', -> client.detach() 22 | client.attach {message} 23 | 24 | handle: (sock) -> 25 | if @listeners.length > 0 26 | @listeners.shift()(sock) 27 | else if not client.isActive() 28 | @connect sock 29 | else 30 | sock.end() 31 | 32 | listen: -> 33 | return Promise.resolve(@port) if @port? 34 | new Promise (resolve, reject) => 35 | externalPort = atom.config.get('julia-client.juliaOptions.externalProcessPort') 36 | if externalPort == 'random' 37 | port = 0 38 | else 39 | port = parseInt(externalPort) 40 | @server = net.createServer((sock) => @handle(sock)) 41 | @server.on 'error', (err) => 42 | if err.code == 'EADDRINUSE' 43 | details = '' 44 | if port != 0 45 | details = 'Please change to another port in the settings and try again.' 46 | atom.notifications.addError "Julia could not be started.", 47 | description: """ 48 | Port `#{port}` is already in use. 49 | 50 | """ + if details isnt '' 51 | """ 52 | #{details} 53 | """ 54 | else 55 | "Please try again or set a fixed port that you know is unused." 56 | dismissable: true 57 | reject(err) 58 | @server.listen port, '127.0.0.1', => 59 | @port = @server.address().port 60 | resolve(@port) 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.0 2 | * PlatformIO terminal integration 3 | * Results can be displayed in the Console instead of inline ([#332](https://github.com/JunoLab/atom-julia-client/pull/332)) 4 | * add support for `.junorc.jl` 5 | * add support for `Juno.notify()` API 6 | * julia path setting will now resolve `~` properly 7 | * added in-editor profile viewer ([#349](https://github.com/JunoLab/atom-julia-client/pull/349)) 8 | * overhaul of inline result display ([#127](https://github.com/JunoLab/atom-ink/pull/127)) 9 | 10 | ## 0.5.12 11 | * Graphical breakpoints 12 | 13 | ## 0.5.11 14 | * Connect Juno to Jupyter or a terminal repl 15 | * Cell-block eval mode 16 | * Inline results can be displayed in block-mode 17 | * Much more powerful `@progress` with support for nested loops and concurrency 18 | 19 | ## 0.5.0 20 | * Inline Documentation 21 | * Debugger 22 | * Workspace view 23 | * Autocomplete in the console 24 | * More helpful error messages when boot fails 25 | * `@progress` macro to show progress in the status bar 26 | * External packages can interact with the Julia client, including to provide custom boot mechanisms (e.g. over SSH) 27 | * Julia boot is now near-instant in many cases 28 | * Stack traces no longer show Atom.jl-internal code 29 | 30 | ## 0.4.0 31 | * Better Console buffering 32 | * History prefix support, like the repl 33 | * The console can work in any module 34 | * Plotting pane 35 | * Julia menu and toolbar 36 | 37 | ## 0.3.0 38 | * New Packages->Julia menu 39 | * Option to launch Julia on startup 40 | * Windows firewall no longer complains on first run 41 | * Console UI improvements 42 | * Improvments to copyability everywhere 43 | * Graphics support 44 | * REPL history integration 45 | 46 | ## 0.2.0 47 | * Improved scrolling for inline results 48 | * Inter-file links in errors and `methods` output 49 | * Interrupts also working on Windows 50 | * On-a-keystroke access to inline documentation and methods 51 | * Inline results are copyable 52 | 53 | ## 0.1.0 - First Release 54 | * Every feature added 55 | * Every bug fixed 56 | -------------------------------------------------------------------------------- /lib/misc.coffee: -------------------------------------------------------------------------------- 1 | {debounce} = require 'underscore-plus' 2 | 3 | module.exports = 4 | paths: require './misc/paths' 5 | blocks: require './misc/blocks' 6 | cells: require './misc/cells' 7 | words: require './misc/words' 8 | weave: require './misc/weave' 9 | colors: require './misc/colors' 10 | scopes: require './misc/scopes' 11 | 12 | bufferLines: (t, f) -> 13 | if not f? then [t, f] = [null, t] 14 | buffer = [''] 15 | flush = if not t? then -> else debounce (-> 16 | if buffer[0] isnt '' 17 | f buffer[0], false 18 | buffer[0] = ''), t 19 | (data) -> 20 | lines = data.toString().split '\n' 21 | buffer[0] += lines.shift() 22 | buffer.push lines... 23 | while buffer.length > 1 24 | f buffer.shift(), true 25 | flush() 26 | 27 | time: (desc, p) -> 28 | s = -> new Date().getTime()/1000 29 | t = s() 30 | p.then -> console.log "#{desc}: #{(s()-t).toFixed(2)}s" 31 | .catch -> 32 | p 33 | 34 | hook: (obj, method, f) -> 35 | souper = obj[method].bind obj 36 | obj[method] = (a...) -> f souper, a... 37 | 38 | once: (f) -> 39 | done = false 40 | (args...) -> 41 | return if done 42 | done = true 43 | f.call @, args... 44 | 45 | mutex: -> 46 | wait = Promise.resolve() 47 | (f) -> 48 | current = wait 49 | release = null 50 | wait = new Promise((resolve) -> release = resolve).catch -> 51 | current.then => f.call @, release 52 | 53 | exclusive: (f) -> 54 | lock = module.exports.mutex() 55 | (args...) -> 56 | lock (release) => 57 | result = f.call @, args... 58 | release result 59 | result 60 | 61 | # takes a time period in seconds and formats it as hh:mm:ss 62 | formatTimePeriod: (dt) -> 63 | return unless dt > 1 64 | h = Math.floor dt/(60*60) 65 | m = Math.floor (dt -= h*60*60)/60 66 | s = Math.round (dt - m*60) 67 | parts = [h, m, s] 68 | for i, dt of parts 69 | parts[i] = if dt < 10 then "0#{dt}" else "#{dt}" 70 | parts.join ':' 71 | -------------------------------------------------------------------------------- /lib/runtime/workspace.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | {client} = require '../connection' 4 | {views} = require '../ui' 5 | goto = require './goto' 6 | modules = require './modules' 7 | 8 | { workspace, gotosymbol: gotoSymbol, clearLazy } = client.import rpc: ['workspace', 'gotosymbol'], msg: 'clearLazy' 9 | 10 | module.exports = 11 | activate: -> 12 | @create() 13 | 14 | client.handle { updateWorkspace: => @update() } 15 | client.onDetached => 16 | @ws.setItems [] 17 | @lazyTrees = [] 18 | 19 | atom.config.observe 'julia-client.uiOptions.layouts.workspace.defaultLocation', (defaultLocation) => 20 | @ws.setDefaultLocation defaultLocation 21 | 22 | lazyTrees: [] 23 | 24 | update: -> 25 | return @ws.setItems [] unless client.isActive() and @ws.currentPane() 26 | clearLazy @lazyTrees 27 | registerLazy = (id) => @lazyTrees.push id 28 | mod = if @mod == modules.follow then modules.current() else (@mod or 'Main') 29 | p = workspace(mod).then (ws) => 30 | for {items} in ws 31 | for item in items 32 | item.value = views.render item.value, {registerLazy} 33 | item.onClick = @onClick(item.name) 34 | @ws.setItems ws 35 | p.catch (err) -> 36 | if err isnt 'disconnected' 37 | console.error 'Error refreshing workspace' 38 | console.error err 39 | 40 | onClick: (name) -> 41 | () => 42 | mod = if @mod == modules.follow then modules.current() else (@mod or 'Main') 43 | gotoSymbol 44 | word: name, 45 | mod: mod 46 | .then (results) => 47 | return if results.error 48 | goto.selectItemsAndGo(results.items) 49 | 50 | create: -> 51 | @ws = @ink.Workspace.fromId 'julia' 52 | @ws.setModule = (mod) => @mod = mod 53 | @ws.refresh = () => @update() 54 | @ws.refreshModule = () => 55 | m = modules.chooseModule() 56 | if m?.then? 57 | m.then(() => @update()) 58 | 59 | open: -> 60 | @ws.open 61 | split: atom.config.get 'julia-client.uiOptions.layouts.workspace.split' 62 | 63 | close: -> 64 | @ws.close() 65 | -------------------------------------------------------------------------------- /spec/juno-spec.js.bak: -------------------------------------------------------------------------------- 1 | const juno = require('../lib/julia-client') 2 | const path = require('path') 3 | const {client} = juno.connection 4 | 5 | function loadDependencies () { 6 | waitsForPromise(() => atom.packages.activatePackage('language-julia')) 7 | waitsForPromise(() => atom.packages.activatePackage('ink')) 8 | waitsForPromise(() => atom.packages.activatePackage('julia-client')) 9 | runs(() => { 10 | atom.config.set('julia-client.juliaPath', 'julia') 11 | atom.config.set('julia-client.juliaOptions', { 12 | bootMode: 'Basic', 13 | optimisationLevel: 2, 14 | deprecationWarnings: false, 15 | } 16 | ) 17 | }) 18 | } 19 | 20 | describe("before booting", function () { 21 | let checkPath = p => juno.misc.paths.getVersion(p) 22 | 23 | beforeEach(loadDependencies) 24 | it("can validate an existing julia binary", () => { 25 | waitsFor(done => { 26 | checkPath("julia").then(() => done()) 27 | }) 28 | }) 29 | it("can not validate a not existing julia binary", () => { 30 | waitsFor(done => { 31 | checkPath("imprettysureimnotjulia").catch(() => done()) 32 | }) 33 | }) 34 | }) 35 | function clientStatus () { 36 | return [client.isActive(), client.isWorking()] 37 | } 38 | let {echo, evalsimple} = client.import(['echo', 'evalsimple']) 39 | 40 | describe("managing a basic client", function () { 41 | beforeEach(loadDependencies) 42 | describe("when booting the client", function () { 43 | it("recognises the client's state before boot", () => { 44 | expect(clientStatus()).toEqual([false, false]) 45 | }) 46 | 47 | it("boots a julia process", () => { 48 | let pingpong = client.import('ping') 49 | waitsFor("the boot process to complete", 5*60*1000, (done) => { 50 | console.log(client); 51 | juno.connection.boot().then(() => { 52 | expect(clientStatus()).toEqual([true, true]) 53 | pingpong().then(val => { 54 | expect(val).toBe('pong') 55 | done() 56 | }) 57 | }) 58 | }) 59 | runs(() => { 60 | conn = client.conn 61 | }) 62 | }) 63 | 64 | it("recognises the client's state after boot", () => { 65 | expect(clientStatus()).toEqual([true, false]) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /spec/eval.coffee: -------------------------------------------------------------------------------- 1 | juno = require '../lib/julia-client' 2 | {client} = juno.connection 3 | 4 | module.exports = -> 5 | 6 | editor = null 7 | 8 | command = (ed, c) -> atom.commands.dispatch(atom.views.getView(ed), c) 9 | 10 | waitsForClient = -> waitsFor (done) -> client.onceDone done 11 | 12 | beforeEach -> 13 | waitsForPromise -> atom.workspace.open().then (ed) -> editor = ed 14 | # editor = atom.workspace.buildTextEditor() 15 | runs -> 16 | editor.setGrammar(atom.grammars.grammarForScopeName('source.julia')) 17 | 18 | it 'can evaluate code', -> 19 | client.handle test: (spy = jasmine.createSpy()) 20 | editor.insertText 'Atom.@rpc test()' 21 | command editor, 'julia-client:run-block' 22 | waitsForClient() 23 | runs -> 24 | expect(spy).toHaveBeenCalled() 25 | 26 | describe 'when an expression is evaluated', -> 27 | 28 | results = null 29 | 30 | beforeEach -> 31 | editor.insertText '2+2' 32 | waitsForPromise => 33 | juno.runtime.evaluation.eval().then (x) => results = x 34 | 35 | it 'retrieves the value of the expression', -> 36 | expect(results.length).toBe 1 37 | view = juno.ui.views.render results[0] 38 | expect(view.innerText).toBe '4' 39 | 40 | it 'displays the result', -> 41 | views = atom.views.getView(editor).querySelectorAll('.result') 42 | expect(views.length).toBe 1 43 | expect(views[0].innerText).toBe '4' 44 | 45 | describe 'completions', -> 46 | 47 | completionsData = -> 48 | editor: editor 49 | bufferPosition: editor.getCursors()[0].getBufferPosition() 50 | scopeDescriptor: editor.getCursors()[0].getScopeDescriptor() 51 | prefix: editor.getText() 52 | 53 | getSuggestions = -> 54 | completions = require '../lib/runtime/completions' 55 | completions.getSuggestions completionsData() 56 | 57 | describe 'basic module completions', -> 58 | 59 | completions = null 60 | 61 | beforeEach -> 62 | editor.insertText 'sin' 63 | waitsForPromise -> 64 | getSuggestions().then (cs) -> 65 | completions = cs 66 | 67 | it 'retrieves completions', -> 68 | completions = completions.map (c) -> c.text 69 | expect(completions).toContain 'sin' 70 | expect(completions).toContain 'sincos' 71 | expect(completions).toContain 'sinc' 72 | -------------------------------------------------------------------------------- /keymaps/julia-client.cson.cmd: -------------------------------------------------------------------------------- 1 | ### 2 | @NOTE 3 | It's best not to override default Atom keybindings if possible, and then 4 | register only in Julia-scoped places (e.g. Julia-syntax buffer, console) 5 | Any global commands should either be non-default or, ideally, prefixed with `C-J`. 6 | ### 7 | 8 | # Debug operations 9 | '.platform-darwin atom-text-editor[data-grammar="source julia"]:not(.mini), 10 | ink-terminal.julia-terminal, 11 | .ink-debugger-container': 12 | 'f5': 'julia-debug:run-file' 13 | 'cmd-f5': 'julia-debug:step-through-file' 14 | 'shift-f5': 'julia-debug:stop-debugging' 15 | 'f8': 'julia-debug:continue' 16 | 'shift-f8': 'julia-debug:step-to-selected-line' 17 | 'f9': 'julia-debug:toggle-breakpoint' 18 | 'shift-f9': 'julia-debug:toggle-conditional-breakpoint' 19 | 'f10': 'julia-debug:step-to-next-expression' 20 | 'shift-f10': 'julia-debug:step-to-next-line' 21 | 'f11': 'julia-debug:step-into' 22 | 'shift-f11': 'julia-debug:step-out' 23 | 24 | # Julia atom-text-editor 25 | '.platform-darwin atom-text-editor[data-grammar="source julia"]': 26 | 'cmd-enter': 'julia-client:run-block' 27 | 'shift-enter': 'julia-client:run-and-move' 28 | 'cmd-shift-enter': 'julia-client:run-all' 29 | 'alt-enter': 'julia-client:run-cell' 30 | 'alt-shift-enter': 'julia-client:run-cell-and-move' 31 | 'cmd-shift-a': 'julia-client:select-block' 32 | 'alt-down': 'julia-client:next-cell' 33 | 'alt-up': 'julia-client:prev-cell' 34 | 'cmd-j cmd-g': 'julia-client:goto-symbol' 35 | 'cmd-j cmd-d': 'julia-client:show-documentation' 36 | 'cmd-j cmd-m': 'julia-client:set-working-module' 37 | 'cmd-j cmd-f': 'julia-client:format-code' 38 | 39 | # Julia REPL 40 | '.platform-darwin .julia-terminal': 41 | 'ctrl-c': 'julia-client:interrupt-julia' 42 | 'cmd-j cmd-m': 'julia-client:set-working-module' 43 | 44 | # atom-workspace 45 | '.platform-darwin atom-workspace': 46 | 'cmd-j cmd-r': 'julia-client:open-external-REPL' 47 | 'cmd-j cmd-o': 'julia-client:open-REPL' 48 | 'cmd-j cmd-c': 'julia-client:clear-REPL' 49 | 'cmd-j cmd-s': 'julia-client:start-julia' 50 | 'cmd-j cmd-k': 'julia-client:kill-julia' 51 | 'cmd-j cmd-p': 'julia-client:open-plot-pane' 52 | 'cmd-j cmd-w': 'julia-client:open-workspace' 53 | 'cmd-j cmd-,': 'julia-client:settings' 54 | 'cmd-j cmd-e': 'julia-client:focus-last-editor' 55 | 'cmd-j cmd-t': 'julia-client:focus-last-terminal' 56 | 'cmd-j cmd-b': 'julia-client:return-from-goto' 57 | -------------------------------------------------------------------------------- /script/boot_repl.jl: -------------------------------------------------------------------------------- 1 | let 2 | # NOTE: No single quotes. File needs to be shorter than 2000 chars. 3 | if VERSION > v"0.7-" 4 | port = parse(Int, popfirst!(Base.ARGS)) 5 | else 6 | port = parse(Int, shift!(Base.ARGS)) 7 | end 8 | 9 | junorc = haskey(ENV, "JUNORC_PATH") ? joinpath(ENV["JUNORC_PATH"], "juno_startup.jl") : joinpath(homedir(), ".julia", "config", "juno_startup.jl") 10 | junorc = abspath(normpath(expanduser(junorc))) 11 | 12 | if isdefined(Base, :ACTIVE_PROJECT) 13 | active_project = Base.ACTIVE_PROJECT[] 14 | else 15 | active_project = nothing 16 | end 17 | 18 | if VERSION > v"0.7-" 19 | using Pkg 20 | Pkg.activate(@__DIR__, io=devnull) 21 | end 22 | 23 | if (VERSION > v"0.7-" ? Base.find_package("Atom") : Base.find_in_path("Atom")) == nothing 24 | p = VERSION > v"0.7-" ? x -> printstyled(x, color=:cyan, bold=true) : x -> print_with_color(:cyan, x, bold=true) 25 | p("\nHold on tight while we are installing some packages for you.\nThis should only take a few seconds...\n\n") 26 | 27 | Pkg.add("Atom") 28 | Pkg.add("Juno") 29 | 30 | println() 31 | end 32 | 33 | 34 | # TODO: Update me when tagging a new relase: 35 | MIN_ATOM_VER = v"0.12.11" 36 | outdated = false 37 | 38 | try 39 | if VERSION >= v"1.0-" 40 | using Pkg 41 | atompath = Base.find_package("Atom") 42 | 43 | if !occursin(Pkg.devdir(), atompath) # package is not `dev`ed 44 | tomlpath = joinpath(dirname(atompath), "..", "Project.toml") 45 | atomversion = VersionNumber(Pkg.TOML.parsefile(tomlpath)["version"]) 46 | 47 | if atomversion < MIN_ATOM_VER 48 | outdated = """ 49 | Please upgrade Atom.jl to at least version `$(MIN_ATOM_VER)` with e.g. `using Pkg; Pkg.update()` in an external REPL. 50 | """ 51 | end 52 | end 53 | end 54 | catch err 55 | end 56 | 57 | println("Starting Julia...") 58 | 59 | try 60 | import Atom 61 | using Juno 62 | if VERSION > v"0.7-" 63 | if active_project !== nothing 64 | Pkg.activate(active_project, io=devnull) 65 | end 66 | end 67 | Atom.handle("junorc") do path 68 | cd(path) 69 | ispath(junorc) && include(junorc) 70 | 71 | if outdated != false 72 | Atom.msg("versionwarning", outdated) 73 | end 74 | nothing 75 | end 76 | Atom.connect(port) 77 | catch 78 | if outdated != false 79 | printstyled("Outdated version of Atom.jl detected.\n", outdated, "\n", color = Base.error_color()) 80 | end 81 | rethrow() 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "julia-client", 3 | "main": "./lib/julia-client", 4 | "version": "0.12.6", 5 | "description": "The core package of Juno, the Julia IDE", 6 | "keywords": [ 7 | "julia", 8 | "Juno", 9 | "IDE", 10 | "evaluation", 11 | "run", 12 | "inline", 13 | "completion", 14 | "REPL", 15 | "terminal", 16 | "workspace", 17 | "outline", 18 | "documentation", 19 | "plot", 20 | "debugger" 21 | ], 22 | "repository": "https://github.com/JunoLab/atom-julia-client", 23 | "license": "MIT", 24 | "engines": { 25 | "atom": ">=1.56.0 <2.0.0" 26 | }, 27 | "dependencies": { 28 | "atom-package-deps": "*", 29 | "atom-select-list": "^0.7.2", 30 | "etch": "^0.14", 31 | "fuzzaldrin-plus": "^0.6.0", 32 | "node-pty-prebuilt-multiarch": "0.9.0", 33 | "object-hash": "^2.0.3", 34 | "physical-cpu-count": "*", 35 | "semver": "^6.3.0", 36 | "ssh2": "^0.8.4", 37 | "underscore-plus": "*" 38 | }, 39 | "scripts": { 40 | "postinstall": "node script/postinstall.js" 41 | }, 42 | "consumedServices": { 43 | "status-bar": { 44 | "versions": { 45 | "^1.0.0": "consumeStatusBar" 46 | } 47 | }, 48 | "tool-bar": { 49 | "versions": { 50 | "^0 || ^1": "consumeToolBar" 51 | } 52 | }, 53 | "ink": { 54 | "versions": { 55 | "*": "consumeInk" 56 | } 57 | }, 58 | "ftp-remote.getCurrentServerConfig": { 59 | "versions": { 60 | "0.1.0": "consumeGetServerConfig" 61 | } 62 | }, 63 | "ftp-remote.getCurrentServerName": { 64 | "versions": { 65 | "0.1.0": "consumeGetServerName" 66 | } 67 | }, 68 | "datatip": { 69 | "versions": { 70 | "0.1.0": "consumeDatatip" 71 | } 72 | } 73 | }, 74 | "providedServices": { 75 | "autocomplete.provider": { 76 | "versions": { 77 | "4.0.0": "provideAutoComplete" 78 | } 79 | }, 80 | "julia-client": { 81 | "description": "Run a Julia process", 82 | "versions": { 83 | "0.1.0": "provideClient" 84 | } 85 | }, 86 | "hyperclick": { 87 | "versions": { 88 | "0.1.0": "provideHyperclick" 89 | } 90 | } 91 | }, 92 | "uriHandler": { 93 | "method": "handleURI", 94 | "deferActivation": false 95 | }, 96 | "package-deps": [ 97 | "ink", 98 | "language-julia" 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /lib/ui/highlighter.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore-plus' 2 | 3 | # Implementation identical to https://github.com/atom/highlights/blob/master/src/highlights.coffee, 4 | # but uses an externally provided grammar. 5 | module.exports = 6 | # Highlights some `text` according to the specified `grammar`. 7 | highlight: (text, grammar, {scopePrefix, block}={}) -> 8 | scopePrefix ?= '' 9 | block ?= false 10 | lineTokens = grammar.tokenizeLines(text) 11 | 12 | # Remove trailing newline 13 | if lineTokens.length > 0 14 | lastLineTokens = lineTokens[lineTokens.length - 1] 15 | 16 | if lastLineTokens.length is 1 and lastLineTokens[0].value is '' 17 | lineTokens.pop() 18 | 19 | html = '' 20 | for tokens in lineTokens 21 | scopeStack = [] 22 | html += "<#{if block then "div" else "span"} class=\"line\">" 23 | for {value, scopes} in tokens 24 | value = ' ' unless value 25 | html = @updateScopeStack(scopeStack, scopes, html, scopePrefix) 26 | html += "#{@escapeString(value)}" 27 | html = @popScope(scopeStack, html) while scopeStack.length > 0 28 | html += "" 29 | html += '' 30 | html 31 | 32 | escapeString: (string) -> 33 | string.replace /[&"'<> ]/g, (match) -> 34 | switch match 35 | when '&' then '&' 36 | when '"' then '"' 37 | when "'" then ''' 38 | when '<' then '<' 39 | when '>' then '>' 40 | when ' ' then ' ' 41 | else match 42 | 43 | updateScopeStack: (scopeStack, desiredScopes, html, scopePrefix) -> 44 | excessScopes = scopeStack.length - desiredScopes.length 45 | if excessScopes > 0 46 | html = @popScope(scopeStack, html) while excessScopes-- 47 | 48 | # pop until common prefix 49 | for i in [scopeStack.length..0] 50 | break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) 51 | html = @popScope(scopeStack, html) 52 | 53 | # push on top of common prefix until scopeStack is desiredScopes 54 | for j in [i...desiredScopes.length] 55 | html = @pushScope(scopeStack, desiredScopes[j], html, scopePrefix) 56 | 57 | html 58 | 59 | pushScope: (scopeStack, scope, html, scopePrefix) -> 60 | scopeStack.push(scope) 61 | className = scopePrefix + scope.replace(/\.+/g, " #{scopePrefix}") 62 | html += "" 63 | 64 | popScope: (scopeStack, html) -> 65 | scopeStack.pop() 66 | html += '' 67 | -------------------------------------------------------------------------------- /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 | [Julia discussion forum](https://discourse.julialang.org) under the "usage" category with "juno" tag, 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](https://discourse.julialang.org) under the "development" category with "juno" tag – 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 | -------------------------------------------------------------------------------- /release-notes/0.12.6.md: -------------------------------------------------------------------------------- 1 | ## Juno 0.12.6 – Release Note 2 | 3 | ### Improvements, Updates 4 | 5 | - set more Documneter.jl-like admonition colors: 6 | * ![admonition-showcase](https://user-images.githubusercontent.com/40514306/93668988-8b43c180-facb-11ea-8e39-06101b94f677.png) 7 | - Juno's inline evaluation and integrated REPL now work in [soft global scope](https://julialang.org/blog/2020/08/julia-1.5-highlights/#the_return_of_quotsoft_scopequot_in_the_repl): [Atom.jl#351](https://github.com/JunoLab/Atom.jl/pull/351) 8 | * note that the behavior of `julia-client: run-all` hasn't changed, same as Julia's toplevel file execution 9 | - update to JuliaInterpreter@0.8 (, which is requirement for Revise@3.0): [Atom.jl#349](https://github.com/JunoLab/Atom.jl/pull/349) 10 | - update to JuliaFormatter: [Atom.jl#345](https://github.com/JunoLab/Atom.jl/pull/345), [Atom.jl#352](https://github.com/JunoLab/Atom.jl/pull/352) 11 | - update to latest SnoopCompile: [Atom.jl#342](https://github.com/JunoLab/Atom.jl/pull/342), [Atom.jl#349](https://github.com/JunoLab/Atom.jl/pull/349) 12 | 13 | ### Bugfixes 14 | 15 | - fix package completions: [Atom.jl#350](https://github.com/JunoLab/Atom.jl/pull/350) 16 | * `using ` will offer suggestions of packages installed in your active environment 17 | * ![package-completion-showcase](https://user-images.githubusercontent.com/40514306/93669046-fbeade00-facb-11ea-89d6-156ce23ae4b4.png) 18 | - fix module detections of files within `Core.Compiler` module: [Atom.jl#347](https://github.com/JunoLab/Atom.jl/pull/347) 19 | - `julia-client: goto-symbol` now works if a current module isn't loaded into the REPL: [Atom.jl#346](https://github.com/JunoLab/Atom.jl/pull/346) 20 | - fix debug stepper display: [Atom.jl#343](https://github.com/JunoLab/Atom.jl/pull/343) 21 | - fix and improve closure rendering: [Atom.jl#340](https://github.com/JunoLab/Atom.jl/pull/340), thanks [@yha](https://github.com/yha) ! 22 | - hides scroll bar in inline result view: [atom-ink#279](https://github.com/JunoLab/atom-ink/pull/279) 23 | - whitelist ctrl-s in integrated REPL so that https://unix.stackexchange.com/questions/12107/how-to-unfreeze-after-accidentally-pressing-ctrl-s-in-a-terminal won't happen: [atom-julia-client#764](https://github.com/JunoLab/atom-julia-client/pull/764) 24 | - fix relative julia path search: [atom-julia-client#749](https://github.com/JunoLab/atom-julia-client/pull/749) 25 | 26 | ## Versions 27 | 28 | Here're the latest versions of Juno packages. 29 | 30 | Julia packages: 31 | * Atom.jl version: 0.12.23 32 | * Juno.jl version: 0.8.4 33 | 34 | Atom packages: 35 | * julia-client version: 0.12.6 36 | * ink version: 0.12.5 37 | -------------------------------------------------------------------------------- /release-notes/0.12.5.md: -------------------------------------------------------------------------------- 1 | 2 | ## Release notes 3 | 4 | ### Features 5 | - Fuzzy completions ([#308](https://github.com/JunoLab/Atom.jl/pull/308) and [#713](https://github.com/JunoLab/atom-julia-client/pull/713)): 6 | ![image|337x441](https://aws1.discourse-cdn.com/business5/uploads/julialang/original/2X/b/b6e7d7a4feb29564356d3e67d1f260a936aa5b74.png) 7 | - Juno now displays the currently active environment in the toolbar ([#330](https://github.com/JunoLab/Atom.jl/pull/330) and [#741](https://github.com/JunoLab/atom-julia-client/pull/741)). 8 | 9 | ### Improvements 10 | 11 | - Improved functor display ([#305](https://github.com/JunoLab/Atom.jl/pull/305) and [#303](https://github.com/JunoLab/Atom.jl/pull/303)). 12 | - Improved `missing` display ([#306](https://github.com/JunoLab/Atom.jl/pull/306)). 13 | - Traceur.jl integration is now lazy-loaded ([#309](https://github.com/JunoLab/Atom.jl/pull/309)). 14 | - Better error handling when retrieving an object's documentation fails ([#310](https://github.com/JunoLab/Atom.jl/pull/310)). 15 | - Better regex for matching paths in the REPL ([#734](https://github.com/JunoLab/atom-julia-client/pull/734), [#731](https://github.com/JunoLab/atom-julia-client/pull/731), [#320](https://github.com/JunoLab/Atom.jl/pull/320), and [#322](https://github.com/JunoLab/Atom.jl/pull/322) -- thanks [**@FAlobaid**](https://github.com/FAlobaid)). 16 | - Juno now supports a `.JuliaFormatter.toml` instead of configuration settings ([#325](https://github.com/JunoLab/Atom.jl/pull/325) and [#735](https://github.com/JunoLab/atom-julia-client/pull/735)). 17 | - SVGs in the plot pane are now zoom- and panable ([#327](https://github.com/JunoLab/Atom.jl/pull/327)). 18 | - Improvements to the `evalsimple` handler ([#333](https://github.com/JunoLab/Atom.jl/pull/333)). 19 | - Allow Julia paths relative to the Atom install directory ([#711](https://github.com/JunoLab/atom-julia-client/pull/711)). 20 | - We're now showing release notes when Juno is updated ([#725](https://github.com/JunoLab/atom-julia-client/pull/725)). 21 | - Upgrade xterm.js to 4.6.0 ([#276](https://github.com/JunoLab/atom-ink/pull/276)). 22 | 23 | ### Bugfixes 24 | - Fix plot display with "Show in REPL" result style ([#312](https://github.com/JunoLab/Atom.jl/pull/312)). 25 | - Make sure to run all user code in `invokelatest` ([#313](https://github.com/JunoLab/Atom.jl/pull/313)). 26 | - Fixed one-element tuple display ([#323](https://github.com/JunoLab/Atom.jl/pull/323)). 27 | - Fixed a bug on x86 systems ([#314](https://github.com/JunoLab/Atom.jl/pull/314)). 28 | - Fixed a bug where `include` statements resolved to the wrong paths when debugging ([#329](https://github.com/JunoLab/Atom.jl/pull/329)). 29 | -------------------------------------------------------------------------------- /menus/julia-client.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | 'atom-text-editor[data-grammar="source julia"]': [ 3 | {type: 'separator'} 4 | { 5 | label: 'Juno', 6 | submenu: [ 7 | {label: 'Run Block', command: 'julia-client:run-block'} 8 | {label: 'Select Block', command: 'julia-client:select-block'} 9 | {type: 'separator'} 10 | {label: 'Go to Definition', command: 'julia-client:goto-symbol'} 11 | {label: 'Show Documentation', command: 'julia-client:show-documentation'} 12 | {label: 'Format Code', command: 'julia-client:format-code'} 13 | {type: 'separator'} 14 | {label: 'Debug: Run Block', command: 'julia-debug:run-block'} 15 | {label: 'Debug: Step through Block', command: 'julia-debug:step-through-block'} 16 | {label: 'Toggle Breakpoint', command: 'julia-debug:toggle-breakpoint'} 17 | {label: 'Toggle Conditional Breakpoint', command: 'julia-debug:toggle-conditional-breakpoint'} 18 | {type: 'separator'} 19 | {label: 'Work in Current Folder', command: 'julia-client:work-in-current-folder'} 20 | {label: 'Activate Environment in Current Folder', command: 'julia-client:activate-environment-in-current-folder'} 21 | {label: 'Activate Environment in Parent Folder', command: 'julia-client:activate-environment-in-parent-folder'} 22 | {label: 'Set Working Environment', command: 'julia-client:set-working-environment'} 23 | {label: 'Set Working Module', command: 'julia-client:set-working-module'} 24 | ] 25 | } 26 | {type: 'separator'} 27 | ] 28 | 29 | '.tree-view li.directory': [ 30 | {type: 'separator'} 31 | { 32 | label: 'Juno', 33 | submenu: [ 34 | { label: 'Work in Folder', command: 'julia-client:work-in-current-folder' } 35 | { label: 'Activate Environment in Folder', command: 'julia-client:activate-environment-in-current-folder' } 36 | { label: 'Activate Environment in Parent Folder', command: 'julia-client:activate-environment-in-parent-folder' } 37 | { label: 'Add Package Folder...', command: 'julia:open-package-as-project-folder' } 38 | { label: 'New Terminal from Folder', command: 'julia-client:new-terminal-from-current-folder' } 39 | ] 40 | } 41 | {type: 'separator'} 42 | ] 43 | 44 | '.tree-view li.file': [ 45 | {type: 'separator'} 46 | { 47 | label: 'Juno', 48 | submenu: [ 49 | { label: 'Run All', command: 'julia-client:run-all' } 50 | { label: 'Debug: Run File', command: 'julia-debug:run-file' } 51 | { label: 'Debug: Step through File', command: 'julia-debug:step-through-file' } 52 | ] 53 | } 54 | {type: 'separator'} 55 | ] 56 | -------------------------------------------------------------------------------- /lib/connection/local.coffee: -------------------------------------------------------------------------------- 1 | {paths} = require '../misc' 2 | messages = require './messages' 3 | client = require './client' 4 | 5 | junorc = client.import 'junorc', false 6 | 7 | cycler = require './process/cycler' 8 | ssh = require './process/remote' 9 | basic = require './process/basic' 10 | 11 | module.exports = 12 | consumeGetServerConfig: (getconf) -> 13 | ssh.consumeGetServerConfig(getconf) 14 | 15 | consumeGetServerName: (name) -> 16 | ssh.consumeGetServerName(name) 17 | 18 | provider: (p) -> 19 | bootMode = undefined 20 | if p? 21 | bootMode = p 22 | else 23 | bootMode = atom.config.get('julia-client.juliaOptions.bootMode') 24 | switch bootMode 25 | when 'Cycler' then cycler 26 | when 'Remote' then ssh 27 | when 'Basic' then basic 28 | 29 | activate: -> 30 | if process.platform == 'win32' 31 | process.env.JULIA_EDITOR = "\"#{process.execPath}\" #{if atom.devMode then '-d' else ''} -a" 32 | else 33 | process.env.JULIA_EDITOR = "atom #{if atom.devMode then '-d' else ''} -a" 34 | 35 | paths.getVersion() 36 | .then => 37 | @provider().start? paths.jlpath(), client.clargs() 38 | .catch -> 39 | 40 | monitor: (proc) -> 41 | client.emitter.emit('boot', proc) 42 | proc.ready = -> false 43 | client.attach(proc) 44 | return proc 45 | 46 | connect: (proc, sock) -> 47 | proc.message = (m) -> sock.write JSON.stringify m 48 | client.readStream sock 49 | sock.on 'end', -> 50 | proc.kill() 51 | client.detach() 52 | sock.on 'error', -> 53 | proc.kill() 54 | client.detach() 55 | proc.ready = -> true 56 | client.flush() 57 | proc 58 | 59 | start: (provider) -> 60 | [path, args] = [paths.jlpath(), client.clargs()] 61 | check = paths.getVersion() 62 | 63 | if provider is 'Remote' 64 | check = Promise.resolve() 65 | else 66 | check.catch (err) => 67 | messages.jlNotFound paths.jlpath(), err 68 | 69 | proc = check 70 | .then => @spawnJulia(path, args, provider) 71 | .then (proc) => @monitor(proc) 72 | 73 | # set working directory here, so we queue this task before anything else 74 | if provider is 'Remote' 75 | ssh.withRemoteConfig((conf) -> junorc conf.remote).catch -> 76 | else 77 | paths.projectDir().then (dir) -> junorc dir 78 | 79 | proc 80 | .then (proc) => 81 | Promise.all [proc, proc.socket] 82 | .then ([proc, sock]) => 83 | @connect proc, sock 84 | .catch (e) -> 85 | client.detach() 86 | console.error("Julia exited with #{e}.") 87 | proc 88 | 89 | spawnJulia: (path, args, provider) -> 90 | @provider(provider).get(path, args) 91 | -------------------------------------------------------------------------------- /keymaps/julia-client.cson.ctrl: -------------------------------------------------------------------------------- 1 | ### 2 | @NOTE 3 | It's best not to override default Atom keybindings if possible, and then 4 | register only in Julia-scoped places (e.g. Julia-syntax buffer, console) 5 | Any global commands should either be non-default or, ideally, prefixed with `C-J`. 6 | ### 7 | 8 | # Debug operations 9 | '.platform-win32 atom-text-editor[data-grammar="source julia"]:not(.mini), 10 | .platform-linux atom-text-editor[data-grammar="source julia"]:not(.mini), 11 | ink-terminal.julia-terminal, 12 | .ink-debugger-container': 13 | 'f5': 'julia-debug:run-file' 14 | 'ctrl-f5': 'julia-debug:step-through-file' 15 | 'shift-f5': 'julia-debug:stop-debugging' 16 | 'f8': 'julia-debug:continue' 17 | 'shift-f8': 'julia-debug:step-to-selected-line' 18 | 'f9': 'julia-debug:toggle-breakpoint' 19 | 'shift-f9': 'julia-debug:toggle-conditional-breakpoint' 20 | 'f10': 'julia-debug:step-to-next-expression' 21 | 'shift-f10': 'julia-debug:step-to-next-line' 22 | 'f11': 'julia-debug:step-into' 23 | 'shift-f11': 'julia-debug:step-out' 24 | 25 | # Julia atom-text-editor 26 | '.platform-win32 atom-text-editor[data-grammar="source julia"], 27 | .platform-linux atom-text-editor[data-grammar="source julia"]': 28 | 'ctrl-enter': 'julia-client:run-block' 29 | 'shift-enter': 'julia-client:run-and-move' 30 | 'ctrl-shift-enter': 'julia-client:run-all' 31 | 'alt-enter': 'julia-client:run-cell' 32 | 'alt-shift-enter': 'julia-client:run-cell-and-move' 33 | 'alt-down': 'julia-client:next-cell' 34 | 'alt-up': 'julia-client:prev-cell' 35 | 'ctrl-j ctrl-g': 'julia-client:goto-symbol' 36 | 'ctrl-j ctrl-d': 'julia-client:show-documentation' 37 | 'ctrl-j ctrl-m': 'julia-client:set-working-module' 38 | 'ctrl-j ctrl-f': 'julia-client:format-code' 39 | 'ctrl-shift-c': 'julia-client:interrupt-julia' 40 | 41 | # Julia REPL 42 | '.platform-win32 .julia-terminal, 43 | .platform-linux .julia-terminal': 44 | 'ctrl-j ctrl-m': 'julia-client:set-working-module' 45 | 'ctrl-c': 'julia-client:copy-or-interrupt' 46 | 'ctrl-shift-c': 'julia-client:copy-or-interrupt' 47 | 'ctrl-v': 'ink-terminal:paste' 48 | 49 | # atom-workspace 50 | '.platform-win32 atom-workspace, 51 | .platform-linux atom-workspace': 52 | 'ctrl-j ctrl-r': 'julia-client:open-external-REPL' 53 | 'ctrl-j ctrl-o': 'julia-client:open-REPL' 54 | 'ctrl-j ctrl-c': 'julia-client:clear-REPL' 55 | 'ctrl-j ctrl-s': 'julia-client:start-julia' 56 | 'ctrl-j ctrl-k': 'julia-client:kill-julia' 57 | 'ctrl-j ctrl-p': 'julia-client:open-plot-pane' 58 | 'ctrl-j ctrl-w': 'julia-client:open-workspace' 59 | 'ctrl-j ctrl-,': 'julia-client:settings' 60 | 'ctrl-j ctrl-e': 'julia-client:focus-last-editor' 61 | 'ctrl-j ctrl-t': 'julia-client:focus-last-terminal' 62 | 'ctrl-j ctrl-b': 'julia-client:return-from-goto' 63 | -------------------------------------------------------------------------------- /lib/ui/focusutils.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import {TextEditor, CompositeDisposable} from 'atom' 4 | 5 | let lastEditor 6 | let lastTerminal 7 | let subs 8 | 9 | class FocusHistory { 10 | constructor (size) { 11 | this.size = size 12 | this.history = [] 13 | this.openedItem = undefined 14 | } 15 | 16 | push (item) { 17 | if (this.openedItem && 18 | this.openedItem.file && 19 | this.openedItem.line && 20 | item.file == this.openedItem.file && 21 | item.line == this.openedItem.line) { 22 | return 23 | } 24 | 25 | this.history.push(item) 26 | while (this.history.length > this.size) { 27 | this.history.shift() 28 | } 29 | return 30 | } 31 | 32 | moveBack () { 33 | const item = this.history.pop() 34 | if (item && item.open) { 35 | const activeItem = atom.workspace.getActivePaneItem() 36 | if (activeItem instanceof TextEditor) { 37 | const file = activeItem.getPath() || 'untitled-' + activeItem.buffer.getId() 38 | const line = activeItem.getCursorBufferPosition().row 39 | this.openedItem = {file, line} 40 | } 41 | item.open() 42 | } 43 | } 44 | } 45 | 46 | export function activate (ink) { 47 | subs = new CompositeDisposable() 48 | 49 | subs.add( 50 | atom.workspace.onDidStopChangingActivePaneItem(item => { 51 | if (item instanceof TextEditor) { 52 | lastEditor = item 53 | } else if (item instanceof ink.InkTerminal) { 54 | lastTerminal = item 55 | } 56 | }), 57 | atom.packages.onDidActivateInitialPackages(() => { 58 | lastEditor = atom.workspace.getActiveTextEditor() 59 | atom.workspace.getPanes().forEach(pane => { 60 | const item = pane.getActiveItem() 61 | if (item instanceof ink.InkTerminal) { 62 | lastTerminal = item 63 | } 64 | }) 65 | }) 66 | ) 67 | 68 | const history = new FocusHistory(30) 69 | ink.Opener.onDidOpen(({newLocation, oldLocation}) => { 70 | if (oldLocation) history.push(oldLocation) 71 | }) 72 | 73 | subs.add(atom.commands.add('atom-workspace', { 74 | 'julia-client:focus-last-editor': () => focusLastEditor(), 75 | 'julia-client:focus-last-terminal': () => focusLastTerminal(), 76 | 'julia-client:return-from-goto': () => history.moveBack() 77 | })) 78 | } 79 | 80 | export function deactivate () { 81 | lastEditor = null 82 | lastTerminal = null 83 | subs.dispose() 84 | subs = null 85 | } 86 | 87 | function focusLastEditor () { 88 | const pane = atom.workspace.paneForItem(lastEditor) 89 | if (pane) { 90 | pane.activate() 91 | pane.activateItem(lastEditor) 92 | } 93 | } 94 | 95 | function focusLastTerminal () { 96 | if (lastTerminal && lastTerminal.open) lastTerminal.open() 97 | } 98 | -------------------------------------------------------------------------------- /lib/package/settings.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | let validSchemes = require('../package/config') 4 | let invalidSchemes = [] // Keeps invalid config schemes to be notified to users 5 | 6 | function dispose() { 7 | validSchemes = null 8 | invalidSchemes = null 9 | } 10 | 11 | /** 12 | * Updates settings by removing deprecated (i.e.: not used anymore) configs so that no one tries to 13 | * tweak them. 14 | */ 15 | export function updateSettings() { 16 | const currentConfig = atom.config.get('julia-client') 17 | searchForDeprecated(currentConfig, []) 18 | 19 | if (invalidSchemes.length > 0) { 20 | const message = atom.notifications.addWarning('Julia-Client: Invalid (deprecated) settings found', { 21 | detail: invalidSchemes.join('\n'), 22 | dismissable: true, 23 | description: 'Remove these invalid settings ?', 24 | buttons: [ 25 | { 26 | text: 'Yes', 27 | onDidClick: () => { 28 | message.dismiss() 29 | invalidSchemes.forEach((invalidScheme) => { 30 | atom.config.unset(invalidScheme) 31 | }) 32 | dispose() 33 | } 34 | }, 35 | { 36 | text: 'No', 37 | onDidClick: () => { 38 | message.dismiss() 39 | dispose() 40 | } 41 | } 42 | ] 43 | }) 44 | } 45 | } 46 | 47 | /** 48 | * Recursively search deprecated configs 49 | */ 50 | function searchForDeprecated(config, currentSchemes) { 51 | Object.entries(config).forEach(([key, value]) => { 52 | // @NOTE: Traverse the current config schemes by post-order in order to push all the invalid 53 | // config schemes into `invalidSchemes` 54 | if (Object.prototype.toString.call(value) === '[object Object]') { 55 | const nextSchemes = currentSchemes.slice(0) 56 | nextSchemes.push(key) 57 | searchForDeprecated(value, nextSchemes) 58 | } 59 | 60 | // Make `validScheme` corresponding to `currentSchemes` path for the validity checking below 61 | let validScheme = validSchemes 62 | currentSchemes.forEach((scheme) => { 63 | Object.entries(validScheme).forEach(([_key, _value]) => { 64 | if (_key === scheme) { 65 | validScheme = _value 66 | } else if (_key === 'properties' && _value[scheme]) { 67 | validScheme = _value[scheme] 68 | } 69 | }) 70 | }); 71 | 72 | // Check if the `config` scheme being searched at this recursion is in `validScheme` 73 | if (!validScheme[key] && (!validScheme.properties || !validScheme.properties[key])) { 74 | let invalidScheme = 'julia-client.' 75 | invalidScheme += currentSchemes.length === 0 ? '' : `${currentSchemes.join('.')}.` 76 | invalidScheme += key 77 | invalidSchemes.push(invalidScheme) 78 | } 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /lib/runtime.coffee: -------------------------------------------------------------------------------- 1 | { CompositeDisposable, Disposable } = require 'atom' 2 | 3 | module.exports = 4 | modules: require './runtime/modules' 5 | environments: require './runtime/environments' 6 | evaluation: require './runtime/evaluation' 7 | console: require './runtime/console' 8 | completions: require './runtime/completions' 9 | workspace: require './runtime/workspace' 10 | plots: require './runtime/plots' 11 | frontend: require './runtime/frontend' 12 | debugger: require './runtime/debugger' 13 | profiler: require './runtime/profiler' 14 | outline: require './runtime/outline' 15 | linter: require './runtime/linter' 16 | packages: require './runtime/packages' 17 | debuginfo: require './runtime/debuginfo' 18 | formatter: require './runtime/formatter' 19 | goto: require './runtime/goto' 20 | 21 | activate: -> 22 | @subs = new CompositeDisposable() 23 | 24 | @modules.activate() 25 | @completions.activate() 26 | @subs.add atom.config.observe 'julia-client.juliaOptions.formatOnSave', (val) => 27 | if val 28 | @formatter.activate() 29 | else 30 | @formatter.deactivate() 31 | 32 | @subs.add new Disposable(=> 33 | mod.deactivate() for mod in [@modules, @completions, @formatter]) 34 | 35 | deactivate: -> 36 | @subs.dispose() 37 | 38 | consumeInk: (ink) -> 39 | @evaluation.ink = ink 40 | for mod in [@console, @debugger, @profiler, @linter, @goto, @outline, @frontend] 41 | mod.activate(ink) 42 | for mod in [@workspace, @plots] 43 | mod.ink = ink 44 | mod.activate() 45 | @subs.add new Disposable => 46 | mod.deactivate() for mod in [@console, @debugger, @profiler, @linter, @goto, @outline] 47 | @environments.consumeInk(ink) 48 | 49 | provideAutoComplete: -> @completions 50 | 51 | provideHyperclick: -> @goto.provideHyperclick() 52 | 53 | consumeStatusBar: (bar) -> 54 | m = @modules.consumeStatusBar bar 55 | e = @environments.consumeStatusBar bar 56 | d = new Disposable => 57 | m.dispose() 58 | e.dispose() 59 | @subs.add d 60 | return d 61 | 62 | consumeDatatip: (datatipService) -> 63 | datatipProvider = require './runtime/datatip' 64 | # @NOTE: Check if the service is passed by Atom-IDE-UI's datatip service: 65 | # currently atom-ide-datatip can't render code snippets correctly. 66 | if datatipService.constructor.name == 'DatatipManager' 67 | datatipProvider.useAtomIDEUI = true 68 | else 69 | # @NOTE: Overwrite the weird default config settings of atom-ide-datatip 70 | atom.config.set 'atom-ide-datatip', 71 | showDataTipOnCursorMove: false 72 | showDataTipOnMouseMove: true 73 | datatipDisposable = datatipService.addProvider(datatipProvider) 74 | @subs.add(datatipDisposable) 75 | datatipDisposable 76 | 77 | handleURI: require './runtime/urihandler' 78 | -------------------------------------------------------------------------------- /lib/connection/process/cycler.coffee: -------------------------------------------------------------------------------- 1 | {isEqual} = require 'underscore-plus' 2 | hash = require 'object-hash' 3 | basic = require './basic' 4 | 5 | IPC = require '../ipc' 6 | 7 | module.exports = 8 | provider: -> 9 | basic 10 | 11 | cacheLength: 1 12 | 13 | procs: {} 14 | 15 | key: (path, args) -> hash([path, args...].join(' ').trim()) 16 | 17 | cache: (path, args) -> @procs[@key(path, args)] ?= [] 18 | 19 | removeFromCache: (path, args, obj) -> 20 | key = @key path, args 21 | @procs[key] = @procs[key].filter (x) -> x != obj 22 | 23 | toCache: (path, args, proc) -> 24 | proc.cached = true 25 | @cache(path, args).push proc 26 | 27 | fromCache: (path, args) -> 28 | ps = @cache path, args 29 | p = ps.shift() 30 | return unless p? 31 | p.cached = false 32 | p.init.then => 33 | @start path, args 34 | p.proc 35 | 36 | start: (path, args) -> 37 | allArgs = [args, atom.config.get('julia-client.juliaOptions')] 38 | @provider().lock (release) => 39 | if @cache(path, allArgs).length < @cacheLength 40 | p = @provider().get_(path, args).then (proc) => 41 | obj = {path, allArgs, proc: proc} 42 | @monitor proc 43 | @warmup obj 44 | @toCache path, allArgs, obj 45 | proc.socket 46 | .then => @start path, allArgs 47 | .catch (e) => @removeFromCache path, allArgs, obj 48 | release proc.socket 49 | p.catch (err) => 50 | release() 51 | else 52 | release() 53 | return 54 | 55 | flush: (events, out, err) -> 56 | for {type, data} in events 57 | (if type == 'stdout' then out else err) data 58 | 59 | monitor: (proc) -> 60 | proc.events = [] 61 | proc.wasCached = true 62 | proc.onStdout (data) -> proc.events?.push {type: 'stdout', data} 63 | proc.onStderr (data) -> proc.events?.push {type: 'stderr', data} 64 | proc.flush = (out, err) => 65 | @flush proc.events, out, err 66 | delete proc.events 67 | 68 | boot: (ipc) -> ipc.rpc 'ping' 69 | repl: (ipc) -> ipc.rpc 'changemodule', {mod: 'Main'} 70 | 71 | warmup: (obj) -> 72 | obj.init = Promise.resolve() 73 | obj.proc.socket 74 | .then (sock) => 75 | return unless obj.cached 76 | ipc = new IPC sock 77 | [@boot, @repl].forEach (f) -> 78 | obj.init = obj.init.then -> 79 | if obj.cached then f ipc 80 | obj.init = obj.init 81 | .catch (err) -> console.warn 'julia warmup error:', err 82 | .then -> ipc.unreadStream() 83 | return 84 | .catch -> 85 | 86 | get: (path, args) -> 87 | allArgs = [args, atom.config.get('julia-client.juliaOptions')] 88 | if (proc = @fromCache path, allArgs) then p = proc 89 | else p = @provider().get path, args 90 | @start path, args 91 | p 92 | 93 | reset: -> 94 | for key, ps of @procs 95 | for p in ps 96 | p.proc.kill() 97 | -------------------------------------------------------------------------------- /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 to get things working, or anything seems out of date, broken, or just plain confusing, please do let us know so we can fix it. Even better, file a PR! 10 | 11 | # Developer Install 12 | 13 | Julia support in Atom consists of a number of packages for both Julia and Atom: 14 | 15 | * [language-julia](https://github.com/JuliaLang/atom-language-julia) – Provides basic 16 | language support for Atom, e.g. syntax highlighting. 17 | * [ink](https://github.com/JunoLab/atom-ink) – Provides generic UI components for building 18 | IDEs in Atom (e.g. the console lives here). 19 | * [CodeTools.jl](http://github.com/JunoLab/CodeTools.jl) – provides backend editor support 20 | for Julia, e.g. autocompletion and evaluation. 21 | * [Atom.jl](http://github.com/JunoLab/Atom.jl) and 22 | [julia-client](http://github.com/JunoLab/atom-julia-client) – these packages tie everything 23 | together. julia-client boots Julia from inside Atom, then communicates with the Atom.jl 24 | package to provide e.g. autocompletion and evaluation within the editor. 25 | 26 | You can install *language-julia* by using Atom's `Install Packages And Themes` command and searching for it. The Julia packages, *Atom.jl* and *CodeTools.jl*, can be installed via 27 | 28 | ```julia 29 | Pkg.clone("http://github.com/JunoLab/Atom.jl") 30 | Pkg.clone("http://github.com/JunoLab/CodeTools.jl") 31 | ``` 32 | 33 | If you already have these packages change `clone` to `checkout` here. 34 | 35 | To install the latest atom packages, run the commands: 36 | 37 | ```shell 38 | apm install https://github.com/JunoLab/atom-ink 39 | apm install https://github.com/JunoLab/atom-julia-client 40 | ``` 41 | 42 | It's a good idea to keep these up to date by running `Pkg.update()` in Julia and syncing the package repos every now and then, which will be in `~/.atom/packages/julia-client` and `~/.atom/packages/ink`. 43 | 44 | Atom will need to be reloaded, either by closing and reopening it or by running the `Window: Reload` command. At this point, you should find that there are a bunch of new Julia commands available in Atom – type "Julia" into the command palette to see what's available. If the `julia` command isn't on your path already, set the Julia path in the julia-client settings panel. 45 | 46 | Get started by going into a buffer set to Julia syntax, typing `2+2`, and pressing C-Enter (where C stands for Ctrl, or Cmd on OS X). After the client boots you should see the result pop up next to the text. You can also work in the Atom REPL by pressing C-J C-O – just type in the input box and Shift-Enter to evaluate. 47 | -------------------------------------------------------------------------------- /lib/package/toolbar.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | consumeToolBar: (bar) -> 3 | return unless atom.config.get 'julia-client.uiOptions.enableToolBar' 4 | 5 | @bar = bar 'julia-client' 6 | 7 | # Files & Folders 8 | 9 | @bar.addButton 10 | icon: 'file-code' 11 | iconset: 'fa' 12 | tooltip: 'New Julia File' 13 | callback: 'julia:new-julia-file' 14 | 15 | @bar.addButton 16 | icon: 'save' 17 | iconset: 'fa' 18 | tooltip: 'Save' 19 | callback: 'core:save' 20 | 21 | @bar.addButton 22 | icon: 'folder-open' 23 | iconset: 'fa' 24 | tooltip: 'Open File...' 25 | callback: 'application:open-file' 26 | 27 | # Julia process 28 | 29 | @bar.addSpacer() 30 | 31 | @bar.addButton 32 | icon: 'globe' 33 | tooltip: 'Start Local Julia Process' 34 | callback: 'julia-client:start-julia' 35 | 36 | @bar.addButton 37 | iconset: 'ion' 38 | icon: 'md-planet' 39 | tooltip: 'Start Remote Julia Process' 40 | callback: 'julia-client:start-remote-julia-process' 41 | 42 | @bar.addButton 43 | icon: 'md-pause' 44 | iconset: 'ion' 45 | tooltip: 'Interrupt Julia' 46 | callback: 'julia-client:interrupt-julia' 47 | 48 | @bar.addButton 49 | icon: 'md-square' 50 | iconset: 'ion' 51 | tooltip: 'Stop Julia' 52 | callback: 'julia-client:kill-julia' 53 | 54 | # Evaluation 55 | 56 | @bar.addSpacer() 57 | 58 | @bar.addButton 59 | icon: 'zap' 60 | tooltip: 'Run Block' 61 | callback: 'julia-client:run-and-move' 62 | 63 | @bar.addButton 64 | icon: 'md-play' 65 | iconset: 'ion' 66 | tooltip: 'Run All' 67 | callback: 'julia-client:run-all' 68 | 69 | @bar.addButton 70 | icon: 'format-float-none' 71 | iconset: 'mdi' 72 | tooltip: 'Format Code' 73 | callback: 'julia-client:format-code' 74 | 75 | # Windows & Panes 76 | 77 | @bar.addSpacer() 78 | 79 | @bar.addButton 80 | icon: 'terminal' 81 | tooltip: 'Show REPL' 82 | callback: 'julia-client:open-REPL' 83 | 84 | @bar.addButton 85 | icon: 'book' 86 | tooltip: 'Show Workspace' 87 | callback: 'julia-client:open-workspace' 88 | 89 | @bar.addButton 90 | icon: 'list-unordered' 91 | tooltip: 'Show Outline' 92 | callback: 'julia-client:open-outline-pane' 93 | 94 | @bar.addButton 95 | icon: 'info' 96 | tooltip: 'Show Documentation Browser' 97 | callback: 'julia-client:open-documentation-browser' 98 | 99 | @bar.addButton 100 | icon: 'graph' 101 | tooltip: 'Show Plot Pane' 102 | callback: 'julia-client:open-plot-pane' 103 | 104 | @bar.addButton 105 | icon: 'bug' 106 | tooltip: 'Show Debugger Pane' 107 | callback: 'julia-debug:open-debugger-pane' 108 | 109 | deactivate: -> 110 | @bar?.removeItems() 111 | -------------------------------------------------------------------------------- /lib/connection/ipc.coffee: -------------------------------------------------------------------------------- 1 | Loading = null 2 | lwaits = [] 3 | withLoading = (f) -> if Loading? then f() else lwaits.push f 4 | 5 | {bufferLines} = require '../misc' 6 | 7 | module.exports = 8 | class IPC 9 | 10 | @consumeInk: (ink) -> 11 | Loading = ink.Loading 12 | f() for f in lwaits 13 | 14 | constructor: (stream) -> 15 | withLoading => 16 | @loading = new Loading 17 | @handlers = {} 18 | @callbacks = {} 19 | @queue = [] 20 | @id = 0 21 | 22 | if stream? then @setStream stream 23 | 24 | @handle 25 | cb: (id, result) => 26 | @callbacks[id]?.resolve result 27 | delete @callbacks[id] 28 | 29 | cancelCallback: (id, e) => 30 | @callbacks[id].reject e 31 | 32 | handle: (type, f) -> 33 | if f? 34 | @handlers[type] = f 35 | else 36 | @handle t, f for t, f of type 37 | 38 | writeMsg: -> throw new Error 'msg not implemented' 39 | 40 | msg: (type, args...) -> @writeMsg [type, args...] 41 | 42 | rpc: (type, args...) -> 43 | p = new Promise (resolve, reject) => 44 | @id += 1 45 | @callbacks[@id] = {resolve, reject} 46 | @msg {type, callback: @id}, args... 47 | @loading?.monitor p 48 | 49 | flush: -> 50 | @writeMsg msg for msg in @queue 51 | @queue = [] 52 | 53 | reset: -> 54 | @loading?.reset() 55 | @queue = [] 56 | cb.reject 'disconnected' for id, cb of @callbacks 57 | @callbacks = {} 58 | 59 | input: ([type, args...]) -> 60 | if type.constructor == Object 61 | {type, callback} = type 62 | if @handlers.hasOwnProperty type 63 | result = Promise.resolve().then => @handlers[type] args... 64 | if callback 65 | result 66 | .then (result) => @msg 'cb', callback, result 67 | .catch (err) => 68 | console.error(err) 69 | @msg 'cancelCallback', callback, @errJson err 70 | else 71 | console.log "julia-client: unrecognised message #{type}", args 72 | 73 | import: (fs, rpc = true, mod = {}) -> 74 | return unless fs? 75 | if fs.constructor == String then return @import([fs], rpc, mod)[fs] 76 | if fs.rpc? or fs.msg? 77 | mod = {} 78 | @import fs.rpc, true, mod 79 | @import fs.msg, false, mod 80 | else 81 | fs.forEach (f) => 82 | mod[f] = (args...) => 83 | if rpc then @rpc f, args... else @msg f, args... 84 | mod 85 | 86 | isWorking: -> @loading?.isWorking() 87 | onWorking: (f) -> @loading?.onWorking f 88 | onDone: (f) -> @loading?.onDone f 89 | onceDone: (f) -> @loading?.onceDone f 90 | 91 | errJson: (obj) -> 92 | return unless obj instanceof Error 93 | {type: 'error', message: obj.message, stack: obj.stack} 94 | 95 | readStream: (s) -> 96 | s.on 'data', cb = bufferLines (m) => if m then @input JSON.parse m 97 | @unreadStream = -> s.removeListener 'data', cb 98 | 99 | writeStream: (s) -> 100 | @writeMsg = (m) -> 101 | s.write JSON.stringify m 102 | s.write '\n' 103 | 104 | setStream: (@stream) -> 105 | @readStream @stream 106 | @writeStream @stream 107 | @stream.on 'end', => @reset() 108 | -------------------------------------------------------------------------------- /lib/connection/messages.coffee: -------------------------------------------------------------------------------- 1 | client = require './client' 2 | tcp = require './process/tcp' 3 | 4 | module.exports = 5 | activate: -> 6 | 7 | client.handleBasic 'install', => 8 | @note?.dismiss() 9 | atom.notifications.addError "Error installing Atom.jl package", 10 | description: 11 | """ 12 | Go to the `Packages -> Juno -> Open REPL` menu and 13 | run `Pkg.add("Atom")` in Julia, then try again. 14 | If you still see an issue, please report it to: 15 | 16 | https://discourse.julialang.org/ 17 | """ 18 | dismissable: true 19 | 20 | client.handleBasic 'load', => 21 | @note?.dismiss() 22 | atom.notifications.addError "Error loading Atom.jl package", 23 | description: 24 | """ 25 | Go to the `Packages -> Juno -> Open REPL` menu and 26 | run `Pkg.update()` in Julia, then try again. 27 | If you still see an issue, please report it to: 28 | 29 | https://discourse.julialang.org/ 30 | """ 31 | dismissable: true 32 | 33 | client.handleBasic 'installing', => 34 | @note?.dismiss() 35 | @note = atom.notifications.addInfo "Installing Julia packages...", 36 | description: 37 | """ 38 | Julia's first run will take a couple of minutes. 39 | See the REPL below for progress. 40 | """ 41 | dismissable: true 42 | @openConsole() 43 | 44 | client.handleBasic 'precompiling', => 45 | @note?.dismiss() 46 | @note = atom.notifications.addInfo "Compiling Julia packages...", 47 | description: 48 | """ 49 | Julia's first run will take a couple of minutes. 50 | See the REPL below for progress. 51 | """ 52 | dismissable: true 53 | @openConsole() 54 | 55 | client.handle welcome: => 56 | @note?.dismiss() 57 | atom.notifications.addSuccess "Welcome to Juno!", 58 | description: 59 | """ 60 | Success! Juno is set up and ready to roll. 61 | Try entering `2+2` in the REPL below. 62 | """ 63 | dismissable: true 64 | @openConsole() 65 | 66 | openConsole: -> 67 | atom.commands.dispatch atom.views.getView(atom.workspace), 68 | 'julia-client:open-REPL' 69 | 70 | jlNotFound: (path, details = '') -> 71 | atom.notifications.addError "Julia could not be started.", 72 | description: 73 | """ 74 | We tried to launch Julia from: `#{path}` 75 | This path can be changed in the settings. 76 | """ 77 | detail: details 78 | dismissable: true 79 | 80 | connectExternal: -> 81 | tcp.listen().then (port) -> 82 | code = "using Atom; using Juno; Juno.connect(#{port})" 83 | msg = atom.notifications.addInfo "Connect an external process", 84 | description: 85 | """ 86 | To connect a Julia process running in the terminal, run the command: 87 | 88 | #{code} 89 | """ 90 | dismissable: true 91 | buttons: [{text: 'Copy', onDidClick: -> atom.clipboard.write code}] 92 | client.onceAttached -> 93 | if not msg.isDismissed() 94 | msg.dismiss() 95 | atom.notifications.addSuccess "Julia is connected." 96 | -------------------------------------------------------------------------------- /lib/misc/weave.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import 'atom' 4 | 5 | export function getCode (ed) { 6 | const text = ed.getText() 7 | const lines = text.split("\n") 8 | const N = ed.getLineCount() 9 | let code = "" 10 | 11 | for (let i = 0; i < N; i++) { 12 | let scopes = ed.scopeDescriptorForBufferPosition([i, 0]).scopes 13 | if (scopes.length > 1) { 14 | if (scopes.indexOf("source.embedded.julia") > -1) { 15 | code += lines[i] + "\n" 16 | } 17 | } 18 | } 19 | return code 20 | } 21 | 22 | function getEmbeddedScope (cursor) { 23 | let scopes = cursor.getScopeDescriptor().scopes 24 | let targetScope = null 25 | for (let scope of scopes) { 26 | if (scope.startsWith('source.embedded.')) { 27 | targetScope = scope 28 | break 29 | } 30 | } 31 | return targetScope 32 | } 33 | 34 | function getCurrentCellRange (ed, cursor) { 35 | let scope = getEmbeddedScope(cursor) 36 | if (scope === null) return null 37 | 38 | let start = cursor.getBufferRow() 39 | let end = start 40 | while (start - 1 >= 0 && 41 | ed.scopeDescriptorForBufferPosition([start - 1, 0]).scopes.indexOf(scope) > -1) { 42 | start -= 1 43 | } 44 | while (end + 1 <= ed.getLastBufferRow() && 45 | ed.scopeDescriptorForBufferPosition([end + 1, 0]).scopes.indexOf(scope) > -1) { 46 | end += 1 47 | } 48 | return [[start, 0], [end, Infinity]] 49 | } 50 | 51 | export function getCursorCellRanges (ed) { 52 | let ranges = [] 53 | for (const cursor of ed.getCursors()) { 54 | let range = getCurrentCellRange(ed, cursor) 55 | if (range !== null) { 56 | ranges.push(range) 57 | } 58 | } 59 | return ranges 60 | } 61 | 62 | export function moveNext (ed) { 63 | for (const cursor of ed.getCursors()) { 64 | let scope = getEmbeddedScope(cursor) 65 | if (scope === null) return null 66 | 67 | let range = getCurrentCellRange(ed, cursor) 68 | let endRow = range[1][0] + 1 69 | while (endRow + 1 <= ed.getLastBufferRow() && 70 | ed.scopeDescriptorForBufferPosition([endRow + 1, 0]).scopes.indexOf(scope) === -1) { 71 | endRow += 1 72 | } 73 | cursor.setBufferPosition([endRow+1, Infinity]) 74 | } 75 | } 76 | 77 | export function movePrev (ed) { 78 | for (const cursor of ed.getCursors()) { 79 | let scope = getEmbeddedScope(cursor) 80 | if (scope === null) return null 81 | 82 | let range = getCurrentCellRange(ed, cursor) 83 | let startRow = range[0][0] - 1 84 | while (startRow - 1 >= 0 && 85 | ed.scopeDescriptorForBufferPosition([startRow - 1, 0]).scopes.indexOf(scope) === -1) { 86 | startRow -= 1 87 | } 88 | cursor.setBufferPosition([startRow-1, Infinity]) 89 | } 90 | } 91 | 92 | export function get (ed) { 93 | let ranges = getCursorCellRanges(ed) 94 | if (ranges.length === 0) return [] 95 | 96 | let processedRanges = [] 97 | for (let range of ranges) { 98 | let text = ed.getTextInBufferRange(range) 99 | range[1][0] += 1 // move result one line down 100 | processedRanges.push({ 101 | range: range, 102 | selection: ed.getSelections()[0], 103 | line: range[0][0], 104 | text: text || ' ' 105 | }) 106 | } 107 | return processedRanges 108 | } 109 | -------------------------------------------------------------------------------- /lib/ui/docs.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { CompositeDisposable } from 'atom' 4 | import { client } from '../connection' 5 | const views = require('./views') 6 | import goto from '../runtime/goto' 7 | 8 | const { 9 | searchdocs: searchDocs, 10 | gotosymbol: gotoSymbol, 11 | moduleinfo: moduleInfo, 12 | regeneratedocs: regenerateDocs 13 | } = client.import({rpc: ['searchdocs', 'gotosymbol', 'moduleinfo'], msg: ['regeneratedocs']}) 14 | 15 | let ink, subs, pane 16 | 17 | export function activate(_ink) { 18 | ink = _ink 19 | 20 | pane = ink.DocPane.fromId('Documentation') 21 | 22 | pane.search = (text, mod, exportedOnly, allPackages, nameOnly) => { 23 | client.boot() 24 | return new Promise((resolve) => { 25 | searchDocs({query: text, mod, exportedOnly, allPackages, nameOnly}).then((res) => { 26 | if (!res.error) { 27 | for (let i = 0; i < res.items.length; i += 1) { 28 | res.items[i].score = res.scores[i] 29 | res.items[i] = processItem(res.items[i]) 30 | } 31 | // erase module input if the actual searched module has been changed 32 | if (res.shoulderase) { 33 | pane.modEd.setText('') 34 | } 35 | } 36 | resolve(res) 37 | }) 38 | }) 39 | } 40 | 41 | pane.regenerateCache = () => { 42 | regenerateDocs() 43 | } 44 | 45 | subs = new CompositeDisposable() 46 | subs.add(atom.commands.add('atom-workspace', 'julia-client:open-documentation-browser', open)) 47 | subs.add(atom.commands.add('atom-workspace', 'julia-client:regenerate-doc-cache', () => { 48 | regenerateDocs() 49 | })) 50 | subs.add(atom.config.observe('julia-client.uiOptions.layouts.documentation.defaultLocation', (defaultLocation) => { 51 | pane.setDefaultLocation(defaultLocation) 52 | })) 53 | } 54 | 55 | export function open () { 56 | return pane.open({ 57 | split: atom.config.get('julia-client.uiOptions.layouts.documentation.split') 58 | }) 59 | } 60 | export function ensureVisible () { 61 | return pane.ensureVisible({ 62 | split: atom.config.get('julia-client.uiOptions.layouts.documentation.split') 63 | }) 64 | } 65 | export function close () { 66 | return pane.close() 67 | } 68 | 69 | export function processItem (item) { 70 | item.html = views.render(item.html) 71 | 72 | processLinks(item.html.getElementsByTagName('a')) 73 | 74 | item.onClickName = () => { 75 | gotoSymbol({ 76 | word: item.name, 77 | mod: item.mod 78 | }).then(results => { 79 | if (results.error) return 80 | return goto.selectItemsAndGo(results.items) 81 | }) 82 | } 83 | 84 | item.onClickModule = () => { 85 | moduleInfo({mod: item.mod}).then(({doc, items}) => { 86 | items.map((x) => processItem(x)) 87 | showDocument(views.render(doc), items) 88 | }) 89 | } 90 | 91 | return item 92 | } 93 | 94 | export function processLinks (links) { 95 | for (let i = 0; i < links.length; i++) { 96 | const link = links[i] 97 | if (link.attributes['href'].value == '@ref') { 98 | links[i].onclick = () => pane.kwsearch(link.innerText) 99 | } 100 | } 101 | } 102 | 103 | export function showDocument (view, items) { 104 | pane.showDocument(view, items) 105 | } 106 | 107 | export function deactivate () { 108 | subs.dispose() 109 | } 110 | -------------------------------------------------------------------------------- /lib/runtime/environments.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import { CompositeDisposable, Disposable } from 'atom' 6 | import { client } from '../connection' 7 | import { show } from '../ui/selector' 8 | 9 | const { allProjects, activateProject } = client.import({ rpc: ['allProjects'], msg: ['activateProject'] }) 10 | 11 | let ink 12 | export function consumeInk (_ink) { 13 | ink = _ink 14 | } 15 | 16 | export function consumeStatusBar (statusBar) { 17 | const subs = new CompositeDisposable() 18 | 19 | const dom = document.createElement('a') 20 | const tileDom = document.createElement('span') // only `span` element can be hide completely 21 | tileDom.classList.add('julia', 'inline-block') 22 | tileDom.appendChild(dom) 23 | const tile = statusBar.addRightTile({ 24 | item: tileDom, 25 | priority: 10 26 | }) 27 | 28 | let projectName = '' 29 | let projectPath = '' 30 | 31 | const showTile = () => tileDom.style.display = '' 32 | const hideTile = () => tileDom.style.display = 'none' 33 | const updateTile = (proj) => { 34 | if (!proj) return hideTile() 35 | projectName = proj.name 36 | dom.innerText = 'Env: ' + projectName 37 | projectPath = proj.path 38 | showTile() 39 | } 40 | client.handle({ updateProject: updateTile }) 41 | 42 | const onClick = (event) => { 43 | if (process.platform === 'darwin' ? event.metaKey : event.ctrlKey) { 44 | if (!fs.existsSync(projectPath)) return 45 | const pending = atom.config.get('core.allowPendingPaneItems') 46 | if (ink) { 47 | ink.Opener.open(projectPath, { 48 | pending, 49 | }) 50 | } else { 51 | atom.workspace.open(projectPath, { 52 | pending, 53 | searchAllPanes: true 54 | }) 55 | } 56 | } else { 57 | chooseEnvironment() 58 | } 59 | } 60 | 61 | const modifiler = process.platform == 'darwin' ? 'Cmd' : 'Ctrl' 62 | const title = () => { 63 | return `Currently working in environment ${projectName} at ${projectPath}
` + 64 | `Click to choose an environment
` + 65 | `${modifiler}-Click to open project file` 66 | } 67 | 68 | dom.addEventListener('click', onClick) 69 | subs.add( 70 | client.onDetached(hideTile), 71 | atom.tooltips.add(dom, { title }), 72 | new Disposable(() => { 73 | dom.removeEventListener('click', onClick) 74 | tile.destroy() 75 | }) 76 | ) 77 | 78 | hideTile() 79 | return subs 80 | } 81 | 82 | export function chooseEnvironment () { 83 | client.require('choose environment', () => { 84 | allProjects() 85 | .then(({ projects, active }) => { 86 | if (!projects) throw '`allProject` handler unsupported' 87 | if (projects.length === 0) throw 'no environment found' 88 | projects = projects.map(proj => { 89 | proj.primary = proj.name 90 | proj.secondary = proj.path 91 | return proj 92 | }) 93 | return { projects, active } 94 | }) 95 | .then(({ projects, active }) => { 96 | show(projects, { active }).then(proj => { 97 | if (!proj) return 98 | const dir = path.dirname(proj.path) 99 | activateProject(dir) 100 | }) 101 | }) 102 | .catch(err => console.log(err)) 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /lib/misc/cells.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { get as weaveGet, 4 | moveNext as weaveMoveNext, 5 | movePrev as weaveMovePrev } from './weave.js' 6 | 7 | import { getLine } from './blocks.js' 8 | 9 | import { Point } from 'atom' 10 | 11 | export function getRange (ed) { 12 | // Cell range is: 13 | // Start of line below top delimiter (and/or start of top row of file) to 14 | // End of line before end delimiter 15 | var buffer = ed.getBuffer() 16 | var start = buffer.getFirstPosition() 17 | var end = buffer.getEndPosition() 18 | var regexString = '^(' + atom.config.get('julia-client.uiOptions.cellDelimiter').join('|') + ')' 19 | var regex = new RegExp(regexString) 20 | var cursor = ed.getCursorBufferPosition() 21 | cursor.column = Infinity // cursor on delimiter line means eval cell below 22 | 23 | 24 | let foundDelim = false 25 | for (let i = cursor.row + 1; i <= ed.getLastBufferRow(); i++) { 26 | let {line, scope} = getLine(ed, i) 27 | foundDelim = regex.test(line) && scope.join('.').indexOf('comment.line') > -1 28 | end.row = i 29 | if (foundDelim) break 30 | } 31 | 32 | if (foundDelim) { 33 | end.row -= 1 34 | if (end.row < 0) end.row = 0 35 | end.column = Infinity 36 | } 37 | 38 | foundDelim = false 39 | if (cursor.row > 0) { 40 | for (let i = end.row; i >= 0; i--) { 41 | let {line, scope} = getLine(ed, i) 42 | foundDelim = regex.test(line) && scope.join('.').indexOf('comment.line') > -1 43 | start.row = i 44 | if (foundDelim) { 45 | break 46 | } 47 | } 48 | start.column = 0 49 | } 50 | 51 | return [start, end] 52 | } 53 | 54 | export function get (ed) { 55 | if (ed.getGrammar().scopeName.indexOf('source.julia') > -1) { 56 | return jlGet(ed) 57 | } else { 58 | return weaveGet(ed) 59 | } 60 | } 61 | 62 | function jlGet (ed) { 63 | var range = getRange(ed) 64 | var text = ed.getTextInBufferRange(range) 65 | if (text.trim() === '') text = ' ' 66 | var res = { 67 | range: [[range[0].row, range[0].column], [range[1].row, range[1].column]], 68 | selection: ed.getSelections()[0], 69 | line: range[0].row, 70 | text: text 71 | } 72 | return [res] 73 | } 74 | 75 | export function moveNext (ed) { 76 | if (ed == null) { 77 | ed = atom.workspace.getActiveTextEditor() 78 | } 79 | if (ed.getGrammar().scopeName.indexOf('source.julia') > -1) { 80 | return jlMoveNext(ed) 81 | } else { 82 | return weaveMoveNext(ed) 83 | } 84 | } 85 | 86 | function jlMoveNext (ed) { 87 | var range = getRange(ed) 88 | var sel = ed.getSelections()[0] 89 | var nextRow = range[1].row + 2 // 2 = 1 to get to delimiter line + 1 more to go past it 90 | return sel.setBufferRange([[nextRow, 0], [nextRow, 0]]) 91 | } 92 | 93 | export function movePrev (ed) { 94 | if (ed == null) { 95 | ed = atom.workspace.getActiveTextEditor() 96 | } 97 | if (ed.getGrammar().scopeName.indexOf('source.weave') > -1) { 98 | return weaveMovePrev(ed) 99 | } else { 100 | return jlMovePrev(ed) 101 | } 102 | } 103 | 104 | function jlMovePrev (ed) { 105 | var range = getRange(ed) 106 | var prevRow = range[0].row - 2 // 2 = 1 to get to delimiter line + 1 more to go past it 107 | var sel = ed.getSelections()[0] 108 | return sel.setBufferRange([[prevRow, 0], [prevRow, 0]]) 109 | } 110 | -------------------------------------------------------------------------------- /lib/misc/scopes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { Point, Range } from 'atom' 4 | 5 | const juliaScopes = ['source.julia', 'source.embedded.julia'] 6 | const openers = [ 7 | 'if', 'while', 'for', 'begin', 'function', 'macro', 'module', 'baremodule', 'type', 'immutable', 8 | 'struct', 'mutable struct', 'try', 'let', 'do', 'quote', 'abstract type', 'primitive type' 9 | ] 10 | const reopeners = [ 'else', 'elseif', 'catch', 'finally' ] 11 | 12 | function isKeywordScope (scopes) { 13 | // Skip 'source.julia' 14 | return scopes.slice(1).some(scope => { 15 | return scope.indexOf('keyword') > -1 16 | }) 17 | } 18 | 19 | export function isStringScope (scopes) { 20 | let isString = false 21 | let isInterp = false 22 | for (const scope of scopes) { 23 | if (scope.indexOf('string') > -1) { 24 | isString = true 25 | } 26 | if (scope.indexOf('interpolation') > -1) { 27 | isInterp = true 28 | } 29 | } 30 | return isString && !isInterp 31 | } 32 | 33 | function forRange (editor, range) { 34 | // this should happen here and not a top-level so that we aren't relying on 35 | // Atom to load packages in a specific order: 36 | const juliaGrammar = atom.grammars.grammarForScopeName('source.julia') 37 | 38 | if (juliaGrammar === undefined) return [] 39 | 40 | const scopes = [] 41 | let n_parens = 0 42 | let n_brackets = 0 43 | const text = editor.getTextInBufferRange(range) 44 | juliaGrammar.tokenizeLines(text).forEach(lineTokens => { 45 | lineTokens.forEach(token => { 46 | const { value } = token 47 | if (!isStringScope(token.scopes)) { 48 | if (n_parens > 0 && value === ')') { 49 | n_parens -= 1 50 | scopes.splice(scopes.lastIndexOf('paren'), 1) 51 | return 52 | } else if (n_brackets > 0 && value === ']') { 53 | n_brackets -= 1 54 | scopes.splice(scopes.lastIndexOf('bracket'), 1) 55 | return 56 | } else if (value === '(') { 57 | n_parens += 1 58 | scopes.push('paren') 59 | return 60 | } else if (value === '[') { 61 | n_brackets += 1 62 | scopes.push('bracket') 63 | return 64 | } 65 | } 66 | if (!(isKeywordScope(token.scopes))) return 67 | if (!(n_parens === 0 && n_brackets === 0)) return 68 | 69 | const reopen = reopeners.includes(value) 70 | if (value === 'end' || reopen) scopes.pop() 71 | if (openers.includes(value) || reopen) scopes.push(value) 72 | }) 73 | }) 74 | return scopes 75 | } 76 | 77 | export function forLines (editor, start, end) { 78 | const startPoint = new Point(start, 0) 79 | const endPoint = new Point(end, Infinity) 80 | const range = new Range(startPoint, endPoint) 81 | return forRange(editor, range) 82 | } 83 | 84 | export function isCommentScope (scopes) { 85 | // Skip 'source.julia' 86 | return scopes.slice(1).some(scope => { 87 | return scope.indexOf('comment') > -1 88 | }) 89 | } 90 | 91 | /** 92 | * Returns `true` if the scope at `bufferPosition` in `editor` is valid code scope to be inspected. 93 | * Supposed to be used within Atom-IDE integrations, whose `grammarScopes` setting doesn't support 94 | * embedded scopes by default. 95 | */ 96 | export function isValidScopeToInspect (editor, bufferPosition) { 97 | const scopes = editor 98 | .scopeDescriptorForBufferPosition(bufferPosition) 99 | .getScopesArray() 100 | return scopes.some(scope => { 101 | return juliaScopes.includes(scope) 102 | }) ? 103 | !isCommentScope(scopes) && !isStringScope(scopes) : 104 | false 105 | } 106 | -------------------------------------------------------------------------------- /lib/runtime/formatter.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import { client } from '../connection' 5 | import { CompositeDisposable } from 'atom' 6 | 7 | const format = client.import('format') 8 | 9 | export function formatCode () { 10 | const editor = atom.workspace.getActiveTextEditor() 11 | if (!editor) return 12 | 13 | const selections = editor.getSelections() 14 | if (selections.length === 1 && !selections[0].getText()) { 15 | formatEditor(editor) 16 | } else { 17 | selections.forEach((selection) => { 18 | formatEditorWithSelection(editor, selection) 19 | }) 20 | } 21 | } 22 | 23 | function formatEditor (editor) { 24 | const range = editor.getBuffer().getRange() 25 | return formatEditorTextInRange(editor, range, editor.getText()) 26 | } 27 | 28 | function formatEditorWithSelection (editor, selection) { 29 | const range = selection.getBufferRange() 30 | return formatEditorTextInRange(editor, range, selection.getText()) 31 | } 32 | 33 | function formatEditorTextInRange (editor, range, text) { 34 | const dir = path.dirname(client.editorPath(editor)) 35 | const marker = markRange(editor, range) 36 | // NOTE: Branch on `getSoftTabs` if supported by formatter. 37 | const indent = editor.getTabLength() 38 | const margin = editor.getPreferredLineLength() 39 | format({ 40 | text, 41 | dir, 42 | indent, 43 | margin, 44 | }).then(({ error, formattedtext }) => { 45 | if (error) { 46 | atom.notifications.addError('Julia-Client: Format-Code', { 47 | description: error, 48 | dismissable: true 49 | }) 50 | } else { 51 | if (marker.isValid()) { 52 | const pos = editor.getCursorBufferPosition() 53 | editor.setTextInBufferRange(marker.getBufferRange(), formattedtext) 54 | editor.scrollToBufferPosition(pos) 55 | editor.setCursorBufferPosition(pos) 56 | } else { 57 | atom.notifications.addError('Julia-Client: Format-Code', { 58 | description: 'Cancelled the formatting task because the selected code has been manually modified.', 59 | dismissable: true 60 | }) 61 | } 62 | } 63 | }).catch(err => { 64 | console.log(err) 65 | }).finally(() => { 66 | marker.destroy() 67 | }) 68 | } 69 | 70 | function markRange(editor, range) { 71 | const marker = editor.markBufferRange(range, { 72 | invalidate: 'inside' 73 | }) 74 | editor.decorateMarker(marker, { 75 | type: 'highlight', 76 | class: 'ink-block' 77 | }) 78 | return marker 79 | } 80 | 81 | let subs 82 | 83 | export function activate() { 84 | subs = new CompositeDisposable() 85 | const edWatch = new WeakSet() 86 | 87 | subs.add(atom.workspace.observeTextEditors(ed => { 88 | edWatch.add(ed) 89 | // use onDidSave instead of onWillSave to guarantee our formatter is the last to run: 90 | const edsub = ed.getBuffer().onDidSave(() => { 91 | if (ed && ed.getGrammar && ed.getGrammar().id === 'source.julia') { 92 | if (client.isActive() && edWatch.has(ed)) { 93 | formatEditor(ed).then(() => { 94 | edWatch.delete(ed) 95 | ed.save().then(() => { 96 | edWatch.add(ed) 97 | }).catch(err => { 98 | console.log(err) 99 | }) 100 | }).catch(err => { 101 | console.log(err) 102 | }) 103 | } 104 | } 105 | }) 106 | subs.add(edsub) 107 | 108 | subs.add(ed.onDidDestroy(() => { 109 | edsub.dispose() 110 | })) 111 | })) 112 | } 113 | 114 | export function deactivate() { 115 | subs && subs.dispose && subs.dispose() 116 | } 117 | -------------------------------------------------------------------------------- /lib/ui/selector.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import SelectList from 'atom-select-list' 4 | 5 | /** 6 | * @type {SelectList} 7 | */ 8 | let selector 9 | let panel, ink 10 | 11 | export function activate (_ink) { 12 | ink = _ink 13 | selector = new SelectList({ 14 | items: [], 15 | elementForItem 16 | }) 17 | selector.element.classList.add('command-palette', 'julia-client-selector') 18 | panel = atom.workspace.addModalPanel({ item: selector.element }) 19 | } 20 | 21 | function elementForItem (item, { selected }) { 22 | const view = document.createElement('li') 23 | if (selected) view.classList.add('active') 24 | const name = (item.primary) ? item.primary.toString() : item.toString() 25 | const primary = ink.matchHighlighter.highlightMatches(name, selector.getFilterQuery()) 26 | view.appendChild(primary) 27 | if (item.secondary) { 28 | const secondary = document.createElement('div') 29 | secondary.classList.add('secondary-line', 'path') 30 | secondary.innerText = item.secondary 31 | view.classList.add('two-lines') 32 | primary.classList.add('primary-line') 33 | view.append(secondary) 34 | } 35 | return view 36 | } 37 | 38 | export function show (items, { active, emptyMessage, errorMessage, infoMessage, allowCustom } = {}) { 39 | selector.update({ 40 | items: [], 41 | query: '', 42 | loadingMessage: 'Loading ...', 43 | }) 44 | const lastFocusedPane = atom.workspace.getActivePane() 45 | panel.show() 46 | selector.focus() 47 | let confirmed = false 48 | return new Promise((resolve, reject) => { 49 | // HACK: 50 | // we can't pass those callback functions to `update` while atom-select-list's document says they can be ... 51 | selector.props.didConfirmSelection = (item) => { 52 | confirmed = true 53 | selector.cancelSelection() 54 | resolve(item) 55 | } 56 | selector.props.didConfirmEmptySelection = () => { 57 | confirmed = true 58 | selector.cancelSelection() 59 | const query = selector.getQuery() 60 | if (allowCustom && query.length > 0) { 61 | resolve(query) 62 | } else { 63 | resolve() 64 | } 65 | } 66 | selector.props.didCancelSelection = () => { 67 | panel.hide() 68 | lastFocusedPane.activate() 69 | const query = selector.getQuery() 70 | if (!confirmed) { 71 | if (allowCustom && query.length > 0) { 72 | resolve(query) 73 | } else { 74 | resolve() 75 | } 76 | } 77 | } 78 | // for handling `Promise` 79 | function updateSelector (items) { 80 | selector.props.filterKeyForItem = (items.length > 0 && items[0] instanceof Object) ? 81 | item => item.primary : item => item 82 | selector.update({ 83 | items, 84 | emptyMessage, 85 | errorMessage, 86 | infoMessage, 87 | loadingMessage: '' 88 | }) 89 | if (active) selectActiveItem(selector, items, active) 90 | } 91 | if (items.constructor == Promise) { 92 | items.then(items => { 93 | updateSelector(items) 94 | }).catch(err => { 95 | reject(err) 96 | selector.cancelSelection() 97 | }) 98 | } else { 99 | updateSelector(items) 100 | } 101 | }) 102 | } 103 | 104 | function selectActiveItem (selector, items, active) { 105 | const index = (active instanceof Number) ? active : 106 | (active instanceof Function) ? items.findIndex(active) : 107 | (items.length > 0 && items[0].primary) ? items.findIndex(item => item.primary === active) : 108 | items.indexOf(active) 109 | if (index === -1) return // do nothing 110 | selector.selectIndex(index) 111 | } 112 | -------------------------------------------------------------------------------- /lib/misc/words.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { Point, Range } from 'atom' 4 | 5 | export const wordRegex = /[\u00A0-\uFFFF\w_!´\.]*@?[\u00A0-\uFFFF\w_!´]+/ 6 | 7 | /** 8 | * Takes an `editor` and gets the word at current cursor position. If that is nonempty, call 9 | * function `fn` with arguments `word` and `range`. 10 | */ 11 | export function withWord (editor, fn) { 12 | const { word, range } = getWordAndRange(editor) 13 | // If we only find numbers or nothing, return prematurely 14 | if (!isValidWordToInspect(word)) return 15 | fn(word, range) 16 | } 17 | 18 | /** 19 | * Returns the word and its range in the `editor`. 20 | * 21 | * `options` 22 | * - `bufferPosition` {Point}: If given returns the word at the `bufferPosition`, returns the word at the current cursor otherwise. 23 | * - `wordRegex` {RegExp} : A RegExp indicating what constitutes a “word” (default: `wordRegex`). 24 | */ 25 | export function getWordAndRange (editor, options = { 26 | bufferPosition: undefined, 27 | wordRegex: wordRegex 28 | }) { 29 | // @TODO?: 30 | // The following lines are kinda iffy: The regex may or may not be well chosen 31 | // and it duplicates the efforts from atom-language-julia. 32 | // It might be better to select the current word via finding the smallest 33 | // containing the bufferPosition/cursor which also has `function` or `macro` as its class. 34 | const bufferPosition = options.bufferPosition ? 35 | options.bufferPosition : 36 | editor.getLastCursor().getBufferPosition() 37 | const range = getWordRangeAtBufferPosition(editor, bufferPosition, { 38 | wordRegex: options.wordRegex ? options.wordRegex : wordRegex 39 | }) 40 | const word = editor.getTextInBufferRange(range) 41 | return { word, range } 42 | } 43 | 44 | /** 45 | * Returns the range of a word containing the `bufferPosition` in `editor`. 46 | * 47 | * `options` 48 | * - `wordRegex` {RegExp}: A RegExp indicating what constitutes a “word” (default: `wordRegex`). 49 | */ 50 | export function getWordRangeAtBufferPosition (editor, bufferPosition, options = { 51 | wordRegex: wordRegex 52 | }) { 53 | // adapted from https://github.com/atom/atom/blob/v1.38.2/src/cursor.js#L606-L616 54 | const { row, column } = bufferPosition 55 | const ranges = editor.getBuffer().findAllInRangeSync( 56 | options.wordRegex ? options.wordRegex : wordRegex, 57 | new Range(new Point(row, 0), new Point(row, Infinity)) 58 | ) 59 | const range = ranges.find(range => 60 | range.end.column >= column && range.start.column <= column 61 | ) 62 | return range ? Range.fromObject(range) : new Range(bufferPosition, bufferPosition) 63 | } 64 | 65 | /** 66 | * Examples: `|` represents `bufferPosition`: 67 | * - `"he|ad.word.foot"` => `Range` of `"head"` 68 | * - `"head|.word.foot"` => `Range` of `"head"` 69 | * - `"head.|word.foot"` => `Range` of `"head.word"` 70 | * - `"head.word.fo|ot"` => `Range` of `"head.word.field"` 71 | */ 72 | export function getWordRangeWithoutTrailingDots (word, range, bufferPosition) { 73 | const { start } = range 74 | const { column: startColumn } = start 75 | const { row: endRow } = range.end 76 | let endColumn = startColumn 77 | 78 | const { column } = bufferPosition 79 | 80 | const elements = word.split('.') 81 | for (const element of elements) { 82 | endColumn += element.length 83 | if (column <= endColumn) { 84 | break 85 | } else { 86 | endColumn += 1 87 | } 88 | } 89 | 90 | const end = new Point(endRow, endColumn) 91 | return new Range(start, end) 92 | } 93 | 94 | /** 95 | * Returns `true` if `word` is valid word to be inspected. 96 | */ 97 | export function isValidWordToInspect (word) { 98 | return word.length > 0 && isNaN(word) 99 | } 100 | -------------------------------------------------------------------------------- /lib/ui/cellhighlighter.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { getRange } from '../misc/cells' 4 | import { CompositeDisposable } from 'atom' 5 | import { getLine } from '../misc/blocks.js' 6 | 7 | let subs 8 | let edSubs 9 | let marker 10 | let borders = [] 11 | 12 | export function activate () { 13 | subs = new CompositeDisposable() 14 | edSubs = new CompositeDisposable() 15 | 16 | subs.add(atom.workspace.observeActiveTextEditor(ed => { 17 | if (ed && ed.getGrammar && ed.getGrammar().id === 'source.julia') { 18 | if (edSubs && edSubs.dispose) { 19 | edSubs.dispose() 20 | edSubs = new CompositeDisposable() 21 | } 22 | borders = highlightCellBorders(ed, borders) 23 | 24 | marker = highlightCurrentCell(ed, marker, borders) 25 | 26 | edSubs.add(ed.onDidChangeCursorPosition(ev => { 27 | marker = highlightCurrentCell(ed, marker, borders) 28 | })) 29 | 30 | edSubs.add(ed.onDidStopChanging(() => { 31 | borders = highlightCellBorders(ed, borders) 32 | marker = highlightCurrentCell(ed, marker, borders) 33 | })) 34 | 35 | edSubs.add(ed.onDidDestroy(() => { 36 | marker && marker.destroy && marker.destroy() 37 | borders.forEach(m => m.destroy()) 38 | edSubs.dispose() 39 | })) 40 | 41 | edSubs.add(ed.onDidChangeGrammar((grammar) => { 42 | marker && marker.destroy && marker.destroy() 43 | borders.forEach(m => m.destroy()) 44 | 45 | if (ed.getGrammar().id == 'source.julia') { 46 | borders = highlightCellBorders(ed, borders) 47 | marker = highlightCurrentCell(ed, marker, borders) 48 | } 49 | })) 50 | } 51 | })) 52 | } 53 | 54 | function highlightCurrentCell (ed, marker, borders) { 55 | if (borders.length === 0) { 56 | marker && marker.destroy && marker.destroy() 57 | return null 58 | } 59 | 60 | const range = getRange(ed) 61 | 62 | range[1].row +=1 63 | range[1].column = 0 64 | 65 | if (marker && marker.destroy) { 66 | const mrange = marker.getBufferRange() 67 | if (mrange.start.row == range[0].row && 68 | mrange.end.row == range[1].row) { 69 | return marker 70 | } else { 71 | marker.destroy() 72 | } 73 | } 74 | 75 | marker = ed.markBufferRange(range) 76 | ed.decorateMarker(marker, { 77 | type: 'line-number', 78 | class: 'julia-current-cell' 79 | }) 80 | ed.decorateMarker(marker, { 81 | type: 'line', 82 | class: 'julia-current-cell' 83 | }) 84 | 85 | return marker 86 | } 87 | 88 | function highlightCellBorders (ed, borders) { 89 | borders.forEach(m => m.destroy()) 90 | 91 | const regexString = '^(' + atom.config.get('julia-client.uiOptions.cellDelimiter').join('|') + ')' 92 | const regex = new RegExp(regexString) 93 | 94 | const buffer = ed.getBuffer() 95 | 96 | borders = [] 97 | 98 | for (let i = 0; i <= buffer.getEndPosition().row; i++) { 99 | const { line, scope } = getLine(ed, i) 100 | if (regex.test(line) && scope.join('.').indexOf('comment.line') > -1) { 101 | const m = ed.markBufferRange([[i, 0], [i, Infinity]]) 102 | ed.decorateMarker(m, { 103 | type: 'line', 104 | class: 'julia-cell-border' 105 | }) 106 | borders.push(m) 107 | } 108 | } 109 | 110 | return borders 111 | } 112 | 113 | function destroyMarkers () { 114 | marker && marker.destroy && marker.destroy() 115 | borders.forEach(m => m.destroy()) 116 | marker = null 117 | borders = [] 118 | } 119 | 120 | export function deactivate () { 121 | destroyMarkers() 122 | subs && subs.dispose && subs.dispose() 123 | edSubs && edSubs.dispose && edSubs.dispose() 124 | } 125 | -------------------------------------------------------------------------------- /lib/runtime/datatip.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | /** 4 | * @FIXME? 5 | * Use `component` property instaed of `markedStrings` and reuse exisiting our full-featured 6 | * components in ../ui/views.coffee. 7 | * Code in https://github.com/TypeStrong/atom-typescript/blob/master/dist/main/atom-ide/datatipProvider.js 8 | * can be helpful. 9 | */ 10 | 11 | import { client } from '../connection' 12 | import modules from './modules' 13 | import { isValidScopeToInspect } from '../misc/scopes' 14 | import { 15 | getWordAndRange, 16 | getWordRangeWithoutTrailingDots, 17 | isValidWordToInspect 18 | } from '../misc/words' 19 | import { getLocalContext } from '../misc/blocks' 20 | 21 | const datatip = client.import('datatip') 22 | 23 | const grammar = atom.grammars.grammarForScopeName('source.julia') 24 | 25 | class DatatipProvider { 26 | providerName = 'julia-client-datatip-provider' 27 | 28 | priority = 100 29 | 30 | grammarScopes = atom.config.get('julia-client.juliaSyntaxScopes') 31 | 32 | useAtomIDEUI = false 33 | 34 | async datatip(editor, bufferPosition) { 35 | // If Julia is not running, do nothing 36 | if (!client.isActive()) return 37 | 38 | // If the scope at `bufferPosition` is not valid code scope, do nothing 39 | if (!isValidScopeToInspect(editor, bufferPosition)) return 40 | 41 | // get word without trailing dot accessors at the buffer position 42 | let { range, word } = getWordAndRange(editor, { 43 | bufferPosition 44 | }) 45 | range = getWordRangeWithoutTrailingDots(word, range, bufferPosition) 46 | word = editor.getTextInBufferRange(range) 47 | 48 | // check the validity of code to be inspected 49 | if (!(isValidWordToInspect(word))) return 50 | 51 | const { main, sub } = await modules.getEditorModule(editor, bufferPosition) 52 | const mod = main ? (sub ? `${main}.${sub}` : main) : 'Main' 53 | 54 | const { column, row } = bufferPosition 55 | const { context, startRow } = getLocalContext(editor, row) 56 | 57 | try { 58 | const result = await datatip({ 59 | word, 60 | mod, 61 | path: editor.getPath(), 62 | column: column + 1, 63 | row: row + 1, 64 | startRow, 65 | context 66 | }) 67 | if (result.error) return 68 | if (this.useAtomIDEUI) { 69 | if (result.line) { 70 | const value = editor.lineTextForBufferRow(result.line).trim() 71 | return { 72 | range, 73 | markedStrings: [{ 74 | type: 'snippet', 75 | value, 76 | grammar 77 | }] 78 | } 79 | } else if (result.strings) { 80 | return { 81 | range, 82 | markedStrings: result.strings.map(string => { 83 | return { 84 | type: string.type, 85 | value: string.value, 86 | grammar: string.type === 'snippet' ? grammar : null 87 | } 88 | }) 89 | } 90 | } 91 | } else { 92 | if (result.line) { 93 | const value = editor.lineTextForBufferRow(result.line).trim() 94 | return { 95 | range, 96 | markedStrings: [{ 97 | type: 'snippet', 98 | value, 99 | grammar 100 | }] 101 | } 102 | } else if (result.strings) { 103 | // @NOTE: atom-ide-datatip can't render multiple `snippet`s in `markedStrings` correctly 104 | return { 105 | range, 106 | markedStrings: result.strings.map(string => { 107 | return { 108 | type: 'markdown', 109 | value: string.type === 'snippet' ? `\`\`\`julia\n${string.value}\n\`\`\`` : string.value, 110 | grammar: string.type === 'snippet' ? grammar : null 111 | } 112 | }) 113 | } 114 | } 115 | } 116 | } catch (error) { 117 | return 118 | } 119 | } 120 | } 121 | 122 | export default new DatatipProvider() 123 | -------------------------------------------------------------------------------- /lib/runtime/outline.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { CompositeDisposable, Disposable, TextEditor } from 'atom' 4 | import { throttle } from 'underscore-plus' 5 | import { client } from '../connection' 6 | import modules from './modules' 7 | 8 | const updateeditor = client.import('updateeditor') 9 | let pane, subs, edSubs, outline 10 | 11 | export function activate (ink) { 12 | pane = ink.Outline.fromId('Julia-Outline') 13 | subs = new CompositeDisposable() 14 | edSubs = new CompositeDisposable() 15 | outline = [] 16 | 17 | subs.add( 18 | atom.config.observe('julia-client.uiOptions.layouts.outline.defaultLocation', defaultLocation => { 19 | pane.setDefaultLocation(defaultLocation) 20 | }), 21 | atom.workspace.onDidStopChangingActivePaneItem(throttle(ed => watchEditor(ed), 300)), 22 | atom.packages.onDidActivateInitialPackages(() => watchEditor(atom.workspace.getActivePaneItem())), 23 | client.onDetached(() => { 24 | outline = [] 25 | pane.setItems([]) 26 | }), 27 | new Disposable(() => { 28 | outline = [] 29 | pane.setItems([]) 30 | if (edSubs) edSubs.dispose() 31 | }) 32 | ) 33 | } 34 | 35 | function watchEditor (ed) { 36 | if (!(ed && ed instanceof TextEditor)) return 37 | 38 | if (edSubs) edSubs.dispose() 39 | edSubs = new CompositeDisposable() // we can't repeat disposing on the same `CompositeDisposable` object 40 | 41 | if (ed.getGrammar().id !== 'source.julia') { 42 | pane.setItems([]) 43 | } else { 44 | edSubs.add( 45 | ed.onDidStopChanging(throttle(() => updateEditor(ed), 300)), 46 | ed.onDidChangeCursorPosition(throttle(() => updateOutline(ed), 300)) 47 | ) 48 | updateEditor(ed, { updateSymbols: false }) 49 | } 50 | edSubs.add( 51 | ed.onDidDestroy(() => { 52 | outline = [] 53 | pane.setItems([]) 54 | }), 55 | ed.onDidChangeGrammar(grammar => { 56 | watchEditor(ed) 57 | }) 58 | ) 59 | } 60 | 61 | // NOTE: update outline and symbols cache all in one go 62 | function updateEditor (ed, options = { 63 | updateSymbols: true 64 | }) { 65 | if (!client.isActive()) return new Promise(resolve => resolve([])) 66 | 67 | const text = ed.getText() 68 | const currentModule = modules.current() 69 | const mod = currentModule ? currentModule : 'Main' 70 | const path = ed.getPath() || 'untitled-' + ed.getBuffer().getId() 71 | 72 | updateeditor({ 73 | text, 74 | mod, 75 | path, 76 | // https://github.com/JunoLab/Juno.jl/issues/407 77 | updateSymbols: options.updateSymbols 78 | }).then(outlineItems => { 79 | outline = handleOutline(ed, outlineItems) 80 | }).catch(err => { 81 | console.log(err); 82 | }) 83 | } 84 | 85 | function handleOutline (ed, outlineItems) { 86 | const cursorLine = ed.getCursorBufferPosition().row + 1 87 | 88 | outlineItems = outlineItems.map(outlineItem => { 89 | outlineItem.isActive = outlineItem.start <= cursorLine && cursorLine <= outlineItem.stop 90 | outlineItem.onClick = () => { 91 | for (const pane of atom.workspace.getPanes()) { 92 | if (pane.getItems().includes(ed)) { 93 | pane.activate() 94 | pane.setActiveItem(ed) 95 | ed.setCursorBufferPosition([outlineItem.start - 1, 0]) 96 | ed.scrollToCursorPosition() 97 | break 98 | } 99 | } 100 | } 101 | return outlineItem 102 | }) 103 | 104 | pane.setItems(outlineItems) 105 | return outlineItems 106 | } 107 | 108 | function updateOutline (ed) { 109 | const cursorLine = ed.getCursorBufferPosition().row + 1 110 | outline = outline.map(item => { 111 | item.isActive = item.start <= cursorLine && cursorLine <= item.stop 112 | return item 113 | }) 114 | pane.setItems(outline) 115 | } 116 | 117 | export function open () { 118 | return pane.open({ 119 | split: atom.config.get('julia-client.uiOptions.layouts.outline.split') 120 | }) 121 | } 122 | 123 | export function close () { 124 | return pane.close() 125 | } 126 | 127 | export function deactivate () { 128 | if (subs) subs.dispose() 129 | } 130 | -------------------------------------------------------------------------------- /lib/connection/process/basic.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import tcp from './tcp' 4 | import * as pty from 'node-pty-prebuilt-multiarch' 5 | import net from 'net' 6 | import { paths, mutex } from '../../misc' 7 | import { jlNotFound } from '../messages' 8 | 9 | export var lock = mutex() 10 | 11 | export function get (path, args) { 12 | return lock((release) => { 13 | let p = get_(path, args) 14 | p.catch((err) => { 15 | release() 16 | }) 17 | release(p.then(({socket}) => socket)) 18 | return p 19 | }) 20 | } 21 | 22 | export function get_ (path, args) { 23 | const env = customEnv() 24 | return getProcess(path, args, env) 25 | } 26 | 27 | export function customEnv (env = process.env) { 28 | let confnt = atom.config.get('julia-client.juliaOptions.numberOfThreads') 29 | let pkgServer = atom.config.get('julia-client.juliaOptions.packageServer') 30 | let confntInt = parseInt(confnt) 31 | 32 | if (confnt == 'auto') { 33 | env.JULIA_NUM_THREADS = require('physical-cpu-count') 34 | } else if (confntInt != 0 && isFinite(confntInt)) { 35 | env.JULIA_NUM_THREADS = confntInt 36 | } 37 | 38 | if (pkgServer) { 39 | env.JULIA_PKG_SERVER = pkgServer 40 | } 41 | 42 | if (atom.config.get('julia-client.disableProxy')) { 43 | delete env.HTTP_PROXY 44 | delete env.HTTPS_PROXY 45 | delete env.http_proxy 46 | delete env.https_proxy 47 | } 48 | 49 | return env 50 | } 51 | 52 | function getProcess (path, args, env) { 53 | return new Promise((resolve, reject) => { 54 | tcp.listen().then((port) => { 55 | paths.fullPath(path).then((path) => { 56 | paths.projectDir().then((cwd) => { 57 | // space before port needed for pty.js on windows: 58 | let ty = pty.spawn(path, [...args, paths.script('boot_repl.jl'), ` ${port}`], { 59 | cols: 100, 60 | rows: 30, 61 | env: env, 62 | cwd: cwd, 63 | useConpty: true, 64 | handleFlowControl: true 65 | }) 66 | 67 | let sock = socket(ty) 68 | 69 | sock.catch((err) => { 70 | reject(err) 71 | }) 72 | 73 | // catch errors when interacting with ty, just to be safe (errors might crash Atom) 74 | let proc = { 75 | ty: ty, 76 | kill: () => { 77 | // only kill pty if it's still alive: 78 | if (ty._readable || ty._writable) { 79 | try { 80 | ty.kill() 81 | } catch (err) { 82 | console.log('Terminal:') 83 | console.log(err); 84 | } 85 | } 86 | }, 87 | interrupt: () => { 88 | try { 89 | ty.write('\x03') 90 | } catch (err) { 91 | console.log('Terminal:') 92 | console.log(err); 93 | } 94 | }, 95 | socket: sock, 96 | onExit: (f) => { 97 | try { 98 | ty.on('exit', f) 99 | } catch (err) { 100 | console.log('Terminal:') 101 | console.log(err); 102 | } 103 | }, 104 | onStderr: (f) => {}, 105 | onStdout: (f) => { 106 | try { 107 | ty.on('data', f) 108 | } catch (err) { 109 | console.log('Terminal:') 110 | console.log(err); 111 | } 112 | } 113 | } 114 | 115 | resolve(proc) 116 | }).catch((err) => { 117 | reject(err) 118 | }) 119 | }).catch((err) => { 120 | jlNotFound(path, err) 121 | reject(err) 122 | }) 123 | }).catch((err) => { 124 | reject(err) 125 | }) 126 | }) 127 | } 128 | 129 | function socket (ty) { 130 | conn = tcp.next() 131 | failure = new Promise((resolve, reject) => { 132 | ty.on('exit', (err) => { 133 | conn.dispose() 134 | reject(err) 135 | }) 136 | }) 137 | return Promise.race([conn, failure]) 138 | } 139 | -------------------------------------------------------------------------------- /spec/client.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | juno = require '../lib/julia-client' 3 | 4 | {client} = juno.connection 5 | 6 | module.exports = -> 7 | 8 | clientStatus = -> [client.isActive(), client.isWorking()] 9 | {echo, evalsimple} = client.import ['echo', 'evalsimple'] 10 | 11 | describe "before booting", -> 12 | checkPath = (p) -> juno.misc.paths.getVersion p 13 | 14 | it "can invalidate a non-existant julia binary", -> 15 | waitsFor (done) -> 16 | checkPath(path.join(__dirname, "foobar")).catch -> done() 17 | 18 | it "can validate a julia command", -> 19 | waitsFor (done) -> 20 | checkPath("julia").then -> done() 21 | 22 | it "can invalidate a non-existant julia command", -> 23 | waitsFor (done) -> 24 | checkPath("nojulia").catch -> done() 25 | 26 | conn = null 27 | beforeEach -> 28 | if conn? 29 | client.attach conn 30 | 31 | describe "when booting the client", -> 32 | 33 | it "recognises the client's state before boot", -> 34 | expect(clientStatus()).toEqual [false, false] 35 | 36 | it "initiates the boot", -> 37 | waitsForPromise -> juno.connection.local.start() 38 | runs -> 39 | conn = client.conn 40 | 41 | it "waits for the boot to complete", -> 42 | pong = client.import('ping')() 43 | expect(clientStatus()).toEqual [true, true] 44 | waitsFor 'the client to boot', 5*60*1000, (done) -> 45 | pong.then (pong) -> 46 | expect(pong).toBe('pong') 47 | done() 48 | 49 | # it "recognises the client's state after boot", -> 50 | # expect(clientStatus()).toEqual [true, false] 51 | 52 | describe "while the client is active", -> 53 | 54 | it "can send and receive nested objects, strings and arrays", -> 55 | msg = {x: 1, y: [1,2,3], z: "foo"} 56 | waitsForPromise -> 57 | echo(msg).then (response) -> 58 | expect(response).toEqual(msg) 59 | 60 | it "can evaluate code and return the result", -> 61 | remote = [1..10].map (x) -> evalsimple("#{x}^2") 62 | waitsForPromise -> 63 | Promise.all(remote).then (remote) -> 64 | expect(remote).toEqual (Math.pow(x, 2) for x in [1..10]) 65 | 66 | it "can rpc into the frontend", -> 67 | client.handle test: (x) -> Math.pow(x, 2) 68 | remote = (evalsimple("Atom.@rpc test(#{x})") for x in [1..10]) 69 | waitsForPromise -> 70 | Promise.all(remote).then (remote) -> 71 | expect(remote).toEqual (Math.pow(x, 2) for x in [1..10]) 72 | 73 | it "can retrieve promise values from the frontend", -> 74 | client.handle test: (x) -> Promise.resolve x 75 | waitsForPromise -> 76 | evalsimple("Atom.@rpc test(2)").then (x) -> 77 | expect(x).toBe 2 78 | 79 | describe "when using callbacks", -> 80 | {cbs, workingSpy, doneSpy} = {} 81 | 82 | beforeEach -> 83 | client.onWorking (workingSpy = jasmine.createSpy 'working') 84 | client.onDone (doneSpy = jasmine.createSpy 'done') 85 | cbs = (evalsimple("peakflops(100)") for i in [1..5]) 86 | 87 | it "enters loading state", -> 88 | expect(client.isWorking()).toBe true 89 | 90 | # it "emits a working event", -> 91 | # expect(workingSpy.calls.length).toBe(1) 92 | 93 | it "isn't done yet", -> 94 | expect(doneSpy).not.toHaveBeenCalled() 95 | 96 | describe "when they finish", -> 97 | 98 | beforeEach -> 99 | waitsFor 10*1000, (done) -> 100 | Promise.all(cbs).then done 101 | 102 | it "stops loading after they are done", -> 103 | expect(client.isWorking()).toBe(false) 104 | 105 | it "emits a done event", -> 106 | expect(doneSpy.calls.length).toBe(1) 107 | 108 | it "can handle a large number of concurrent callbacks", -> 109 | n = 100 110 | cbs = (evalsimple("sleep(rand()); #{i}^2") for i in [0...n]) 111 | waitsForPromise -> 112 | Promise.all(cbs).then (xs) -> 113 | expect(xs).toEqual (Math.pow(x, 2) for x in [0...n]) 114 | 115 | it "handles shutdown correctly", -> 116 | waitsFor (done) -> 117 | evalsimple('exit()').catch -> done() 118 | runs -> 119 | expect(client.isWorking()).toBe(false) 120 | expect(clientStatus()).toEqual [false, false] 121 | -------------------------------------------------------------------------------- /lib/package/menu.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | module.exports = 4 | activate: -> 5 | @subs = new CompositeDisposable 6 | # Package submenu 7 | @subs.add atom.menu.add [{ 8 | label: 'Packages', 9 | submenu: @menu 10 | }] 11 | 12 | # App Menu 13 | if atom.config.get 'julia-client.uiOptions.enableMenu' 14 | @subs.add = atom.menu.add @menu 15 | # TODO: find a less hacky way to do this 16 | menu = atom.menu.template.pop() 17 | atom.menu.template.splice 3, 0, menu 18 | 19 | deactivate: -> 20 | @subs.dispose() 21 | 22 | menu: [{ 23 | label: 'Juno' 24 | submenu: [ 25 | {label: 'Start Julia', command: 'julia-client:start-julia'} 26 | {label: 'Start Remote Julia Process', command: 'julia-client:start-remote-julia-process'} 27 | {label: 'Interrupt Julia', command: 'julia-client:interrupt-julia'} 28 | {label: 'Stop Julia', command: 'julia-client:kill-julia'} 29 | 30 | {type: 'separator'} 31 | 32 | {label: 'Open REPL', command: 'julia-client:open-REPL'} 33 | {label: 'Clear REPL', command: 'julia-client:clear-REPL'} 34 | {label: 'Open External REPL', command: 'julia-client:open-external-REPL'} 35 | { 36 | label: 'Working Directory' 37 | submenu: [ 38 | {label: 'Current File\'s Folder', command: 'julia-client:work-in-current-folder'} 39 | {label: 'Select Project Folder', command: 'julia-client:work-in-project-folder'} 40 | {label: 'Home Folder', command: 'julia-client:work-in-home-folder'} 41 | {label: 'Select...', command: 'julia-client:select-working-folder'} 42 | ] 43 | } 44 | { 45 | label: 'Environment', 46 | submenu: [ 47 | {label: 'Environment in Current File\'s Folder', command: 'julia-client:activate-environment-in-current-folder'} 48 | {label: 'Environment in Parent Folder', command: 'julia-client:activate-environment-in-parent-folder'} 49 | {label: 'Default Environment', command: 'julia-client:activate-default-environment'} 50 | {label: 'Set Working Environment', command: 'julia-client:set-working-environment'} 51 | ] 52 | } 53 | {label: 'Set Working Module', command: 'julia-client:set-working-module'} 54 | 55 | {type: 'separator'} 56 | 57 | {label: 'Run Block', command: 'julia-client:run-block'} 58 | {label: 'Run All', command: 'julia-client:run-all'} 59 | 60 | {type: 'separator'} 61 | 62 | {label: 'Format Code', command: 'julia-client:format-code'} 63 | 64 | {type: 'separator'} 65 | 66 | {label: 'Debug: Run Block', command: 'julia-debug:run-block'} 67 | {label: 'Debug: Step through Block', command: 'julia-debug:step-through-block'} 68 | {label: 'Debug: Run File', command: 'julia-debug:run-file'} 69 | {label: 'Debug: Step through File', command: 'julia-debug:step-through-file'} 70 | 71 | {type: 'separator'} 72 | 73 | {label: 'Open Workspace', command: 'julia-client:open-workspace'} 74 | {label: 'Open Outline Pane', command: 'julia-client:open-outline-pane'} 75 | {label: 'Open Documentation Browser', command: 'julia-client:open-documentation-browser'} 76 | {label: 'Open Plot Pane', command: 'julia-client:open-plot-pane'} 77 | {label: 'Open Debugger Pane', command: 'julia-debug:open-debugger-pane'} 78 | 79 | {type: 'separator'} 80 | 81 | {label: 'Open New Julia File', command: 'julia:new-julia-file'} 82 | {label: 'Open Julia Startup File', command: 'julia:open-julia-startup-file'} 83 | {label: 'Open Juno Startup File', command: 'julia:open-juno-startup-file'} 84 | {label: 'Open Julia Home', command: 'julia:open-julia-home'} 85 | {label: 'Open Package in New Window...', command: 'julia:open-package-in-new-window'} 86 | {label: 'Open Package as Project Folder...', command: 'julia:open-package-as-project-folder'} 87 | 88 | {type: 'separator'} 89 | 90 | { 91 | label: 'New Terminal' 92 | submenu: [ 93 | {label: 'Current File\'s Folder', command: 'julia-client:new-terminal-from-current-folder'} 94 | {label: 'Select Project Folder', command: 'julia-client:new-terminal'} 95 | ] 96 | } 97 | {label: 'New Remote Terminal', command: 'julia-client:new-remote-terminal'} 98 | 99 | {type: 'separator'} 100 | 101 | {label: 'Debug Information', command: 'julia-client:debug-info'} 102 | {label: 'Release Note...', command: 'julia-client:open-release-note'} 103 | {label: 'Help...', command: 'julia:get-help'} 104 | {label: 'Settings...', command: 'julia-client:settings'} 105 | ] 106 | }] 107 | -------------------------------------------------------------------------------- /lib/runtime/plots.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { client } from '../connection' 4 | import { views } from '../ui' 5 | 6 | const { webview } = views.tags 7 | 8 | function consoleLog (e) { 9 | let log = console.log 10 | if (e.level === 0) { 11 | log = console.log 12 | } else if (e.level === 1) { 13 | log = console.warn 14 | } else if (e.level === 2) { 15 | log = console.error 16 | } 17 | log(e.message, `\nat ${e.sourceID}:${e.line}`) 18 | } 19 | 20 | // https://stackoverflow.com/a/5100158/12113178 21 | function dataURItoBlob (dataURI) { 22 | // convert base64/URLEncoded data component to raw binary data held in a string 23 | let byteString 24 | if (dataURI.split(',')[0].indexOf('base64') >= 0) 25 | byteString = atob(dataURI.split(',')[1]) 26 | else 27 | byteString = unescape(dataURI.split(',')[1]) 28 | 29 | // separate out the mime component 30 | var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; 31 | 32 | // write the bytes of the string to a typed array 33 | var ia = new Uint8Array(byteString.length) 34 | for (var i = 0; i < byteString.length; i++) { 35 | ia[i] = byteString.charCodeAt(i) 36 | } 37 | 38 | return new Blob([ia], {type:mimeString}) 39 | } 40 | 41 | export default { 42 | activate () { 43 | client.handle({ 44 | plot: x => this.show(x), 45 | plotsize: () => this.plotSize(), 46 | ploturl: url => this.ploturl(url), 47 | jlpane: (id, opts) => this.jlpane(id, opts) 48 | }) 49 | this.create() 50 | 51 | atom.config.observe('julia-client.uiOptions.usePlotPane', enabled => { 52 | if (enabled) { 53 | return this.pane.setTitle('Plots') 54 | } else { 55 | return this.pane.setTitle('Plots (disabled)') 56 | } 57 | }) 58 | 59 | return atom.config.observe('julia-client.uiOptions.layouts.plotPane.defaultLocation', defaultLocation => { 60 | this.pane.setDefaultLocation(defaultLocation) 61 | }) 62 | }, 63 | 64 | create () { 65 | return this.pane = this.ink.PlotPane.fromId('default') 66 | }, 67 | 68 | open () { 69 | return this.pane.open({ 70 | split: atom.config.get('julia-client.uiOptions.layouts.plotPane.split')}) 71 | }, 72 | 73 | ensureVisible () { 74 | return this.pane.ensureVisible({ split: atom.config.get('julia-client.uiOptions.layouts.plotPane.split') }) 75 | }, 76 | 77 | close () { 78 | return this.pane.close() 79 | }, 80 | 81 | show (view) { 82 | this.ensureVisible() 83 | const v = views.render(view) 84 | this.pane.show(new this.ink.Pannable(v), { 85 | maxSize: atom.config.get('julia-client.uiOptions.maxNumberPlots') 86 | }) 87 | return v 88 | }, 89 | 90 | plotSize () { 91 | return this.ensureVisible().then(() => { 92 | return { 93 | size: this.pane.size(), 94 | ratio: window.devicePixelRatio 95 | } 96 | }) 97 | }, 98 | 99 | webview (url) { 100 | const isDataURI = url.startsWith('data') 101 | if (isDataURI) { 102 | const object = dataURItoBlob(url) 103 | url = URL.createObjectURL(object) 104 | } 105 | 106 | const v = views.render(webview({ 107 | class: 'blinkjl', 108 | src: url, 109 | style: 'width: 100%; height: 100%' 110 | })) 111 | v.classList.add('native-key-bindings') 112 | v.addEventListener('console-message', e => consoleLog(e)) 113 | if (isDataURI) { 114 | v.addEventListener('dom-ready', e => { 115 | URL.revokeObjectURL(url) 116 | }) 117 | } 118 | return v 119 | }, 120 | 121 | ploturl (url) { 122 | const v = this.webview(url) 123 | this.ensureVisible() 124 | return this.pane.show(v, { 125 | maxSize: atom.config.get('julia-client.uiOptions.maxNumberPlots') 126 | }) 127 | }, 128 | 129 | jlpane (id, opts) { 130 | if (opts == null) { opts = {} } 131 | let v = undefined 132 | if (opts.url) { 133 | v = this.webview(opts.url) 134 | if (opts.devtools) { 135 | v.addEventListener('dom-ready', () => { 136 | return v.openDevTools() 137 | }) 138 | } 139 | } 140 | 141 | const pane = this.ink.HTMLPane.fromId(id) 142 | 143 | if (opts.close) { 144 | return pane.close() 145 | } else if (opts.destroy) { 146 | if (pane.closeAndDestroy) { 147 | return pane.closeAndDestroy() 148 | } 149 | } else { 150 | pane.show({ 151 | item: v, 152 | icon: opts.icon, 153 | title: opts.title 154 | }) 155 | 156 | return pane.ensureVisible({ 157 | split: opts.split || atom.config.get('julia-client.uiOptions.layouts.plotPane.split') 158 | }) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/ui/views.coffee: -------------------------------------------------------------------------------- 1 | Highlighter = require './highlighter' 2 | 3 | {client} = require '../connection' 4 | {once} = require '../misc' 5 | 6 | getlazy = client.import 'getlazy' 7 | 8 | module.exports = views = 9 | dom: ({tag, attrs, contents}, opts) -> 10 | view = document.createElement tag 11 | for k, v of attrs 12 | if v instanceof Array then v = v.join ' ' 13 | view.setAttribute k, v 14 | if contents? 15 | if contents.constructor isnt Array 16 | contents = [contents] 17 | for child in contents 18 | view.appendChild @render child, opts 19 | view 20 | 21 | html: ({content, block = false}) -> 22 | view = @render if block then @tags.div() else @tags.span() 23 | view.innerHTML = content 24 | view = if view.children.length == 1 then view.children[0] else view 25 | 26 | tree: ({head, children, expand}, opts) -> 27 | @ink.tree.treeView(@render(head, opts), 28 | children.map((x)=>@render(@tags.div([x]), opts)), 29 | expand: expand) 30 | 31 | lazy: ({head, id}, opts) -> 32 | conn = client.conn 33 | if opts.registerLazy? 34 | opts.registerLazy id 35 | else 36 | console.warn 'Unregistered lazy view' 37 | view = @ink.tree.treeView @render(head, opts), [], 38 | onToggle: once => 39 | return unless client.conn == conn 40 | getlazy(id).then (children) => 41 | body = view.querySelector ':scope > .body' 42 | children.map((x) => @render(@tags.div([x]), opts)).forEach (x) => 43 | body.appendChild(@ink.ansiToHTML(x)) 44 | 45 | subtree: ({label, child}, opts) -> 46 | @render (if child.type == "tree" 47 | type: "tree" 48 | head: @tags.span [label, child.head] 49 | children: child.children 50 | # children: child.children.map((x) => @tags.span "gutted", x) 51 | else 52 | @tags.span "gutted", [label, child]), opts 53 | 54 | copy: ({view, text}, opts) -> 55 | view = @render view, opts 56 | atom.commands.add view, 57 | 'core:copy': (e) -> 58 | atom.clipboard.write text 59 | e.stopPropagation() 60 | view 61 | 62 | link: ({file, line, contents}) -> 63 | view = @render @tags.a {href: '#'}, contents 64 | # TODO: maybe need to dispose of the tooltip onclick and readd them, but 65 | # that doesn't seem to be necessary 66 | if @ink.Opener.isUntitled(file) 67 | tt = atom.tooltips.add view, title: -> 'untitled' 68 | else 69 | tt = atom.tooltips.add view, title: -> file 70 | view.onclick = (e) => 71 | @ink.Opener.open(file, line, { 72 | pending: atom.config.get('core.allowPendingPaneItems') 73 | }) 74 | e.stopPropagation() 75 | view.addEventListener 'DOMNodeRemovedFromDocument', => 76 | tt.dispose() 77 | view 78 | 79 | number: ({value, full}) -> 80 | rounded = value.toPrecision(3) 81 | rounded += '…' unless rounded.toString().length >= full.length 82 | view = @render @tags.span 'syntax--constant syntax--numeric', rounded 83 | isfull = false 84 | view.onclick = (e) -> 85 | view.innerText = if !isfull then full else rounded 86 | isfull = !isfull 87 | e.stopPropagation() 88 | view 89 | 90 | code: ({text, attrs, scope}) -> 91 | grammar = atom.grammars.grammarForScopeName("source.julia") 92 | block = attrs?.block || false 93 | highlighted = Highlighter.highlight(text, grammar, {scopePrefix: 'syntax--', block}) 94 | @render {type: 'html', block, content: highlighted} 95 | 96 | latex: ({attrs, text}) -> 97 | block = attrs?.block || false 98 | latex = @ink.KaTeX.texify(text, block) 99 | @render {type: 'html', block, content: latex} 100 | 101 | views: 102 | dom: (a...) -> views.dom a... 103 | html: (a...) -> views.html a... 104 | tree: (a...) -> views.tree a... 105 | lazy: (a...) -> views.lazy a... 106 | subtree: (a...) -> views.subtree a... 107 | link: (a...) -> views.link a... 108 | copy: (a...) -> views.copy a... 109 | number: (a...) -> views.number a... 110 | code: (a...) -> views.code a... 111 | latex: (a...) -> views.latex a... 112 | 113 | render: (data, opts = {}) -> 114 | if @views.hasOwnProperty(data.type) 115 | r = @views[data.type](data, opts) 116 | @ink.ansiToHTML(r) 117 | r 118 | else if data?.constructor is String 119 | new Text data 120 | else 121 | @render "julia-client: can't render #{data?.type}" 122 | 123 | tag: (tag, attrs, contents) -> 124 | if attrs?.constructor is String 125 | attrs = class: attrs 126 | if attrs?.constructor isnt Object 127 | [contents, attrs] = [attrs, undefined] 128 | type: 'dom' 129 | tag: tag 130 | attrs: attrs 131 | contents: contents 132 | 133 | tags: {} 134 | 135 | ['div', 'span', 'a', 'strong', 'table', 'tr', 'td', 'webview'].forEach (tag) -> 136 | views.tags[tag] = (attrs, contents) -> 137 | views.tag tag, attrs, contents 138 | -------------------------------------------------------------------------------- /lib/misc/paths.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import fs from 'fs' 5 | import child_process from 'child_process' 6 | 7 | export function home (...p) { 8 | const key = process.platform === 'win32' ? 'USERPROFILE' : 'HOME' 9 | return path.join(process.env[key], ...p) 10 | } 11 | 12 | export function juliaHome (...p) { 13 | const juliaHome = (process.env.JULIA_HOME || home('.julia')) 14 | return path.join(juliaHome, ...p) 15 | } 16 | 17 | export function jlpath () { 18 | return expandHome(atom.config.get('julia-client.juliaPath')) 19 | } 20 | 21 | export function expandHome (p) { 22 | return p.startsWith('~') ? p.replace('~', home()) : p 23 | } 24 | 25 | export function fullPath (p) { 26 | return new Promise((resolve, reject) => { 27 | if (fs.existsSync(p)) { 28 | return resolve(fs.realpathSync(p)) 29 | } 30 | const current_dir = process.cwd() 31 | const exepath = path.dirname(process.execPath) 32 | 33 | try { 34 | process.chdir(exepath) 35 | const realpath = fs.realpathSync(p) 36 | if (fs.existsSync(realpath)) { 37 | resolve(realpath) 38 | } 39 | } catch (err) { 40 | console.log(err) 41 | } finally { 42 | try { 43 | process.chdir(current_dir) 44 | } catch (err) { 45 | console.error(err) 46 | } 47 | } 48 | if (process.platform === 'win32') { 49 | if (/[a-zA-Z]\:/.test(p)) return reject("Couldn't resolve path.") 50 | } 51 | const which = process.platform === 'win32' ? 'where' : 'which' 52 | child_process.exec(`${which} "${p}"`, (err, stdout, stderr) => { 53 | if (err) return reject(stderr) 54 | const p = stdout.trim() 55 | if (fs.existsSync(p)) return resolve(p) 56 | return reject('Couldn\'t resolve path.') 57 | }) 58 | }) 59 | } 60 | 61 | export function getVersion (path = jlpath()) { 62 | return new Promise((resolve, reject) => { 63 | fullPath(path).then(path => { 64 | child_process.exec(`"${path}" --version`, (err, stdout, stderr) => { 65 | if (err) return reject(stderr) 66 | const res = stdout.match(/(\d+)\.(\d+)\.(\d+)/) 67 | if (!res) return reject('Couldn\'t resolve version.') 68 | const [_, major, minor, patch] = res 69 | return resolve({ major, minor, patch }) 70 | }) 71 | }).catch(e => { 72 | reject('Couldn\'t resolve version.') 73 | }) 74 | }) 75 | } 76 | 77 | export function projectDir () { 78 | if (atom.config.get('julia-client.juliaOptions.persistWorkingDir')) { 79 | return new Promise(resolve => { 80 | const p = atom.config.get('julia-client.juliaOptions.workingDir') 81 | try { 82 | fs.stat(p, (err, stats) => { 83 | if (err) { 84 | return resolve(atomProjectDir()) 85 | } else { 86 | return resolve(p) 87 | } 88 | }) 89 | } catch (err) { 90 | return resolve(atomProjectDir()) 91 | } 92 | }) 93 | } else { 94 | return atomProjectDir() 95 | } 96 | } 97 | 98 | function atomProjectDir () { 99 | const dirs = atom.workspace.project.getDirectories() 100 | let ws = process.env.HOME 101 | if (!ws) { 102 | ws = process.env.USERPROFILE 103 | } 104 | if (dirs.length === 0 || dirs[0].path.match('app.asar')) { 105 | return Promise.resolve(ws) 106 | } 107 | return new Promise(resolve => { 108 | // use the first open project folder (or its parent folder for files) if 109 | // it is valid 110 | try { 111 | fs.stat(dirs[0].path, (err, stats) => { 112 | if (err) return resolve(ws) 113 | if (stats.isFile()) return resolve(path.dirname(dirs[0].path)) 114 | return resolve(dirs[0].path) 115 | }) 116 | } catch (err) { 117 | return resolve(ws) 118 | } 119 | }) 120 | } 121 | 122 | function packageDir (...s) { 123 | const packageRoot = path.resolve(__dirname, '..', '..') 124 | return path.join(packageRoot, ...s) 125 | } 126 | 127 | export const script = (...s) => packageDir('script', ...s) 128 | 129 | export function getPathFromTreeView (el) { 130 | // invoked from tree-view context menu 131 | let pathEl = el.closest('[data-path]') 132 | if (!pathEl) { 133 | // invoked from command with focusing on tree-view 134 | const activeEl = el.querySelector('.tree-view .selected') 135 | if (activeEl) pathEl = activeEl.querySelector('[data-path]') 136 | } 137 | if (pathEl) return pathEl.dataset.path 138 | return null 139 | } 140 | 141 | export function getDirPathFromTreeView (el) { 142 | // invoked from tree-view context menu 143 | let dirEl = el.closest('.directory') 144 | if (!dirEl) { 145 | // invoked from command with focusing on tree-view 146 | const activeEl = el.querySelector('.tree-view .selected') 147 | if (activeEl) dirEl = activeEl.closest('.directory') 148 | } 149 | if (dirEl) { 150 | const pathEl = dirEl.querySelector('[data-path]') 151 | if (pathEl) return pathEl.dataset.path 152 | } 153 | return null 154 | } 155 | 156 | export const readCode = (path) => fs.readFileSync(path, 'utf-8') 157 | -------------------------------------------------------------------------------- /lib/misc/blocks.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | // TODO: docstrings 3 | 4 | import { forLines } from './scopes' 5 | 6 | export function getLine (ed, l) { 7 | return { 8 | scope: ed.scopeDescriptorForBufferPosition([l, 0]).scopes, 9 | line: ed.getTextInBufferRange([[l, 0], [l, Infinity]]) 10 | } 11 | } 12 | 13 | function isBlank ({line, scope}, allowDocstrings = false) { 14 | for (const s of scope) { 15 | if (/\bcomment\b/.test(s) || (!allowDocstrings && /\bdocstring\b/.test(s))) { 16 | return true 17 | } 18 | } 19 | return /^\s*(#.*)?$/.test(line) 20 | } 21 | function isEnd ({ line, scope }) { 22 | if (isStringEnd({ line, scope })) { 23 | return true 24 | } 25 | return /^(end\b|\)|\]|\})/.test(line) 26 | } 27 | function isStringEnd ({ line, scope }) { 28 | scope = scope.join(' ') 29 | return /\bstring\.multiline\.end\b/.test(scope) || 30 | (/\bstring\.end\b/.test(scope) && /\bbacktick\b/.test(scope)) 31 | } 32 | function isCont ({ line, scope }) { 33 | scope = scope.join(' ') 34 | if (/\bstring\b/.test(scope) && !(/\bpunctuation\.definition\.string\b/.test(scope))) { 35 | return true 36 | } 37 | 38 | return line.match(/^(else|elseif|catch|finally)\b/) 39 | } 40 | function isStart (lineInfo) { 41 | return !(/^\s/.test(lineInfo.line) || isBlank(lineInfo) || isEnd(lineInfo) || isCont(lineInfo)) 42 | } 43 | 44 | function walkBack(ed, row) { 45 | while ((row > 0) && !isStart(getLine(ed, row))) { 46 | row-- 47 | } 48 | return row 49 | } 50 | 51 | function walkForward (ed, start) { 52 | let end = start 53 | let mark = start 54 | while (mark < ed.getLastBufferRow()) { 55 | mark++ 56 | const lineInfo = getLine(ed, mark) 57 | 58 | if (isStart(lineInfo)) { 59 | break 60 | } 61 | if (isEnd(lineInfo)) { 62 | // An `end` only counts when there still are unclosed blocks (indicated by `forLines` 63 | // returning a non-empty array). 64 | // If the line closes a multiline string we also take that as ending the block. 65 | if ( 66 | !(forLines(ed, start, mark-1).length === 0) || 67 | isStringEnd(lineInfo) 68 | ) { 69 | end = mark 70 | } 71 | } else if (!(isBlank(lineInfo) || isStart(lineInfo))) { 72 | end = mark 73 | } 74 | } 75 | return end 76 | } 77 | 78 | function getRange (ed, row) { 79 | const start = walkBack(ed, row) 80 | const end = walkForward(ed, start) 81 | if (start <= row && row <= end) { 82 | return [[start, 0], [end, Infinity]] 83 | } 84 | } 85 | 86 | function getSelection (ed, sel) { 87 | const {start, end} = sel.getBufferRange() 88 | const range = [[start.row, start.column], [end.row, end.column]] 89 | while (isBlank(getLine(ed, range[0][0]), true) && (range[0][0] <= range[1][0])) { 90 | range[0][0]++ 91 | range[0][1] = 0 92 | } 93 | while (isBlank(getLine(ed, range[1][0]), true) && (range[1][0] >= range[0][0])) { 94 | range[1][0]-- 95 | range[1][1] = Infinity 96 | } 97 | return range 98 | } 99 | 100 | export function moveNext (ed, sel, range) { 101 | // Ensure enough room at the end of the buffer 102 | const row = range[1][0] 103 | let last 104 | while ((last = ed.getLastBufferRow()) < (row+2)) { 105 | if ((last !== row) && !isBlank(getLine(ed, last))) { 106 | break 107 | } 108 | sel.setBufferRange([[last, Infinity], [last, Infinity]]) 109 | sel.insertText('\n') 110 | } 111 | // Move the cursor 112 | let to = row + 1 113 | while ((to < ed.getLastBufferRow()) && isBlank(getLine(ed, to))) { 114 | to++ 115 | } 116 | to = walkForward(ed, to) 117 | return sel.setBufferRange([[to, Infinity], [to, Infinity]]) 118 | } 119 | 120 | function getRanges (ed) { 121 | const ranges = ed.getSelections().map(sel => { 122 | return { 123 | selection: sel, 124 | range: sel.isEmpty() ? 125 | getRange(ed, sel.getHeadBufferPosition().row) : 126 | getSelection(ed, sel) 127 | } 128 | }) 129 | return ranges.filter(({ range }) => { 130 | return range && ed.getTextInBufferRange(range).trim() 131 | }) 132 | } 133 | 134 | export function get (ed) { 135 | return getRanges(ed).map(({ range, selection }) => { 136 | return { 137 | range, 138 | selection, 139 | line: range[0][0], 140 | text: ed.getTextInBufferRange(range) 141 | } 142 | }) 143 | } 144 | 145 | export function getLocalContext (editor, row) { 146 | const range = getRange(editor, row) 147 | const context = range ? editor.getTextInBufferRange(range) : '' 148 | // NOTE: 149 | // backend code expects startRow to be number for most cases, e.g.: `row = row - startRow` 150 | // so let's just return `0` when there is no local context 151 | // to check there is a context or not, just check `isempty(context)` 152 | const startRow = range ? range[0][0] : 0 153 | return { 154 | context, 155 | startRow 156 | } 157 | } 158 | 159 | export function select (ed = atom.workspace.getActiveTextEditor()) { 160 | if (!ed) return 161 | return ed.mutateSelectedText(selection => { 162 | const range = getRange(ed, selection.getHeadBufferPosition().row) 163 | if (range) { 164 | selection.setBufferRange(range) 165 | } 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /lib/runtime/completions.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | /** 4 | * @TODO: Custom sorting? 5 | * @TODO: Complete quotes for strings 6 | */ 7 | 8 | import { CompositeDisposable, Point, Range } from 'atom' 9 | 10 | import { client } from '../connection' 11 | import modules from './modules' 12 | 13 | import { getLocalContext } from '../misc/blocks' 14 | 15 | const bracketScope = 'meta.bracket.julia' 16 | const baselineCompletionAdapter = client.import('completions') 17 | const completionDetail = client.import('completiondetail') 18 | 19 | class AutoCompleteProvider { 20 | selector = '.source.julia' 21 | disableForSelector = `.source.julia .comment` 22 | excludeLowerPriority = true 23 | inclusionPriority = 1 24 | suggestionPriority = atom.config.get('julia-client.juliaOptions.autoCompletionSuggestionPriority') 25 | filterSuggestions = false 26 | 27 | activate () { 28 | this.subscriptions = new CompositeDisposable() 29 | this.subscriptions.add( 30 | atom.config.observe('julia-client.juliaOptions.fuzzyCompletionMode', v => { 31 | this.fuzzyCompletionMode = v 32 | }), 33 | atom.config.observe('julia-client.juliaOptions.noAutoParenthesis', v => { 34 | this.noAutoParenthesis = v 35 | }) 36 | ) 37 | } 38 | 39 | deactivate () { 40 | this.subscriptions.dispose() 41 | } 42 | 43 | getSuggestions (data) { 44 | if (!client.isActive()) return [] 45 | const { editor, bufferPosition, activatedManually } = data 46 | const { row, column } = bufferPosition 47 | const startPoint = new Point(row, 0) 48 | const endPoint = new Point(row, column) 49 | const lineRange = new Range(startPoint, endPoint) 50 | const line = editor.getTextInBufferRange(lineRange) 51 | 52 | // suppress completions if an whitespace precedes, except the special cases below 53 | // - activatedManually (i.e. an user forces completions) 54 | // - the current position is in function call: show method completions 55 | // - after `using`/`import` keyword: show package completions 56 | if (!activatedManually) { 57 | if (column === 0) return [] 58 | const prevCharPosition = new Point(row, column - 1) 59 | const charRange = new Range(prevCharPosition, bufferPosition) 60 | const char = editor.getTextInBufferRange(charRange) 61 | const { scopes } = editor.scopeDescriptorForBufferPosition(bufferPosition) 62 | if ( 63 | !scopes.includes(bracketScope) && 64 | !(/\b(import|using)\b/.test(line)) && 65 | char === ' ' 66 | ) return [] 67 | } 68 | 69 | const baselineCompletions = this.baselineCompletions(data, line) 70 | return Promise.race([baselineCompletions, this.sleep()]) 71 | } 72 | 73 | baselineCompletions (data, line) { 74 | const { editor, bufferPosition: { row, column }, activatedManually } = data 75 | const { context, startRow } = getLocalContext(editor, row) 76 | return baselineCompletionAdapter({ 77 | // general 78 | line, 79 | path: editor.getPath(), 80 | mod: modules.current(), 81 | // local context 82 | context, 83 | row: row + 1, 84 | startRow, 85 | column: column + 1, 86 | // configurations 87 | is_fuzzy: this.fuzzyCompletionMode, 88 | force: activatedManually || false, 89 | }).then(completions => { 90 | return completions.map(completion => { 91 | return this.toCompletion(completion) 92 | }) 93 | }).catch(() => { 94 | return [] 95 | }) 96 | } 97 | 98 | toCompletion (completion) { 99 | const icon = this.makeIcon(completion.icon) 100 | if (icon) completion.iconHTML = icon 101 | // workaround https://github.com/atom/autocomplete-plus/issues/868 102 | if (!completion.description && completion.descriptionMoreURL) { 103 | completion.description = ' ' 104 | } 105 | return completion 106 | } 107 | 108 | // should sync with atom-ink/lib/workspace/workspace.js 109 | makeIcon(icon) { 110 | // if not specified, just fallback to `completion.type` 111 | if (!icon) return '' 112 | if (icon.startsWith('icon-')) return `` 113 | return icon.length === 1 ? icon : '' 114 | } 115 | 116 | sleep () { 117 | return new Promise(resolve => { 118 | setTimeout(() => { 119 | resolve(null) 120 | }, 1000) 121 | }) 122 | } 123 | 124 | getSuggestionDetailsOnSelect (_completion) { 125 | const completionWithDetail = completionDetail(_completion).then(completion => { 126 | // workaround https://github.com/atom/autocomplete-plus/issues/868 127 | if (!completion.description && completion.descriptionMoreURL) { 128 | completion.description = ' ' 129 | } 130 | return completion 131 | }).catch(err => { 132 | console.log(err) 133 | }) 134 | return Promise.race([completionWithDetail, this.sleep()]) 135 | } 136 | 137 | onDidInsertSuggestion ({ editor, suggestion: { type } }) { 138 | if (type !== 'function' || this.noAutoParenthesis) return 139 | editor.mutateSelectedText(selection => { 140 | if (!selection.isEmpty()) return 141 | const { row, column } = selection.getBufferRange().start 142 | const currentPoint = new Point(row, column) 143 | const nextPoint = new Point(row, column + 1) 144 | const range = new Range(currentPoint, nextPoint) 145 | const finishRange = new Range(nextPoint, nextPoint) 146 | if (editor.getTextInBufferRange(range) !== '(') { 147 | selection.insertText('()') 148 | } 149 | selection.setBufferRange(finishRange) 150 | }) 151 | } 152 | } 153 | 154 | export default new AutoCompleteProvider() 155 | -------------------------------------------------------------------------------- /lib/ui/layout.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | const repl = () => { 4 | return require('../runtime').console 5 | } 6 | const workspace = () => { 7 | return require('../runtime').workspace 8 | } 9 | const documentation = () => { 10 | return require('../ui').docpane 11 | } 12 | const plotPane = () => { 13 | return require('../runtime').plots 14 | } 15 | const debuggerPane = () => { 16 | return require('../runtime').debugger 17 | } 18 | const linter = () => { 19 | return require('../runtime').linter 20 | } 21 | const outline = () => { 22 | return require('../runtime').outline 23 | } 24 | 25 | function specifiedPanes () { 26 | const panes = [] 27 | // @NOTE: Push panes in order of their 'importance': Refer to function `openPanesHelper` for why 28 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.console')) panes.push(repl) 29 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.workspace')) panes.push(workspace) 30 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.documentation')) panes.push(documentation) 31 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.plotPane')) panes.push(plotPane) 32 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.debuggerPane')) panes.push(debuggerPane) 33 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.linter')) panes.push(linter) 34 | if (atom.config.get('julia-client.uiOptions.layouts.defaultPanes.outline')) panes.push(outline) 35 | 36 | return panes 37 | } 38 | 39 | export function closePromises () { 40 | // Close only specified panes, i.e.: non-specified panes won't be closed/opened 41 | const panes = specifiedPanes() 42 | 43 | const promises = panes.map(pane => { 44 | return pane().close() 45 | }) 46 | 47 | return promises 48 | } 49 | 50 | function bundlePanes () { 51 | const containers = [] 52 | containers.push(atom.workspace.getCenter()) 53 | containers.push(atom.workspace.getLeftDock()) 54 | containers.push(atom.workspace.getBottomDock()) 55 | containers.push(atom.workspace.getRightDock()) 56 | 57 | containers.forEach(container => { 58 | const panes = container.getPanes() 59 | const firstPane = panes[0] 60 | const otherPanes = panes.slice(1) 61 | otherPanes.forEach(pane => { 62 | const items = pane.getItems() 63 | items.forEach(item => { 64 | pane.moveItemToPane(item, firstPane) 65 | }) 66 | }) 67 | }) 68 | } 69 | 70 | function openPanes () { 71 | const panes = specifiedPanes() 72 | 73 | openPanesHelper(panes) 74 | } 75 | 76 | function openPanesHelper (panes) { 77 | if (panes.length === 0) { 78 | // If there is no more pane to be opened, activate the first item in each pane. This works since 79 | // Juno-panes are opened in order of their importance as defined in `specifiedPanes` function 80 | atom.workspace.getPanes().forEach(pane => { 81 | pane.activateItemAtIndex(0) 82 | }) 83 | // Activate `WorkspaceCenter` at last 84 | atom.workspace.getCenter().activate() 85 | return 86 | } 87 | 88 | const pane = panes.shift() 89 | pane().open().catch((err) => { 90 | // @FIXME: This is a temporal remedy for https://github.com/JunoLab/atom-julia-client/pull/561#issuecomment-500150318 91 | console.error(err) 92 | pane().open() 93 | }).finally(() => { 94 | // Re-focus the previously focused pane (i.e. the bundled pane by `bundlePanes`) after each opening 95 | // This prevents opening multiple panes with the same splitting rule in a same location from 96 | // ending up in a funny state 97 | const container = atom.workspace.getActivePaneContainer() 98 | container.activatePreviousPane() 99 | openPanesHelper(panes) 100 | }) 101 | } 102 | 103 | export function restoreDefaultLayout () { 104 | // Close Juno-specific panes first to reset to default layout 105 | Promise.all(closePromises()).then(() => { 106 | 107 | // Simplify layouts in each container to prevent funny splitting 108 | bundlePanes() 109 | 110 | // Open Juno-specific panes again 111 | openPanes() 112 | }) 113 | } 114 | 115 | export function resetDefaultLayoutSettings () { 116 | const onStartup = atom.config.get('julia-client.uiOptions.layouts.openDefaultPanesOnStartUp') 117 | atom.config.unset('julia-client.uiOptions.layouts') 118 | atom.config.set('julia-client.uiOptions.layouts.openDefaultPanesOnStartUp', onStartup) 119 | } 120 | 121 | export function queryDefaultLayout () { 122 | const message = atom.notifications.addInfo('Julia-Client: Open Juno-specific panes on startup ?', { 123 | buttons: [ 124 | { 125 | text: 'Yes', 126 | onDidClick: () => { 127 | restoreDefaultLayout() 128 | message.dismiss() 129 | atom.config.set('julia-client.firstBoot', false) 130 | atom.config.set('julia-client.uiOptions.layouts.openDefaultPanesOnStartUp', true) 131 | } 132 | }, 133 | { 134 | text: 'No', 135 | onDidClick: () => { 136 | message.dismiss() 137 | atom.config.set('julia-client.firstBoot', false) 138 | atom.config.set('julia-client.uiOptions.layouts.openDefaultPanesOnStartUp', false) 139 | } 140 | } 141 | ], 142 | description: 143 | `You can specify the panes to be opened and their _default location_ and _splitting rule_ in 144 | **\`Packages -> Juno -> Settings -> Julia-Client -> UI Options -> Layout Options\`**. 145 | \`Julia-Client: Restore-Default-Layout\` command will restore the layout at later point in time. 146 | Use \`Julia-Client: Reset-Default-Layout-Settings\` command to reset the layout settings if it gets messed up.`, 147 | dismissable: true 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /lib/julia-client.coffee: -------------------------------------------------------------------------------- 1 | etch = require 'etch' 2 | 3 | commands = require './package/commands' 4 | config = require './package/config' 5 | menu = require './package/menu' 6 | settings = require './package/settings' 7 | release = require './package/release-note' 8 | toolbar = require './package/toolbar' 9 | semver = require 'semver' 10 | 11 | # TODO: Update me when tagging a new relase (and release note) 12 | INK_VERSION_COMPAT = "^0.12.4" 13 | LATEST_RELEASE_NOTE_VERSION = "0.12.6" 14 | 15 | INK_LINK = '[`ink`](https://github.com/JunoLab/atom-ink)' 16 | LANGUAGE_JULIA_LINK = '[`language-julia`](https://github.com/JuliaEditorSupport/atom-language-julia)' 17 | 18 | module.exports = JuliaClient = 19 | misc: require './misc' 20 | ui: require './ui' 21 | connection: require './connection' 22 | runtime: require './runtime' 23 | 24 | activate: (state) -> 25 | etch.setScheduler(atom.views) 26 | process.env['TERM'] = 'xterm-256color' 27 | commands.activate @ 28 | x.activate() for x in [menu, @connection, @runtime] 29 | @ui.activate @connection.client 30 | 31 | @requireDeps => 32 | settings.updateSettings() 33 | 34 | if atom.config.get('julia-client.firstBoot') 35 | @ui.layout.queryDefaultLayout() 36 | else 37 | if atom.config.get('julia-client.uiOptions.layouts.openDefaultPanesOnStartUp') 38 | setTimeout (=> @ui.layout.restoreDefaultLayout()), 150 39 | 40 | requireDeps: (fn) -> 41 | isLoaded = atom.packages.isPackageLoaded("ink") and atom.packages.isPackageLoaded("language-julia") 42 | 43 | if isLoaded 44 | fn() 45 | else 46 | require('atom-package-deps').install('julia-client') 47 | .then => @enableDeps fn 48 | .catch (err) -> 49 | console.error err 50 | atom.notifications.addError 'Installing Juno\'s dependencies failed.', 51 | description: 52 | """ 53 | Juno requires the packages #{INK_LINK} and #{LANGUAGE_JULIA_LINK} to run. 54 | Please install them manually via `File -> Settings -> Packages`, 55 | or open a terminal and run 56 | 57 | apm install ink 58 | apm install language-julia 59 | 60 | and then restart Atom. 61 | """ 62 | dismissable: true 63 | 64 | enableDeps: (fn) -> 65 | isEnabled = atom.packages.isPackageLoaded("ink") and atom.packages.isPackageLoaded("language-julia") 66 | 67 | if isEnabled 68 | fn() 69 | else 70 | atom.packages.enablePackage('ink') 71 | atom.packages.enablePackage('language-julia') 72 | 73 | if atom.packages.isPackageLoaded("ink") and atom.packages.isPackageLoaded("language-julia") 74 | atom.notifications.addSuccess "Automatically enabled Juno's dependencies.", 75 | description: 76 | """ 77 | Juno requires the #{INK_LINK} and #{LANGUAGE_JULIA_LINK} packages. 78 | We've automatically enabled them for you. 79 | """ 80 | dismissable: true 81 | 82 | inkVersion = atom.packages.loadedPackages["ink"].metadata.version 83 | if not atom.devMode and not semver.satisfies(inkVersion, INK_VERSION_COMPAT) 84 | atom.notifications.addWarning "Potentially incompatible `ink` version detected.", 85 | description: 86 | """ 87 | Please make sure to upgrade #{INK_LINK} to a version compatible with `#{INK_VERSION_COMPAT}`. 88 | The currently installed version is `#{inkVersion}`. 89 | 90 | If you cannot install an appropriate version via via `File -> Settings -> Packages`, 91 | open a terminal and run 92 | 93 | apm install ink@x.y.z 94 | 95 | where `x.y.z` is satisfies `#{INK_VERSION_COMPAT}`. 96 | """ 97 | dismissable: true 98 | 99 | fn() 100 | else 101 | atom.notifications.addError "Failed to enable Juno's dependencies.", 102 | description: 103 | """ 104 | Juno requires the #{INK_LINK} and #{LANGUAGE_JULIA_LINK} packages. 105 | Please install them manually via `File -> Settings -> Packages`, 106 | or open a terminal and run 107 | 108 | apm install ink 109 | apm install language-julia 110 | 111 | and then restart Atom. 112 | """ 113 | dismissable: true 114 | 115 | config: config 116 | 117 | deactivate: -> 118 | x.deactivate() for x in [commands, menu, toolbar, release, @connection, @runtime, @ui] 119 | 120 | consumeInk: (ink) -> 121 | commands.ink = ink 122 | x.consumeInk ink for x in [@connection, @runtime, @ui] 123 | try 124 | v = atom.config.get('julia-client.currentVersion') 125 | if v isnt LATEST_RELEASE_NOTE_VERSION 126 | release.activate(ink, LATEST_RELEASE_NOTE_VERSION) 127 | else 128 | release.activate(ink) 129 | catch err 130 | console.log(err) 131 | finally 132 | atom.config.set('julia-client.currentVersion', LATEST_RELEASE_NOTE_VERSION) 133 | 134 | consumeStatusBar: (bar) -> @runtime.consumeStatusBar bar 135 | 136 | consumeToolBar: (bar) -> toolbar.consumeToolBar bar 137 | 138 | consumeGetServerConfig: (conf) -> @connection.consumeGetServerConfig(conf) 139 | 140 | consumeGetServerName: (name) -> @connection.consumeGetServerName(name) 141 | 142 | consumeDatatip: (datatipService) -> @runtime.consumeDatatip datatipService 143 | 144 | provideClient: -> @connection.client 145 | 146 | provideAutoComplete: -> @runtime.provideAutoComplete() 147 | 148 | provideHyperclick: -> @runtime.provideHyperclick() 149 | 150 | handleURI: (parsedURI) -> @runtime.handleURI parsedURI 151 | -------------------------------------------------------------------------------- /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 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', '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 code 29 | eval(parse(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`. 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 | we can just use the `rpc` function from JS: 40 | 41 | ```coffeescript 42 | client.rpc('eval', '2+2').then (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 | Atom.@msg type(args...) 64 | ``` 65 | 66 | Handlers are defined on the Atom side as follows: 67 | 68 | ```coffeescript 69 | client.handle 'log', (args...) -> 70 | console.log args 71 | ``` 72 | 73 | It's also possible for Julia to wait for a response from Atom, using the `rpc` function. 74 | 75 | ```coffeescript 76 | client.handle 'echo', (data) -> 77 | data 78 | ``` 79 | 80 | (It's very easy to add this code to `julia-client`'s [`activate` 81 | function](https://github.com/JunoLab/atom-julia-client/blob/master/lib/julia-client.coffee) 82 | if you want to try this out.) 83 | 84 | Calling the following from the REPL: 85 | 86 | ```julia 87 | Atom.@rpc echo(Dict(:a=>1, :b=>2)) 88 | ``` 89 | 90 | will return `Dict("a"=>1, "b"=>2)`. The data was passed to Atom and simply returned as-is. 91 | Try changing the handler to modify the data before returning it. 92 | 93 | This mechanism is how Julia commands like `Atom.select()` are implemented, and in general it 94 | makes it very simple for Julia to control the Atom frontend – see 95 | [frontend.jl](https://github.com/JunoLab/Atom.jl/blob/master/src/frontend.jl) and 96 | [frontend.coffee](https://github.com/JunoLab/atom-julia-client/blob/master/lib/frontend.coffee) 97 | 98 | ## Debugging and Learning 99 | 100 | A good way to get a handle on this stuff is just to use `console.log` and `@show`, on the 101 | Atom and Julia sides respectively, to take a peek at what's going over the wire. For example, 102 | it's easy to change the above Julia handler to 103 | 104 | ```julia 105 | handle("eval") do data 106 | @show data 107 | @show Dict(:result => eval(parse(data["code"]))) 108 | end 109 | ``` 110 | 111 | This will show you both the data being sent to Julia (in the example above, 112 | `Dict("code"=>"2+2")`) and the data being sent back to Atom (`Dict(:result => 4)`). 113 | Modifying say, the completions handler in a similar way will show you what completion data 114 | Julia sends back to Atom (there will probably be a lot, so try looking at specific 115 | keys, for example). 116 | 117 | You don't need to reload Atom or restart the Julia client every time you make a change like 118 | this. If you open a file from the `Atom.jl` source code, you should see from the status bar 119 | that Juno knows you're working with the `Atom` module (try evaluating `current_module()` if 120 | you're not sure). Evaluating `handlers` from within the `Atom` module will show you what 121 | message types are currently defined. If you change a handler, just press `C-Enter` to update 122 | it in place; you should see the effect of your update immediately next time the handler is 123 | triggered. For example, if you modify the 124 | [`eval`](https://github.com/JunoLab/Atom.jl/blob/master/src/eval.jl) handler as follows: 125 | 126 | ```julia 127 | handle("eval") do data 128 | println(data["code"]) # <- insert this line 129 | # ... 130 | ``` 131 | 132 | and update it, you should find that the *next* time you evaluate you see the contents of the 133 | current editor dumped into the console. Thus, most features or fixes you'd want to add to 134 | Juno can be made without a long edit – compile – run cycle. 135 | -------------------------------------------------------------------------------- /lib/connection/client.coffee: -------------------------------------------------------------------------------- 1 | {throttle} = require 'underscore-plus' 2 | {Emitter} = require 'atom' 3 | 4 | IPC = require './ipc' 5 | 6 | module.exports = 7 | 8 | # Connection logic injects a connection via `attach`. 9 | ## Required interface: 10 | # .message(json) 11 | ## Optional interface: 12 | # .stdin(data) 13 | # .interrupt() 14 | # .kill() 15 | 16 | # Messaging 17 | 18 | ipc: new IPC 19 | 20 | handle: (a...) -> @ipc.handle a... 21 | input: (m) -> @ipc.input m 22 | readStream: (s) -> @ipc.readStream s 23 | import: (a...) -> @ipc.import a... 24 | 25 | activate: -> 26 | 27 | @emitter = new Emitter 28 | 29 | @bootMode = atom.config.get('julia-client.juliaOptions.bootMode') 30 | 31 | @ipc.writeMsg = (msg) => 32 | if @isActive() and @conn.ready?() isnt false 33 | @conn.message msg 34 | else 35 | @ipc.queue.push msg 36 | 37 | @handle 'error', (options) => 38 | if atom.config.get 'julia-client.uiOptions.errorNotifications' 39 | atom.notifications.addError options.msg, options 40 | console.error options.detail 41 | atom.beep() 42 | 43 | plotpane = null 44 | 45 | @onAttached => 46 | args = atom.config.get 'julia-client.juliaOptions.arguments' 47 | @import('connected')() 48 | if args.length > 0 49 | @import('args') args 50 | 51 | plotpane = atom.config.observe 'julia-client.uiOptions.usePlotPane', (use) => 52 | @import('enableplotpane')(use) 53 | 54 | @onDetached => 55 | plotpane?.dispose() 56 | 57 | @onBoot (proc) => 58 | @remoteConfig = proc.config 59 | 60 | setBootMode: (@bootMode) -> 61 | 62 | editorPath: (ed) -> 63 | if not ed? then return ed 64 | if @bootMode is 'Remote' and @remoteConfig? 65 | path = ed.getPath() 66 | if not path? then return path 67 | ind = path.indexOf(@remoteConfig.host) 68 | if ind > -1 69 | path = path.slice(ind + @remoteConfig.host.length, path.length) 70 | path = path.replace(/\\/g, '/') 71 | return path 72 | else 73 | return path 74 | else 75 | return ed.getPath() 76 | 77 | deactivate: -> 78 | @emitter.dispose() 79 | if @isActive() then @detach() 80 | 81 | # Basic handlers (communication through stderr) 82 | 83 | basicHandlers: {} 84 | 85 | basicHandler: (s) -> 86 | if (match = s.toString().match /juno-msg-(.*)/) 87 | @basicHandlers[match[1]]?() 88 | true 89 | 90 | handleBasic: (msg, f) -> @basicHandlers[msg] = f 91 | 92 | # Connecting & Booting 93 | 94 | emitter: new Emitter 95 | 96 | onAttached: (cb) -> @emitter.on 'attached', cb 97 | onDetached: (cb) -> @emitter.on 'detached', cb 98 | 99 | onceAttached: (cb) -> 100 | f = @onAttached (args...) -> 101 | f.dispose() 102 | cb.call this, args... 103 | 104 | isActive: -> @conn? 105 | 106 | attach: (@conn) -> 107 | @flush() unless @conn.ready?() is false 108 | @emitter.emit 'attached' 109 | 110 | detach: -> 111 | delete @conn 112 | @ipc.reset() 113 | @emitter.emit 'detached' 114 | 115 | flush: -> @ipc.flush() 116 | 117 | isWorking: -> @ipc.isWorking() 118 | onWorking: (f) -> @ipc.onWorking f 119 | onDone: (f) -> @ipc.onDone f 120 | onceDone: (f) -> @ipc.onceDone f 121 | 122 | # Management & UI 123 | 124 | onStdout: (f) -> @emitter.on 'stdout', f 125 | onStderr: (f) -> @emitter.on 'stderr', f 126 | onInfo: (f) -> @emitter.on 'info', f 127 | onBoot: (f) -> @emitter.on 'boot', f 128 | stdout: (data) -> @emitter.emit 'stdout', data 129 | stderr: (data) -> @emitter.emit 'stderr', data unless @basicHandler data 130 | info: (data) -> @emitter.emit 'info', data 131 | 132 | clientCall: (name, f, args...) -> 133 | if not @conn[f]? 134 | atom.notifications.addError "This client doesn't support #{name}." 135 | else 136 | @conn[f].call @conn, args... 137 | 138 | stdin: (data) -> @clientCall 'STDIN', 'stdin', data 139 | 140 | interrupt: -> 141 | if @isActive() 142 | @clientCall 'interrupts', 'interrupt' 143 | 144 | disconnect: -> 145 | if @isActive() 146 | @clientCall 'disconnecting', 'disconnect' 147 | 148 | kill: -> 149 | if @isActive() 150 | if not @isWorking() 151 | @import('exit')().catch -> 152 | else 153 | @clientCall 'kill', 'kill' 154 | else 155 | @ipc.reset() 156 | 157 | clargs: -> 158 | {optimisationLevel, deprecationWarnings} = 159 | atom.config.get 'julia-client.juliaOptions' 160 | as = [] 161 | as.push "--depwarn=#{if deprecationWarnings then 'yes' else 'no'}" 162 | as.push "-O#{optimisationLevel}" unless optimisationLevel is 2 163 | as.push "--color=yes" 164 | as.push "-i" 165 | startupArgs = atom.config.get 'julia-client.juliaOptions.startupArguments' 166 | if startupArgs.length > 0 167 | as = as.concat startupArgs 168 | as = as.map (arg) => arg.trim() 169 | as = as.filter (arg) => arg.length > 0 170 | as 171 | 172 | connectedError: (action = 'do that') -> 173 | if @isActive() 174 | atom.notifications.addError "Can't #{action} with a Julia client running.", 175 | description: "Stop the current client with `Packages -> Juno -> Stop Julia`." 176 | true 177 | else 178 | false 179 | 180 | notConnectedError: (action = 'do that') -> 181 | if not @isActive() 182 | atom.notifications.addError "Can't #{action} without a Julia client running.", 183 | description: "Start a client with `Packages -> Juno -> Start Julia`." 184 | true 185 | else 186 | false 187 | 188 | require: (a, f) -> 189 | f ? [a, f] = [null, a] 190 | @notConnectedError(a) or f() 191 | 192 | disrequire: (a, f) -> 193 | f ? [a, f] = [null, a] 194 | @connectedError(a) or f() 195 | 196 | withCurrent: (f) -> 197 | current = @conn 198 | (a...) => 199 | return unless current is @conn 200 | f(a...) 201 | -------------------------------------------------------------------------------- /lib/connection/process/server.coffee: -------------------------------------------------------------------------------- 1 | os = require 'os' 2 | net = require 'net' 3 | path = require 'path' 4 | fs = require 'fs' 5 | child_process = require 'child_process' 6 | 7 | {exclusive} = require '../../misc' 8 | 9 | IPC = require '../ipc' 10 | basic = require './basic' 11 | cycler = require './cycler' 12 | 13 | module.exports = 14 | 15 | socketPath: (name) -> 16 | if process.platform is 'win32' 17 | "\\\\.\\pipe\\#{name}" 18 | else 19 | path.join(os.tmpdir(), "#{name}.sock") 20 | 21 | removeSocket: (name) -> 22 | new Promise (resolve, reject) => 23 | p = @socketPath name 24 | fs.exists p, (exists) -> 25 | if not exists then return resolve() 26 | fs.unlink p, (err) -> 27 | if err then reject(err) else resolve() 28 | 29 | # Client 30 | 31 | boot: -> 32 | @removeSocket('juno-server').then => 33 | new Promise (resolve, reject) => 34 | console.info 'booting julia server' 35 | proc = child_process.fork path.join(__dirname, 'boot.js'), 36 | detached: true 37 | env: process.env 38 | proc.on 'message', (x) -> 39 | if x == 'ready' then resolve() 40 | else console.log 'julia server:', x 41 | proc.on 'exit', (code, status) -> 42 | console.warn 'julia server:', [code, status] 43 | reject([code, status]) 44 | 45 | connect: -> 46 | new Promise (resolve, reject) => 47 | client = net.connect @socketPath('juno-server'), => 48 | ipc = new IPC client 49 | resolve ipc.import Object.keys(@serverAPI()), true, {ipc} 50 | client.on 'error', (err) -> 51 | reject err 52 | 53 | activate: exclusive -> 54 | return @server if @server? 55 | @connect() 56 | .catch (err) => 57 | if err.code in ['ECONNREFUSED', 'ENOENT'] 58 | @boot().then => @connect() 59 | else Promise.reject err 60 | .then (@server) => 61 | @server.ipc.stream.on 'end', => delete @server 62 | @server.setPS basic.wrapperEnabled() 63 | @server 64 | 65 | getStream: (id, s) -> 66 | @connect().then ({ipc}) -> 67 | sock = ipc.stream 68 | ipc.msg s, id 69 | ipc.unreadStream() 70 | sock 71 | 72 | getStreams: (id) -> Promise.all (@getStream id, s for s in ['stdin', 'stdout', 'stderr']) 73 | 74 | getSocket: (id) -> 75 | @server.onBoot(id).then => 76 | @getStream(id, 'socket').then (sock) => 77 | @server.onAttach(id).then -> sock 78 | 79 | get: (path, args) -> 80 | @activate() 81 | .then => @server.get path, args 82 | .then (id) => Promise.all [id, @getStreams(id), @server.events(id)] 83 | .then ([id, [stdin, stdout, stderr], events]) => 84 | stdin: (data) -> stdin.write data 85 | onStdout: (f) -> stdout.on 'data', f 86 | onStderr: (f) -> stderr.on 'data', f 87 | flush: (out, err) -> cycler.flush events, out, err 88 | interrupt: => @server.interrupt id 89 | kill: => @server.kill id 90 | socket: @getSocket id 91 | onExit: (f) => 92 | Promise.race [@server.onExit(id), 93 | new Promise (resolve) => @server.ipc.stream.on 'end', resolve] 94 | .then f 95 | 96 | start: (path, args) -> 97 | @activate() 98 | .then => @server.start path, args 99 | 100 | reset: -> 101 | @connect() 102 | .then (server) -> server.exit().catch -> 103 | .catch -> atom.notifications.addInfo 'No server running.' 104 | 105 | # Server 106 | 107 | initIPC: (sock) -> 108 | # TODO: exit once all clients close 109 | ipc = new IPC sock 110 | ipc.handle @serverAPI() 111 | @streamHandlers ipc 112 | ipc 113 | 114 | serve: -> 115 | cycler.cacheLength = 2 116 | basic.wrapperEnabled = -> true 117 | @server = net.createServer (sock) => 118 | @initIPC sock 119 | @server.listen @socketPath('juno-server'), -> 120 | process.send 'ready' 121 | @server.on 'error', (err) -> 122 | process.send err 123 | process.exit() 124 | 125 | pid: 0 126 | ps: {} 127 | 128 | serverAPI: -> 129 | 130 | setPS: (enabled) -> basic.wrapperEnabled = -> enabled 131 | 132 | get: (path, args) => 133 | cycler.get(path, args) 134 | .then (p) => 135 | p.id = (@pid += 1) 136 | @ps[p.id] = p 137 | p.id 138 | 139 | start: (path, args) -> cycler.start path, args 140 | 141 | onBoot: (id) => @ps[id].socket.then -> true 142 | onExit: (id) => new Promise (resolve) => @ps[id].onExit resolve 143 | onAttach: (id) => @ps[id].attached.then -> 144 | interrupt: (id) => @ps[id].interrupt() 145 | kill: (id) => @ps[id].kill() 146 | 147 | events: (id) => 148 | proc = @ps[id] 149 | events = proc.events ? [] 150 | delete proc.events 151 | for event in events 152 | event.data = event.data?.toString() 153 | events 154 | 155 | exit: => 156 | cycler.reset() 157 | for id, proc of @ps 158 | proc.kill() 159 | process.exit() 160 | 161 | crossStreams: (a, b) -> 162 | [[a, b], [b, a]].forEach ([from, to]) -> 163 | from.on 'data', (data) -> 164 | try to.write data 165 | catch e 166 | if process.connected 167 | process.send {type: 'error', message: e.message, stack: e.stack, data: data.toString()} 168 | 169 | mutualClose: (a, b) -> 170 | [[a, b], [b, a]].forEach ([from, to]) -> 171 | from.on 'end', -> to.end() 172 | from.on 'error', -> to.end() 173 | 174 | streamHandlers: (ipc) -> 175 | ['socket', 'stdout', 'stderr', 'stdin'].forEach (stream) => 176 | ipc.handle stream, (id) => 177 | proc = @ps[id] 178 | sock = ipc.stream 179 | ipc.unreadStream() 180 | source = if stream == 'socket' then proc.socket else proc.proc[stream] 181 | proc.attached = Promise.resolve(source).then (source) => 182 | @crossStreams source, sock 183 | if stream == 'socket' then @mutualClose source, sock 184 | else sock.on 'end', -> proc.kill() 185 | -------------------------------------------------------------------------------- /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 | @import "syntax-variables"; 7 | 8 | .julia { 9 | table { 10 | td { 11 | padding-left: 5px; 12 | } 13 | } 14 | .fade, &.fade, &.fade a { 15 | color: @text-color-subtle; 16 | } 17 | &.error, .error { 18 | color: @text-color-error; 19 | a { 20 | color: @text-color-error; 21 | } 22 | 23 | .body { 24 | color: @text-color-error; 25 | .error-description { 26 | color: @syntax-text-color; 27 | } 28 | span [style="color:#FFF"] { 29 | color: @syntax-text-color!important; 30 | } 31 | } 32 | } 33 | } 34 | 35 | atom-text-editor.editor { 36 | .syntax--keyword.syntax--end { 37 | opacity: 0.5; 38 | } 39 | } 40 | 41 | .error-trace { 42 | .dark { 43 | opacity: 0.6; 44 | } 45 | .medium { 46 | opacity: 0.8; 47 | } 48 | } 49 | 50 | .markdown { 51 | .admonition { 52 | margin: 1em 0; 53 | .admonition-title { 54 | margin: 0; 55 | padding: 1em 1em .5em 1em; 56 | font-weight: bold; 57 | 58 | &:before { 59 | margin-right: 0.5em; 60 | } 61 | } 62 | >.markdown { 63 | padding: 1em 0 0.5em 1em; 64 | } 65 | 66 | .admonition-title { 67 | background-color: mix(@background-color-info, @syntax-background-color, 15%); 68 | } 69 | .markdown { 70 | background-color: mix(@background-color-info, @syntax-background-color, 10%); 71 | } 72 | 73 | &.warning { 74 | .admonition-title { 75 | background-color: mix(@background-color-warning, @syntax-background-color, 15%); 76 | } 77 | .markdown { 78 | background-color: mix(@background-color-warning, @syntax-background-color, 10%); 79 | } 80 | } 81 | 82 | &.danger { 83 | .admonition-title { 84 | background-color: mix(@background-color-error, @syntax-background-color, 15%); 85 | } 86 | .markdown { 87 | background-color: mix(@background-color-error, @syntax-background-color, 10%); 88 | } 89 | } 90 | 91 | &.tip { 92 | .admonition-title { 93 | background-color: mix(@background-color-success, @syntax-background-color, 15%); 94 | } 95 | .markdown { 96 | background-color: mix(@background-color-success, @syntax-background-color, 10%); 97 | } 98 | } 99 | 100 | &.compat { 101 | .admonition-title { 102 | background-color: mix(@background-color-info, @syntax-selection-color, 20%); 103 | } 104 | .markdown { 105 | background-color: mix(@background-color-info, @syntax-selection-color, 15%); 106 | } 107 | } 108 | 109 | .title { 110 | font-weight: bold; 111 | } 112 | } 113 | } 114 | 115 | .julia-client-selector { 116 | .two-lines { 117 | padding: 0.25em 1em !important; 118 | .primary-line, .secondary-line { 119 | line-height: 1.8em; 120 | } 121 | } 122 | .error-message { 123 | color: @text-color-error; 124 | } 125 | .character-match { 126 | font-weight: bold; 127 | } 128 | } 129 | 130 | .julia-syntax-color-selector { 131 | color: @syntax-text-color; 132 | background-color: @syntax-background-color; 133 | opacity: 0; 134 | } 135 | 136 | .ink-canopy.julia-profile { 137 | .node { 138 | &.dynamic-dispatch > div { 139 | border: solid fade(@background-color-warning, 20%) 1px; 140 | background: fade(@background-color-warning, 10%); 141 | &:hover { 142 | background: fade(@background-color-warning, 20%); 143 | } 144 | } 145 | &.garbage-collection > div { 146 | border: solid fade(@background-color-error, 20%) 1px; 147 | background: fade(@background-color-error, 10%); 148 | &:hover { 149 | background: fade(@background-color-error, 20%); 150 | } 151 | } 152 | } 153 | } 154 | 155 | .ink-profile-line { 156 | &.dynamic-dispatch { 157 | background: fade(@background-color-warning, 20%)!important; 158 | } 159 | &.garbage-collection { 160 | background: fade(@background-color-error, 20%)!important; 161 | } 162 | } 163 | 164 | atom-text-editor.editor[data-grammar$="source julia"], 165 | atom-text-editor.editor[data-grammar$="source weave md"], 166 | atom-text-editor.editor[data-grammar$="source weave latex"] { 167 | autocomplete-suggestion-list.select-list.popover-list { 168 | .suggestion-description { 169 | line-height: 1; 170 | .suggestion-description-content { 171 | overflow-y : hidden; 172 | display : inline; 173 | white-space: normal; // can be useful to workaround https://github.com/atom/autocomplete-plus/issues/868 174 | } 175 | } 176 | } 177 | } 178 | 179 | /* Hack datatip component style (overwrite user's global style) */ 180 | // atom-ide-ui 181 | .datatip-marked { 182 | max-width: 500px; 183 | } 184 | .datatip-marked-text-editor-container { 185 | padding-left: @component-padding; 186 | } 187 | // atom-ide-datatip 188 | .datatip-marked-container { 189 | // @NOTE: This is only needed for atom-ide-datatip package and might be able to be removed in the future 190 | font-family: @font-family; 191 | max-width: 500px; 192 | } 193 | 194 | .line-number.julia-current-cell { 195 | background-image: linear-gradient(to right, rgba(255,255,0,0), 90%, fade(@syntax-text-color, 10%)); 196 | } 197 | 198 | .julia-cell-border { 199 | border-top: solid fade(@syntax-text-color, 80%) 1px; 200 | margin-top: -1px; 201 | } 202 | 203 | .ink-debug-toolbar { 204 | --color: @text-color; 205 | --color-subtle: @text-color-subtle; 206 | --color-highlight: @text-color-error; 207 | 208 | button { 209 | &.custom-svg-icon { 210 | display: flex; 211 | align-items: center; 212 | height: 2.5em!important; 213 | 214 | svg { 215 | height: 1.6em; 216 | margin: auto; 217 | } 218 | } 219 | } 220 | } 221 | 222 | .terminal-link-tooltip { 223 | z-index: 100; 224 | color: @text-color; 225 | background-color: @app-background-color; 226 | padding: 0.2em 0.5em; 227 | position: absolute; 228 | border: 1px solid fade(@text-color, 70%); 229 | } 230 | -------------------------------------------------------------------------------- /lib/runtime/modules.coffee: -------------------------------------------------------------------------------- 1 | # TODO: this code is awful, refactor 2 | 3 | {CompositeDisposable, Disposable, Emitter} = require 'atom' 4 | {debounce} = require 'underscore-plus' 5 | 6 | {client} = require '../connection' 7 | {show} = require '../ui/selector' 8 | 9 | {module: getmodule, allmodules, ismodule} = client.import ['module', 'allmodules', 'ismodule'] 10 | 11 | module.exports = 12 | 13 | activate: -> 14 | @subs = new CompositeDisposable 15 | @itemSubs = new CompositeDisposable 16 | @subs.add @emitter = new Emitter 17 | 18 | @subs.add atom.workspace.observeActivePaneItem (item) => @updateForItem item 19 | @subs.add client.onAttached => @updateForItem() 20 | @subs.add client.onDetached => @updateForItem() 21 | 22 | deactivate: -> 23 | @subs.dispose() 24 | 25 | _current: null 26 | lastEditorModule: null 27 | 28 | setCurrent: (@_current, editor) -> 29 | if editor then @lastEditorModule = @_current 30 | @emitter.emit 'did-change', @_current 31 | 32 | onDidChange: (f) -> @emitter.on 'did-change', f 33 | 34 | current: (m = @_current) -> 35 | return unless m? 36 | {main, inactive, sub, subInactive} = m 37 | if main is @follow then return @current @lastEditorModule 38 | if not main or inactive 39 | "Main" 40 | else if not sub or subInactive 41 | main 42 | else 43 | "#{main}.#{sub}" 44 | 45 | # Choosing Modules 46 | 47 | itemSelector: 'atom-text-editor[data-grammar="source julia"], .julia-console.julia, ink-terminal, .ink-workspace' 48 | 49 | isValidItem: (item) -> atom.views.getView(item)?.matches @itemSelector 50 | 51 | autodetect: 'Auto Detect' 52 | follow: 'Follow Editor' 53 | 54 | chooseModule: -> 55 | item = atom.workspace.getActivePaneItem() 56 | ised = atom.workspace.isTextEditor item 57 | return unless @isValidItem item 58 | client.require 'change modules', => 59 | if (item = atom.workspace.getActivePaneItem()) 60 | active = item.juliaModule or (if ised then @autodetect else 'Main') 61 | modules = allmodules().then (modules) => 62 | if ised 63 | modules.unshift @autodetect 64 | else if @lastEditorModule? 65 | modules.unshift @follow 66 | modules 67 | modules.catch (err) => 68 | console.log err 69 | show(modules, { active }).then (mod) => 70 | return unless mod? 71 | if mod is @autodetect 72 | delete item.juliaModule 73 | else 74 | item.juliaModule = mod 75 | item.setModule?(mod if mod isnt @autodetect) 76 | @updateForItem item 77 | 78 | updateForItem: (item = atom.workspace.getActivePaneItem()) -> 79 | @itemSubs.dispose() 80 | if not @isValidItem item 81 | @itemSubs.add item?.onDidChangeGrammar? => @updateForItem() 82 | @setCurrent() 83 | else if not client.isActive() 84 | @setCurrent main: 'Main', inactive: true 85 | else if atom.workspace.isTextEditor item 86 | @updateForEditor item 87 | else 88 | mod = item.juliaModule or 'Main' 89 | ismodule(mod) 90 | .then (ismod) => 91 | @setCurrent main: mod, inactive: !ismod 92 | .catch (err) => 93 | console.log err 94 | 95 | updateForEditor: (editor) -> 96 | @setCurrent main: editor.juliaModule or 'Main', true 97 | @setEditorModule editor 98 | @itemSubs.add editor.onDidChangeCursorPosition => 99 | @setEditorModuleLazy editor 100 | 101 | getEditorModule: (ed, bufferPosition = null) -> 102 | return unless client.isActive() 103 | if bufferPosition 104 | {row, column} = bufferPosition 105 | else 106 | sels = ed.getSelections() 107 | {row, column} = sels[sels.length - 1].getBufferRange().end 108 | data = 109 | path: client.editorPath(ed) 110 | code: ed.getText() 111 | row: row+1, column: column+1 112 | module: ed.juliaModule 113 | getmodule(data) 114 | .catch (err) => 115 | console.log err 116 | 117 | setEditorModule: (ed) -> 118 | modulePromise = @getEditorModule ed 119 | return unless modulePromise 120 | modulePromise.then (mod) => 121 | if atom.workspace.getActivePaneItem() is ed 122 | @setCurrent mod, true 123 | 124 | setEditorModuleLazy: debounce ((ed) -> @setEditorModule(ed)), 100 125 | 126 | # The View 127 | 128 | activateView: -> 129 | @onDidChange (c) => @updateView c 130 | 131 | @dom = document.createElement 'span' 132 | @dom.classList.add 'julia', 'inline-block' 133 | 134 | @mainView = document.createElement 'a' 135 | @dividerView = document.createElement 'span' 136 | @subView = document.createElement 'span' 137 | 138 | @dom.appendChild x for x in [@mainView, @dividerView, @subView] 139 | 140 | @mainView.onclick = => 141 | atom.commands.dispatch atom.views.getView(atom.workspace.getActivePaneItem()), 142 | 'julia-client:set-working-module' 143 | 144 | atom.tooltips.add @dom, 145 | title: => "Currently working in module #{@current()}" 146 | 147 | # @NOTE: Grammar selector has `priority` 10 and thus set the it to a bit lower 148 | # than that to avoid collision that may cause unexpected result. 149 | @tile = @statusBar.addRightTile item: @dom, priority: 5 150 | disposable = new Disposable(=> 151 | @tile.destroy() 152 | delete @tile) 153 | @subs.add(disposable) 154 | disposable 155 | 156 | updateView: (m = @_current) -> 157 | return unless @tile? 158 | if not m? 159 | @dom.style.display = 'none' 160 | else 161 | {main, sub, inactive, subInactive} = m 162 | if main is @follow 163 | return @updateView @lastEditorModule 164 | @dom.style.display = '' 165 | @mainView.innerText = 'Module: ' + (main or 'Main') 166 | if sub 167 | @subView.innerText = sub 168 | @dividerView.innerText = '/' 169 | else 170 | view.innerText = '' for view in [@subView, @dividerView] 171 | if inactive 172 | @dom.classList.add 'fade' 173 | else 174 | @dom.classList.remove 'fade' 175 | for view in [@subView, @dividerView] 176 | if subInactive 177 | view.classList.add 'fade' 178 | else 179 | view.classList.remove 'fade' 180 | 181 | consumeStatusBar: (bar) -> 182 | @statusBar = bar 183 | disposable = @activateView() 184 | @updateView @_current 185 | disposable 186 | -------------------------------------------------------------------------------- /lib/runtime/goto.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import fs from 'fs' 5 | import { CompositeDisposable, Range } from 'atom' 6 | 7 | import { client } from '../connection' 8 | import modules from './modules' 9 | import { isValidScopeToInspect } from '../misc/scopes' 10 | import { 11 | getWordAndRange, 12 | getWordRangeAtBufferPosition, 13 | getWordRangeWithoutTrailingDots, 14 | isValidWordToInspect 15 | } from '../misc/words' 16 | import { getLocalContext } from '../misc/blocks' 17 | import { show } from '../ui/selector' 18 | 19 | const { 20 | gotosymbol: gotoSymbol, 21 | regeneratesymbols: regenerateSymbols, 22 | clearsymbols: clearSymbols, 23 | } = client.import(['gotosymbol', 'regeneratesymbols', 'clearsymbols']) 24 | 25 | const includeRegex = /(include|include_dependency)\(".+\.jl"\)/ 26 | const filePathRegex = /".+\.jl"/ 27 | 28 | class Goto { 29 | activate (ink) { 30 | this.ink = ink 31 | this.subscriptions = new CompositeDisposable() 32 | this.subscriptions.add( 33 | atom.commands.add('atom-workspace', 'julia-client:regenerate-symbols-cache', () => { 34 | regenerateSymbols() 35 | }), 36 | atom.commands.add('atom-workspace', 'julia-client:clear-symbols-cache', () => { 37 | clearSymbols() 38 | }) 39 | ) 40 | } 41 | 42 | deactivate () { 43 | this.subscriptions.dispose() 44 | } 45 | 46 | getJumpFilePath(editor, bufferPosition) { 47 | const includeRange = getWordRangeAtBufferPosition(editor, bufferPosition, { 48 | wordRegex: includeRegex 49 | }) 50 | if (includeRange.isEmpty()) return false 51 | 52 | // return if the bufferPosition is not on the path string 53 | const filePathRange = getWordRangeAtBufferPosition(editor, bufferPosition, { 54 | wordRegex: filePathRegex 55 | }) 56 | if (filePathRange.isEmpty()) return false 57 | 58 | const filePathText = editor.getTextInBufferRange(filePathRange) 59 | const filePathBody = filePathText.replace(/"/g, '') 60 | const dirPath = path.dirname(editor.getPath()) 61 | const filePath = path.join(dirPath, filePathBody) 62 | 63 | // return if there is not such a file exists 64 | if (!fs.existsSync(filePath)) return false 65 | return { range: filePathRange, filePath } 66 | } 67 | 68 | isClientAndInkReady () { 69 | return client.isActive() && this.ink !== undefined 70 | } 71 | 72 | // TODO: handle remote files ? 73 | selectItemsAndGo (items) { 74 | if (items.length === 0) return 75 | if (items.length === 1) { 76 | const item = items[0] 77 | return this.ink.Opener.open(item.file, item.line, { 78 | pending: atom.config.get('core.allowPendingPaneItems') 79 | }) 80 | } 81 | items = items.map(result => { 82 | result.primary = result.text 83 | result.secondary = `${result.file}:${result.line}` 84 | return result 85 | }) 86 | return show(items).then(item => { 87 | if (!item) return 88 | this.ink.Opener.open(item.file, item.line, { 89 | pending: atom.config.get('core.allowPendingPaneItems') 90 | }) 91 | }) 92 | } 93 | 94 | gotoSymbol () { 95 | const editor = atom.workspace.getActiveTextEditor() 96 | const bufferPosition = editor.getCursorBufferPosition() 97 | 98 | // file jumps 99 | const rangeFilePath = this.getJumpFilePath(editor, bufferPosition) 100 | if (rangeFilePath) { 101 | const { filePath } = rangeFilePath 102 | return this.ink.Opener.open(filePath, 0, { 103 | pending: atom.config.get('core.allowPendingPaneItems'), 104 | }) 105 | } 106 | 107 | if (!this.isClientAndInkReady()) return 108 | 109 | // get word without trailing dot accessors at the buffer position 110 | let { word, range } = getWordAndRange(editor, { 111 | bufferPosition 112 | }) 113 | range = getWordRangeWithoutTrailingDots(word, range, bufferPosition) 114 | word = editor.getTextInBufferRange(range) 115 | 116 | // check the validity of code to be inspected 117 | if (!(isValidWordToInspect(word))) return 118 | 119 | // local context 120 | const { column, row } = bufferPosition 121 | const { context, startRow } = getLocalContext(editor, row) 122 | 123 | // module context 124 | const currentModule = modules.current() 125 | const mod = currentModule ? currentModule : 'Main' 126 | const text = editor.getText() // buffer text that will be used for fallback entry 127 | 128 | return gotoSymbol({ 129 | word, 130 | path: editor.getPath() || 'untitled-' + editor.getBuffer().getId(), 131 | // local context 132 | column: column + 1, 133 | row: row + 1, 134 | startRow, 135 | context, 136 | onlyGlobal: false, 137 | // module context 138 | mod, 139 | text 140 | }).then(results => { 141 | if (results.error) return 142 | this.selectItemsAndGo(results.items) 143 | }).catch(err => { 144 | console.log(err) 145 | }) 146 | } 147 | 148 | provideHyperclick () { 149 | const getSuggestion = async (textEditor, bufferPosition) => { 150 | // file jumps -- invoked even if Julia isn't running 151 | const rangeFilePath = this.getJumpFilePath(textEditor, bufferPosition) 152 | if (rangeFilePath) { 153 | const { range, filePath } = rangeFilePath 154 | return { 155 | range, 156 | callback: () => { 157 | return this.ink.Opener.open(filePath, 0, { 158 | pending: atom.config.get('core.allowPendingPaneItems'), 159 | }) 160 | } 161 | } 162 | } 163 | 164 | // If Julia is not running, do nothing 165 | if (!this.isClientAndInkReady()) return 166 | 167 | // If the scope at `bufferPosition` is not valid code scope, do nothing 168 | if (!isValidScopeToInspect(textEditor, bufferPosition)) return 169 | 170 | // get word without trailing dot accessors at the buffer position 171 | let { word, range } = getWordAndRange(textEditor, { 172 | bufferPosition 173 | }) 174 | range = getWordRangeWithoutTrailingDots(word, range, bufferPosition) 175 | word = textEditor.getTextInBufferRange(range) 176 | 177 | // check the validity of code to be inspected 178 | if (!(isValidWordToInspect(word))) return 179 | 180 | // local context 181 | const { column, row } = bufferPosition 182 | const { context, startRow } = getLocalContext(textEditor, row) 183 | 184 | // module context 185 | const { main, sub } = await modules.getEditorModule(textEditor, bufferPosition) 186 | const mod = main ? (sub ? `${main}.${sub}` : main) : 'Main' 187 | const text = textEditor.getText() // buffer text that will be used for fallback entry 188 | 189 | return new Promise((resolve) => { 190 | gotoSymbol({ 191 | word, 192 | path: textEditor.getPath() || 'untitled-' + textEditor.getBuffer().getId(), 193 | // local context 194 | column: column + 1, 195 | row: row + 1, 196 | startRow, 197 | context, 198 | onlyGlobal: false, 199 | // module context 200 | mod, 201 | text 202 | }).then(results => { 203 | // If the `goto` call fails or there is no where to go to, do nothing 204 | if (results.error) { 205 | resolve({ 206 | range: new Range([0,0], [0,0]), 207 | callback: () => {} 208 | }) 209 | } 210 | resolve({ 211 | range, 212 | callback: () => setTimeout(() => this.selectItemsAndGo(results.items), 5) 213 | }) 214 | }).catch(err => { 215 | console.log(err) 216 | }) 217 | }) 218 | } 219 | 220 | return { 221 | providerName: 'julia-client-hyperclick-provider', 222 | priority: 100, 223 | grammarScopes: atom.config.get('julia-client.juliaSyntaxScopes'), 224 | getSuggestion 225 | } 226 | } 227 | } 228 | 229 | export default new Goto() 230 | -------------------------------------------------------------------------------- /lib/runtime/evaluation.coffee: -------------------------------------------------------------------------------- 1 | # TODO: this is very horrible, refactor 2 | path = require 'path' 3 | {dialog, BrowserWindow} = require('electron').remote 4 | 5 | {client} = require '../connection' 6 | {notifications, views, selector, docpane} = require '../ui' 7 | {paths, blocks, cells, words, weave} = require '../misc' 8 | {processLinks} = require '../ui/docs' 9 | workspace = require './workspace' 10 | modules = require './modules' 11 | { 12 | eval: evaluate, evalall, evalshow, module: getmodule, 13 | cd, clearLazy, activateProject, activateParentProject, activateDefaultProject 14 | } = client.import 15 | rpc: ['eval', 'evalall', 'evalshow', 'module'], 16 | msg: ['cd', 'clearLazy', 'activateProject', 'activateParentProject', 'activateDefaultProject'] 17 | searchDoc = client.import('docs') 18 | 19 | module.exports = 20 | _currentContext: -> 21 | editor = atom.workspace.getActiveTextEditor() 22 | mod = modules.current() || 'Main' 23 | edpath = client.editorPath(editor) || 'untitled-' + editor.getBuffer().id 24 | {editor, mod, edpath} 25 | 26 | _showError: (r, lines) -> 27 | @errorLines?.lights.destroy() 28 | lights = @ink.highlights.errorLines (file: file, line: line-1 for {file, line} in lines) 29 | @errorLines = {r, lights} 30 | r.onDidDestroy => 31 | if @errorLines?.r == r then @errorLines.lights.destroy() 32 | 33 | eval: ({move, cell}={}) -> 34 | {editor, mod, edpath} = @_currentContext() 35 | codeSelector = if cell? then cells else blocks 36 | # global options 37 | resultsDisplayMode = atom.config.get('julia-client.uiOptions.resultsDisplayMode') 38 | errorInRepl = atom.config.get('julia-client.uiOptions.errorInRepl') 39 | scrollToResult = atom.config.get('julia-client.uiOptions.scrollToResult') 40 | 41 | Promise.all codeSelector.get(editor).map ({range, line, text, selection}) => 42 | codeSelector.moveNext editor, selection, range if move 43 | [[start], [end]] = range 44 | @ink.highlight editor, start, end 45 | rtype = resultsDisplayMode 46 | if cell and not (rtype is 'console') 47 | rtype = 'block' 48 | if rtype is 'console' 49 | evalshow({text, line: line+1, mod, path: edpath}) 50 | notifications.show "Evaluation Finished" 51 | workspace.update() 52 | else 53 | r = null 54 | setTimeout (=> r ?= new @ink.Result editor, [start, end], {type: rtype, scope: 'julia', goto: scrollToResult}), 0.1 55 | evaluate({text, line: line+1, mod, path: edpath, errorInRepl}) 56 | .catch -> r?.destroy() 57 | .then (result) => 58 | if not result? 59 | r?.destroy() 60 | console.error 'Error: Something went wrong while evaluating.' 61 | return 62 | error = result.type == 'error' 63 | view = if error then result.view else result 64 | if not r? or r.isDestroyed then r = new @ink.Result editor, [start, end], {type: rtype, scope: 'julia', goto: scrollToResult} 65 | registerLazy = (id) -> 66 | r.onDidDestroy client.withCurrent -> clearLazy [id] 67 | editor.onDidDestroy client.withCurrent -> clearLazy id 68 | r.setContent views.render(view, {registerLazy}), {error} 69 | if error 70 | atom.beep() if error 71 | @ink.highlight editor, start, end, 'error-line' 72 | if result.highlights? 73 | @_showError r, result.highlights 74 | notifications.show "Evaluation Finished" 75 | workspace.update() 76 | result 77 | 78 | evalAll: (el) -> 79 | if el 80 | path = paths.getPathFromTreeView el 81 | if not path 82 | return atom.notifications.addError 'This file has no path.' 83 | try 84 | code = paths.readCode(path) 85 | data = 86 | path: path 87 | code: code 88 | row: 1 89 | column: 1 90 | getmodule(data) 91 | .then (mod) => 92 | evalall({ 93 | path: path 94 | module: modules.current mod 95 | code: code 96 | }) 97 | .then (result) -> 98 | notifications.show "Evaluation Finished" 99 | workspace.update() 100 | .catch (err) => 101 | console.log(err) 102 | .catch (err) => 103 | console.log(err) 104 | 105 | catch error 106 | atom.notifications.addError 'Error happened', 107 | detail: error 108 | dismissable: true 109 | else 110 | {editor, mod, edpath} = @_currentContext() 111 | atom.commands.dispatch atom.views.getView(editor), 'inline-results:clear-all' 112 | [scope] = editor.getRootScopeDescriptor().getScopesArray() 113 | weaveScopes = ['source.weave.md', 'source.weave.latex'] 114 | module = if weaveScopes.includes scope then mod else editor.juliaModule 115 | code = if weaveScopes.includes scope then weave.getCode editor else editor.getText() 116 | evalall({ 117 | path: edpath 118 | module: module 119 | code: code 120 | }) 121 | .then (result) -> 122 | notifications.show "Evaluation Finished" 123 | workspace.update() 124 | .catch (err) => 125 | console.log(err) 126 | 127 | toggleDocs: () -> 128 | { editor, mod, edpath } = @_currentContext() 129 | bufferPosition = editor.getLastCursor().getBufferPosition() 130 | # get word without trailing dot accessors at the buffer position 131 | { word, range } = words.getWordAndRange(editor, { bufferPosition }) 132 | range = words.getWordRangeWithoutTrailingDots(word, range, bufferPosition) 133 | word = editor.getTextInBufferRange(range) 134 | 135 | return unless words.isValidWordToInspect(word) 136 | searchDoc({word: word, mod: mod}) 137 | .then (result) => 138 | if result.error then return 139 | v = views.render result 140 | processLinks(v.getElementsByTagName('a')) 141 | if atom.config.get('julia-client.uiOptions.docsDisplayMode') == 'inline' 142 | d = new @ink.InlineDoc editor, range, 143 | content: v 144 | highlight: true 145 | d.view.classList.add 'julia' 146 | else 147 | docpane.ensureVisible() 148 | docpane.showDocument(v, []) 149 | .catch (err) => 150 | console.log(err) 151 | 152 | # Working Directory 153 | 154 | _cd: (dir) -> 155 | if atom.config.get('julia-client.juliaOptions.persistWorkingDir') 156 | atom.config.set('julia-client.juliaOptions.workingDir', dir) 157 | cd(dir) 158 | 159 | cdHere: (el) -> 160 | dir = @currentDir(el) 161 | if dir 162 | @_cd(dir) 163 | 164 | activateProject: (el) -> 165 | dir = @currentDir(el) 166 | if dir 167 | activateProject(dir) 168 | 169 | activateParentProject: (el) -> 170 | dir = @currentDir(el) 171 | if dir 172 | activateParentProject(dir) 173 | 174 | activateDefaultProject: -> 175 | activateDefaultProject() 176 | 177 | currentDir: (el) -> 178 | dirPath = paths.getDirPathFromTreeView el 179 | return dirPath if dirPath 180 | # invoked from normal command usage 181 | file = client.editorPath(atom.workspace.getCenter().getActiveTextEditor()) 182 | return path.dirname(file) if file 183 | atom.notifications.addError 'This file has no path.' 184 | return null 185 | 186 | cdProject: -> 187 | dirs = atom.project.getPaths() 188 | if dirs.length < 1 189 | atom.notifications.addError 'This project has no folders.' 190 | else if dirs.length == 1 191 | @_cd dirs[0] 192 | else 193 | selector.show(dirs, { infoMessage: 'Select project to work in' }) 194 | .then (dir) => 195 | return unless dir? 196 | @_cd dir 197 | .catch (err) => 198 | console.log(err) 199 | 200 | cdHome: -> 201 | @_cd paths.home() 202 | 203 | cdSelect: -> 204 | opts = properties: ['openDirectory'] 205 | dialog.showOpenDialog BrowserWindow.getFocusedWindow(), opts, (path) => 206 | if path? then @_cd path[0] 207 | --------------------------------------------------------------------------------