Skip to content

Commit

Permalink
clojure support! also lots of bugfixes and bullet-proofing. also a te…
Browse files Browse the repository at this point in the history
…st suite!
  • Loading branch information
jaredly committed Apr 29, 2015
1 parent d796c1d commit 52ced7a
Show file tree
Hide file tree
Showing 19 changed files with 815 additions and 30 deletions.
1 change: 1 addition & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
module.exports = {
extensions: {
// clojurescript: require('./ext/clojurescript'),
clojure: require('./build/ext/clojure'),
clojurescript: require('./build/ext/clojurescript'),
coffee: require('./build/ext/coffee'),
babel: require('./build/ext/babel')
Expand Down
171 changes: 171 additions & 0 deletions ext/clojure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@

const net = require('net')
const fs = require('fs')
const path = require('path')
const bencode = require('bencode')
const {EventEmitter} = require('events')

function bocket(sock) {
const em = new EventEmitter()
let buffer = new Buffer('')
sock.on('connect', () => em.emit('connect'))
sock.on('data', data => {
buffer = Buffer.concat([buffer, data])
while (buffer.length) {
let res
try {
res = bencode.decode(buffer, 'utf8')
} catch (err) {
return
}
let used = bencode.encode(res, 'utf8').length
buffer = buffer.slice(used)
em.emit('data', res)
}
})
sock.on('error', err => em.emit('error', err))
return {
on(val, fn) {
return em.on(val, fn)
},
off(val, fn) {
return em.removeListener(val, fn)
},
close() {
sock.close()
},
send(val) {
return sock.write(bencode.encode(val))
},
_sock: sock
}
}

class Session extends EventEmitter {
constructor(port, session) {
super()
this.bock = bocket(net.connect({port}))
this.waiting = []
this.queue = []
this.session = session || null
this.bock.on('data', this.onData.bind(this))
this.bock.on('connect', () => {
if (!this.session) {
this.send({op: 'clone'}, data => {
this.session = data[0]['new-session']
console.log(data)
this.emit('connect')
})
} else {
this.emit('connect')
}
})
}

send(data, done) {
if (this.session) {
data.session = this.session
}
this.waiting.push(done)
this.bock.send(data)
}

eval(val, done) {
this.send({op: 'eval', code: val}, done)
}

op(name, args, done) {
if (arguments.length === 2) {
done = args
args = {}
}
args = args || {}
args.op = name
this.send(args, done)
}

onData(data) {
if (!this.waiting.length) {
return console.error('Data, came, but no one waited...', data)
}
if (this.waiting[0].partial) {
this.waiting[0].partial(data)
}
this.queue.push(data)
if (data.status && data.status[0] === 'done'){
if (this.waiting[0].final) {
this.waiting.shift().final(this.queue)
} else {
this.waiting.shift()(this.queue)
}
this.queue = []
}
}

e(val) {
this.eval(val, data => data.map(d => console.log(JSON.stringify(d, null, 2))))
}
}

export default function extension(ctx, args, done) {
let port = args && parseInt(args[0])
if (!port) {
return done(new Error("Port is required"))
}
let s
try {
s = new Session(port)
} catch (e) {
return done(e)
}
s.on('connect', () => {
s.eval(`(do (use 'clj-info) (use 'clojure.repl) (use 'complete.core))`, data => {
done(null, {block:{clojure:{
execute(ctx, args, code, out, done) {
s.eval(`(do ${code})`, {
partial: data => {
if (data.out) {
out.stream('stdout', data.out)
} else if (data.value) {
out.output({'text/plain': data.value})
} else if (data.err) {
// out.error('Runtime error', 'err', data.err)
}
},
final: data => {
if (data[0].status && data[0].status[0] === 'eval-error') {
return done(new Error("Eval failed: " + data[1].err))
}
done(null)
}
})
},
complete(ctx, code, pos, done) {
const line = code.slice(0, pos).split('\n').pop()
const chunks = line.split(/[\s()\[{}\]]/g)
const last = chunks[chunks.length - 1]
// console.log('Completing:', JSON.stringify([code, pos], null, 2), last, last.length)
s.eval(`(map symbol (completions "${last}"))`, data => {
if (data[0].value) {
const res = {
matches: data[0].value.slice(1, -1).split(' '),
status: 'ok',
cursor_start: pos - last.length,
cursor_end: pos,
}
done(null, res)
} else {
console.log('Matches failed!')
data.forEach(d => console.log(JSON.stringify(d, null, 2)))
done(null, {
status: 'err'
})
}
})
}
}}})
})
})
}


118 changes: 108 additions & 10 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ import config from '../config'
import getCompletions from './complete'
import loadExt from './load-ext'
import async from 'async'
import {spawn} from 'child_process'
import fs from 'fs'
import path from 'path'

export default class Context {
constructor(wired) {
this.wired = wired
this.ctx = {
require,
require(name) {
const full = path.resolve(path.join('node_modules', name))
if (fs.existsSync(full)) {
return require(full)
}
return require(name)
},
Buffer,
setTimeout,
setInterval,
itreed: this,
process,
}
Expand All @@ -35,11 +47,31 @@ export default class Context {
lineMagic(line, out, done) {
const parts = line.trim().split(/\s+/g)
if (!this.magics.line[parts[0]]) {
return done(new Error(`Unknown magic: ${parts[0]}`))
return done(new Error(`Unknown line magic: ${parts[0]}`))
}
this.magics.line[parts[0]](this, out, parts.slice(1), done)
}

magicComplete(line, code, pos, done) {
const parts = line.trim().split(/\s+/g)
if (!this.magics.block[parts[0]]) {
return false
}
const magic = this.magics.block[parts[0]]
const args = parts.slice(1)
if (!magic.complete) return false
var d = require('domain').create()
d.on('error', err => {
console.log('async error', err)
console.log('async error', err.stack)
done(err)
})
d.run(() => {
magic.complete(this, code, pos, done)
})
return true
}

blockMagic(line, block, out, done) {
const parts = line.trim().split(/\s+/g)
if (!this.magics.block[parts[0]]) {
Expand All @@ -49,6 +81,8 @@ export default class Context {
const args = parts.slice(1)
if ('function' === typeof magic) {
return magic(this, args, block, out, done)
} else if (magic.execute) {
return magic.execute(this, args, block, out, done)
} else if (magic.transform) {
return magic.transform(this, args, block, out, (err, code) => {
if (err) return done(err)
Expand Down Expand Up @@ -78,16 +112,42 @@ export default class Context {

rawEvaluate(code, out, done) {
let result
try {
result = this.rawRun(code)
} catch (error) {
return done(error)
}
// TODO allow custom formatters
return done(null, result !== undefined ? format(result) : null)
var d = require('domain').create()
d.on('error', err => {
console.log('async error', err)
out.error('Async Error', err.message, err.stack)
})
d.run(() => {
try {
result = this.rawRun(code)
} catch (error) {
return done(error)
}
// TODO allow custom formatters
return done(null, result !== undefined ? format(result) : null)
})
}

complete(code, pos, done) {
complete(code, pos, variant, done) {
let off = 0
if (code.match(/^%%/)) {
const lines = code.split('\n')
const first = lines.shift()
variant = first.slice(2)
code = lines.join('\n')
off = first.length + 1
}
if (variant) {
if (this.magicComplete(variant, code, pos - off, (err, res) => {
if (res) {
res.cursor_start += off
res.cursor_end += off
}
done(err, res)
})) {
return
}
}
// TODO allow extension to the completion logic? e.g. for clojurescript
try {
return getCompletions(this.magics, this.ctx.getGlobal(), code, pos, done)
Expand Down Expand Up @@ -118,10 +178,48 @@ export default class Context {
}
}

shell(code, out, done) {
const proc = spawn('sh', ['-x', '-c', code])
proc.stdout.on('data', data => out.stream('stdout', data.toString()))
proc.stderr.on('data', data => out.stream('stderr', data.toString()))
proc.on('close', code => {
done(code !== 0 ? new Error(`Exit code: ${code}`) : null)
})
}

cd(code, out, done) {
code = code.replace(/~/g, process.env.HOME)
try {
process.chdir(code)
} catch (e) {
return done(new Error(`No such dir: ${code}`))
}
out.stream('stdout', 'cd to ' + code)
done()
}

checkSpecial(code, out, done) {
if (code.match(/^%%/)) {
code = code.split('\n').slice(1).join('\n')
}

if (code[0] === '!' && code.indexOf('\n') === -1) {
this.shell(code.slice(1), out, done)
return true
}

if (code.indexOf('cd ') === 0 && code.indexOf('\n') === -1) {
this.cd(code.slice('cd '.length), out, done)
return true
}
}

execute(code, out, done) {
if (this.dead) throw new Error('Kernel has been shutdown. Cannot execute')
this.outToContext(out)

if (this.checkSpecial(code, out, done)) return

if (code.match(/^%%/)) {
const lines = code.split('\n')
const first = lines.shift()
Expand Down
Loading

0 comments on commit 52ced7a

Please sign in to comment.