├── 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 | [](https://gitter.im/JunoLab/Juno) [](https://travis-ci.org/JunoLab/atom-julia-client) [](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 += "#{if block then "div" else "span"}>"
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 | * 
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 | * 
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 | 
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 |
--------------------------------------------------------------------------------