diff --git a/README.md b/README.md
index 765294c..e51226d 100644
--- a/README.md
+++ b/README.md
@@ -27,22 +27,33 @@ Please refer to the [./doc](./doc) for features, architecture, contributing, etc
## Quick Start
-Install:
-
- $ npm install
-Run:
-
- $ npm start
+###Install:
+
+ cd puddle-server
+ npm install
+ cd ..
+ cd puddle-editor
+ npm install
+
+###Run:
+ Two terminal windows:
+
+ One:
+
+ cd puddle-server
+ npm run develop # Ctrl-C to stop
+
+ Two:
+
+ cd puddle-editor
+ npm run develop # Ctrl-C to stop
Then navigate to
-## Develop mode:
-
- $ npm run develop-editor # Ctrl-C to stop
-
-In another terminal
+## Deploy mode:
+
+ Same as above but you can use `npm start` istead of `npm run develop`
- $ npm run develop-server # Ctrl-C to stop
diff --git a/doc/reference.md b/doc/reference.md
index b88e68a..dc041f8 100644
--- a/doc/reference.md
+++ b/doc/reference.md
@@ -5,20 +5,18 @@
* [Server socket.io API](#server-socketio)
* [Client `TODO.js`](#todojs)
* [Client `assert.js`](#assertjs)
-* [Client `log.js`](#logjs)
-* [Client `keycode.js`](#keycodejs)
-* [Client `test.js`](#testjs)
+* [Client `trace.js`](#tracejs)
* [Client `corpus.js`](#corpusjs)
-* [Client `navigate.js`](#navigatejs)
+* [Client `forest.js`](#forestjs)
+* [Client `cursor.js`](#cursorjs)
* [Client `view.js`](#viewjs)
-* [Client `menu.js`](#menujs)
-* [Client `editor.js`](#editorjs)
-* [Client `main.js`](#mainjs)
+* [Client `menu-builder.js`](#menubuilder)
+* [Client `menu-renderer.js`](#menurenderer)
Related documentation:
-* [puddle-syntax](https://github.com/fritzo/puddle-syntax)
-* [pomagma client](https://github.com/fritzo/pomagma/blob/master/doc/client.md)
+* [puddle-syntax](https://github.com/pomagma/puddle-syntax)
+* [pomagma client](https://github.com/pomagma/blob/master/doc/client.md)
## Server
@@ -131,118 +129,73 @@ Related documentation:
var assert = require('./assert');
assert(condition, message);
assert.equal(actual, expected);
- assert.forward(fwd, listOfPairs); // fwd(x) === y for each pair [x, y]
- assert.backward(bwd, listOfPairs); // x === bwd(y) for each pair [x, y]
- assert.inverses(fwd, bwd, listOfPairs); // fwd(x) === y && x === bwd(y)
-### `log.js`
+### `trace.js`
- var log = require('./log');
- log('just like console.log, bug works in web workers');
+Convenience method for debugging.
+Allows to quikly show trace and arguments passed into function.
-### `keycode.js`
-
-Just a static dictionary of ascii key codes.
-
- {
- 'backspace': 8,
- 'tab': 9,
- 'enter': 13,
- 'shift': 16,
- ...
- }
-
-### `test.js`
-
-Unit testing library.
-
- var test = require('./test');
-
- test('test title', callback); // declares synchronous test
- test.async('test title', callback); // declares async test
- test.runAll(); // run all unit tests
-
- console.log(test.testing()); // prints whether tests are being run
- console.log(test.hasRun()); // prints whether tests have finished
- console.log(test.testCount()); // prints cumulative test count
- console.log(test.failCount()); // prints cumulative failed test count
-
-Utilities for performing functions on trees.
-
- var root = arborist.getRoot(indexed);
- var varList = arborist.getBoundAbove(term); // -> ['a', 'b']
- var varSet = arborist.getVars(term); // -> {'a': null, 'b': null}
- var name = arborist.getFresh(term); // -> 'c'
+ var debug = require('debug')('puddle:editor');
+ var trace = require('./trace')(debug);
+
+ //Will output to console 'puddle:editor Hello'
+ //arguments converted to array.
+ //if third parameter is true will do console.trace() as well.
+ //Second and third arguments are optional
+ trace('Hello', arguments, true)
+
### `corpus.js`
+
+ Uses puddle-socket to fetch data from server
+ Stores corpus as `lines` internally
+ Provides knowledge of defenitions/occurrences
+ Fetches validities, emits 'updateValidity' event.
+ Exposes CRUD API and emits CRUD events for corpus lines.
+ Handles checkin/checkout of lines (w/o real blocking on server yet)
+ Does not care about any other modules.
+
+
+### `forest.js`
+
+ Stores corpus as trees internally
+ Keeps trees sorted
+ Listens for CRUD events from Corpus and reemits them as trees
+
+
+### `cursor.js`
+
+ Stores/shares pointer to node in a Forest.
+ Does not affect nodes in any way.
+ Exposes various methods to move itself between nodes of Forest
+ Notifies corpus of check-ins, check-outs
+ Exposes .replaceBelow to alter trees.
+ Listens to Forest changes to avoid being in orphan nodes.
+ Emits `move` event.
-Editor's view of the server-side corpus.
-Each client stores an in-memory copy.
-
- var corpus = require('./corpus');
- corpus.ready(cb); // calls cb after client loads corpus from server
- corpus.validate(); // validates corpus, throws AssertError if invalid
- var line = corpus.findLine(id);
- var lines = corpus.findAllLines();
- var names = corpus.findAllNames();
- var id = corpus.findDefinition(name);
- if (corpus.canDefine(name)) {
- // we can create a new definition of name
- }
- var ids = corpus.findOccurrences(name);
- if (corpus.hasOccurrences(name)) {
- // we cannot delete the definition of name
- }
- corpus.insert(line, done, fail);
- corpus.update(newLine);
- corpus.remove(id);
-
-### `navigate.js`
-
- var navigate = require('./navagate');
- navigate.on(name, callback, description); // add callback
- navigate.off(); // clear callbacks
- navigate.trigger(event);
-
- // search for global variable
- navigate.search(rankedStrings, acceptMatch, cancel, renderString);
-
- // create new global variable name
- navigate.choose(isValidFilter, acceptName, cancel);
### `view.js`
-The view object is the pane on the left showing the corpus.
-
- var view = require('./view');
- view.init({
- getLine: ...,
- getValidity: ...,
- lines: ...
- });
-
- view.insertAfter(prevId, id);
- view.update(id);
- view.remove(id);
-
-### `menu.js`
-
-The menu object is the pane on the right.
-It rebuilds itself at every action.
-The menu is the sole form of input to puddle, by design.
+ The view object is the pane on the left showing the corpus.
+ Renders corpus using trees from Forest
+ Listens for forest CRUD events.
+ Listens for cursor moves.
+ Listens for validities update.
+ Uses DOM as internal state.
+ Does not have API
- var menu = require('./menu');
- menu.init({
- actions = {...}, // callbacks bound to actions
- getCursor = function () {...} // returns cursor
- });
-### `editor.js`
+### `menu-builder.js`
+
+ Aware of cursor position in a forest
+ Knows mapping of keys <=> UI actions
+ Builds UI action functions using Forest and Cursor API and module.exports them
+ Logs every user action to server via socket.io
+ Does not have internal state.
- var editor = require('./editor');
- editor.main(); // start puddle editor
-### `main.js`
+### `menu-renderer.js`
-Main entry point, either starts unit tests or starts editor,
-depending on whether `location.hash === '#test'`.
+ Takes array of actions/keys from menu-builder and renders it into HTML with onKeyPress events.
+ Listens for Cursor 'move' event to rerender menu.
+ Does not have internal state or API
\ No newline at end of file
diff --git a/package.json b/package.json
index 4e77c7d..cc3c8de 100644
--- a/package.json
+++ b/package.json
@@ -4,10 +4,7 @@
"description": "A responsive editor built on Pomagma",
"main": "",
"scripts": {
- "test": "cd $MODULE; npm install; npm test;",
- "start": "cd puddle-server; npm start",
- "develop-server": "cd puddle-server; DEBUG='puddle:*' npm run develop",
- "develop-editor": "cd puddle-editor; npm run develop"
+ "test": "cd $MODULE; npm install; npm test;"
},
"repository": {
"type": "git",
diff --git a/puddle-editor/.jshintignore b/puddle-editor/.jshintignore
index 52b3a86..543a6e8 100644
--- a/puddle-editor/.jshintignore
+++ b/puddle-editor/.jshintignore
@@ -1,4 +1,4 @@
node_modules
coverage
build
-source/lib/puddle-syntax-0.1.2/node_modules
\ No newline at end of file
+source/lib
\ No newline at end of file
diff --git a/puddle-editor/gulpfile.js b/puddle-editor/gulpfile.js
index 012995c..0fc31af 100644
--- a/puddle-editor/gulpfile.js
+++ b/puddle-editor/gulpfile.js
@@ -40,7 +40,7 @@ gulp.task('copyHtml', function () {
});
gulp.task('browserify', function () {
- return gulp.src('./source/app.js')
+ return gulp.src('./source/app/main.js')
.pipe(browserify({
exclude: ['mocha'],
debug: true
diff --git a/puddle-editor/package.json b/puddle-editor/package.json
index 5e0867a..3b8ae16 100644
--- a/puddle-editor/package.json
+++ b/puddle-editor/package.json
@@ -34,8 +34,8 @@
"jquery": "^2.1.1",
"lodash": "^2.4.1",
"node-uuid": "^1.4.1",
- "puddle-socket": "0.0.1",
- "puddle-syntax": "^0.1.9",
+ "puddle-socket": "0.0.2",
+ "puddle-syntax": "^0.2.0",
"socket.io-client": "^1.1.0"
},
"devDependencies": {
diff --git a/puddle-editor/readme.md b/puddle-editor/readme.md
index 0fb0d0e..073c69f 100644
--- a/puddle-editor/readme.md
+++ b/puddle-editor/readme.md
@@ -5,8 +5,8 @@ HTML app to be served by puddle-server
###Features:
- [ ] Working version of editor, navigator using same render as before
- [ ] Built on top of puddle-hub
+ [X] Working version of editor, navigator using same render as before
+ [X] Built on top of puddle-hub
###Installation:
diff --git a/puddle-editor/source/app.js b/puddle-editor/source/app.js
deleted file mode 100644
index 7ebc420..0000000
--- a/puddle-editor/source/app.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use strict';
-global.debug = require('debug');
-var io = require('socket.io-client');
-global.puddleSocket = require('puddle-socket').client(io);
-global.syntaxNew = require('puddle-syntax');
-
-require('./lib/main.js');
diff --git a/puddle-editor/source/lib/TODO.js b/puddle-editor/source/app/TODO.js
similarity index 100%
rename from puddle-editor/source/lib/TODO.js
rename to puddle-editor/source/app/TODO.js
diff --git a/puddle-editor/source/lib/assert.js b/puddle-editor/source/app/assert.js
similarity index 100%
rename from puddle-editor/source/lib/assert.js
rename to puddle-editor/source/app/assert.js
diff --git a/puddle-editor/source/app/corpus.js b/puddle-editor/source/app/corpus.js
new file mode 100644
index 0000000..00915d1
--- /dev/null
+++ b/puddle-editor/source/app/corpus.js
@@ -0,0 +1,386 @@
+/**
+ * Corpus of lines of code.
+ */
+'use strict';
+
+var _ = require('lodash');
+var uuid = require('node-uuid');
+var syntax = require('puddle-syntax');
+var tokens = syntax.tokens;
+var assert = require('./assert');
+var debug = require('debug')('puddle:editor:corpus');
+var io = require('socket.io-client');
+var socket = io();
+var puddleSocket = require('puddle-socket').client(io);
+var EventEmitter = require('events').EventEmitter;
+var emitter = new EventEmitter();
+var trace = require('./trace')(debug);
+var $ = require('jquery');
+var serverSyntax = require('./server-syntax');
+trace('corpus init');
+var checkedOut = {};
+//--------------------------------------------------------------------------
+// client state
+
+/** Example.
+ var exampleLine = {
+ 'id': 'asfgvg1tr457et46979yujkm',
+ 'name': 'div', // or null for anonymous lines
+ 'code': 'APP V K', // compiled code
+ 'free': {} // set of free variables in line : string -> null
+ 'validity': {is_top: null, is_bot: null, pending: true} //default value
+};
+ */
+
+
+//--------------------------------------------------------------------------
+var corpus = {};
+
+// These maps fail with names like 'constructor';
+// as a hack we require a '.' in all names in corpus.
+var lines = corpus.lines = {}; // id -> line
+var definitions = {}; // name -> id
+var occurrences = {}; // name -> (set id)
+
+var insertDefinition = function (name, id) {
+ assert(tokens.isGlobal(name));
+ assert(definitions[name] === undefined);
+ assert(occurrences[name] === undefined);
+ definitions[name] = id;
+ occurrences[name] = {};
+};
+
+var removeDefinition = function (name) {
+ assert(definitions[name] !== undefined);
+ assert(occurrences[name] !== undefined);
+ assert(_.isEmpty(occurrences[name]));
+ delete definitions[name];
+ delete occurrences[name];
+};
+
+var insertOccurrence = function (name, id) {
+ var occurrencesName = occurrences[name];
+ assert(occurrencesName !== undefined);
+ assert(occurrencesName[id] === undefined);
+ occurrences[name][id] = null;
+};
+
+var removeOccurrence = function (name, id) {
+ var occurrencesName = occurrences[name];
+ assert(occurrencesName !== undefined);
+ assert(occurrencesName[id] !== undefined);
+ delete occurrencesName[id];
+};
+
+var createLine = function (line) {
+ trace('insertLine', arguments);
+ var id = line.id;
+ /* jshint camelcase: false */
+ line.validity = {is_top: null, is_bot: null, pending: true};
+ /* jshint camelcase: true */
+ assert(!_.has(lines, id));
+ lines[id] = line;
+ if (line.name !== null) {
+ insertDefinition(line.name, id);
+ }
+ line.free = tokens.getFreeVariables(line.code);
+ for (var name in line.free) {
+ insertOccurrence(name, id);
+ }
+ pollValidities();
+ emitter.emit('create', line);
+};
+
+var removeLine = function (id) {
+ trace('remove', arguments);
+ assert(_.has(lines, id));
+ var line = lines[id];
+ delete lines[id];
+ for (var name in line.free) {
+ removeOccurrence(name, id);
+ }
+ if (line.name !== null) {
+ removeDefinition(line.name);
+ }
+ pollValidities();
+ emitter.emit('remove', id);
+};
+
+var updateLine = function (newline) {
+ trace('update', arguments);
+ var name;
+ var id = newline.id;
+ assert(id !== undefined, 'expected .id field in updated line');
+ var line = lines[id];
+ for (name in line.free) {
+ removeOccurrence(name, id);
+ }
+ line.code = newline.code;
+ line.free = tokens.getFreeVariables(line.code);
+ for (name in line.free) {
+ insertOccurrence(name, id);
+ }
+ pollValidities();
+ emitter.emit('update', line);
+};
+
+
+puddleSocket.on('create', function (id, code) {
+ trace('Hub incoming create');
+ var line = serverSyntax.loadStatement(code);
+ line.id = id;
+ createLine(line);
+}, 'corpus');
+
+puddleSocket.on('remove', function (id) {
+ trace('Hub incoming remove');
+ removeLine(id);
+}, 'corpus');
+
+puddleSocket.on('update', function (id, code) {
+ trace('Hub incoming update');
+ var line = serverSyntax.loadStatement(code);
+ line.id = id;
+ updateLine(line);
+}, 'corpus');
+
+
+puddleSocket.on('reset', function (codes) {
+ trace('Hub reset');
+
+ lines = corpus.lines = {};
+ definitions = {};
+ occurrences = {};
+
+ _.each(codes, function (code, id) {
+ var line = serverSyntax.loadStatement(code);
+ line.id = id;
+ lines[id] = line;
+ });
+
+ //insert all defenitions
+ _.each(lines, function (line, id) {
+ if (line.name !== null) {
+ insertDefinition(line.name, id);
+ }
+ });
+
+ //insert all occurences of these defenitions
+ _.each(lines, function (line, id) {
+ line.free = tokens.getFreeVariables(line.code);
+ for (var name in line.free) {
+ insertOccurrence(name, id);
+ }
+ });
+ emitter.emit('reset', lines);
+ validate();
+ pollValidities();
+});
+
+
+var validate = function () {
+ trace('validating corpus', arguments);
+ for (var id in lines) {
+ var line = lines[id];
+ var name = line.name;
+ if (name !== null) {
+ assert(
+ tokens.isGlobal(name),
+ 'name is not global: ' + name);
+ assert(
+ !tokens.isKeyword(name),
+ 'name is keyword: ' + name);
+ assert(
+ definitions[name] === line.id,
+ 'missing definition: ' + name);
+ }
+ var free = tokens.getFreeVariables(line.code);
+ assert.equal(line.free, free, 'wrong free variables:');
+ for (name in free) {
+ assert(
+ tokens.isGlobal(name),
+ 'name is not global: ' + name);
+ var occurrencesName = occurrences[name];
+ assert(
+ occurrencesName !== undefined,
+ 'missing occurrences: ' + name);
+ assert(
+ occurrencesName[id] === null,
+ 'missing occurrence: ' + name);
+ }
+ }
+};
+
+var pollValidities = (function () {
+ var delay = 500;
+ var delayFail = 15000;
+ var polling = false;
+
+ var poll = function () {
+ polling = true;
+ debug('...polling...');
+ var ids = [];
+ var linesForAnalyst = _.map(lines, function (line, id) {
+ ids.push(id);
+ //TODO why line.name is undefined after syntax.compiler.dumpLine?
+ //Should not dumpLine be compatible with analyst?
+ return {name: line.name || null, code: line.code};
+ });
+ $.ajax({
+ url: '/analyst/validities',
+ cache: false,
+ type: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify(linesForAnalyst)
+ })
+ /*jslint unparam: true*/
+ .fail(function (jqXHR, textStatus) {
+ debug('pollValidities failed: ' + textStatus);
+ setTimeout(poll, delayFail);
+ })
+ /*jslint unparam: false*/
+ .done(function (validities) {
+ var updated = [];
+ polling = false;
+ debug('...validities received');
+
+ validities.forEach(function (validity, index) {
+ var line = lines[ids[index]];
+ if (!_.isEqual(line.validity, validity)) {
+ line.validity = validity;
+ updated.push(line);
+ }
+ });
+
+ if (updated.length) {
+ emitter.emit('updateValidity', updated);
+ }
+ if (_.find(validities, 'pending')) {
+ polling = true;
+ setTimeout(poll, delay);
+ }
+
+ });
+ };
+ return function () {
+ trace('PollingValidities...');
+ if (!polling) {
+ poll();
+ } else {
+ debug('...skip polling');
+ }
+ };
+})();
+
+// API
+
+corpus.create = function (line) {
+ line.id = uuid();
+ createLine(line);
+ puddleSocket.create(line.id, serverSyntax.dumpStatement(line), 'corpus');
+ return line;
+};
+
+corpus.remove = function (id) {
+ removeLine(id);
+ puddleSocket.remove(id, 'corpus');
+};
+
+//not exposed as API, called by corpus.checkIn
+var update = function (line) {
+ updateLine(line);
+ puddleSocket.update(line.id, serverSyntax.dumpStatement(line), 'corpus');
+ return line;
+};
+
+corpus.id = function (id) {
+ return lines[id];
+};
+
+corpus.findAllLines = function () {
+ trace('findAllLines', arguments);
+ return _.keys(lines);
+};
+corpus.findDefinition = function (name) {
+ trace('findDefinition', arguments);
+ var id = definitions[name];
+ if (id !== undefined) {
+ return id;
+ } else {
+ return null;
+ }
+};
+corpus.canDefine = function (name) {
+ return syntax.tokens.isGlobal(name) &&
+ definitions[name] === undefined;
+};
+corpus.getDefinitions = function () {
+ return definitions;
+};
+
+corpus.findOccurrences = function (name) {
+ trace('findOccurrences', arguments);
+ assert(_.has(definitions, name));
+ return _.keys(occurrences[name]);
+};
+
+corpus.hasOccurrences = function (name) {
+ trace('hasOccurrences', arguments);
+ assert(_.has(definitions, name));
+ return !_.isEmpty(occurrences[name]);
+};
+
+corpus.insertDefine = function (varName) {
+ var VAR = syntax.compiler.fragments.church.VAR;
+ var HOLE = syntax.compiler.fragments.church.HOLE;
+ var DEFINE = syntax.compiler.fragments.church.DEFINE;
+ var churchTerm = DEFINE(VAR(varName), HOLE);
+ var line = syntax.compiler.dumpLine(churchTerm);
+ return corpus.create(line);
+};
+
+corpus.insertAssert = function () {
+ var HOLE = syntax.compiler.fragments.church.HOLE;
+ var ASSERT = syntax.compiler.fragments.church.ASSERT;
+ var churchTerm = ASSERT(HOLE);
+ var line = syntax.compiler.dumpLine(churchTerm);
+ line.name = null;
+ return corpus.create(line);
+};
+
+corpus.checkOut = function (tree) {
+ trace('checkout');
+ var id = tree.id;
+ socket.emit('checkOut', id);
+ assert(id);
+ assert(!checkedOut[id]);
+ checkedOut[id] = syntax.compiler.dumpLine(syntax.tree.dump(tree));
+};
+
+//used when there was no change to the line
+corpus.unCheckOut = function (tree) {
+ trace('unCheckOut');
+ var id = tree.id;
+ socket.emit('checkIn', id);
+ assert(id);
+ assert(checkedOut[id]);
+ delete checkedOut[id];
+};
+
+corpus.checkIn = function (updatedTree) {
+ trace('checkin');
+ var id = updatedTree.id;
+ socket.emit('checkIn', id);
+ var oldLine = checkedOut[id];
+ assert(oldLine);
+ var newLine = syntax.compiler.dumpLine(syntax.tree.dump(updatedTree));
+ if (!_.isEqual(oldLine, newLine)) {
+ newLine.id = id;
+ update(newLine);
+ }
+ delete checkedOut[id];
+};
+
+corpus.on = _.bind(emitter.on, emitter);
+
+module.exports = corpus;
diff --git a/puddle-editor/source/app/cursor.js b/puddle-editor/source/app/cursor.js
new file mode 100644
index 0000000..dbd52dd
--- /dev/null
+++ b/puddle-editor/source/app/cursor.js
@@ -0,0 +1,206 @@
+'use strict';
+
+var _ = require('lodash');
+var assert = require('assert');
+var EventEmitter = require('events').EventEmitter;
+var emitter = new EventEmitter();
+var syntax = require('puddle-syntax');
+var debug = require('debug')('puddle:editor:cursor');
+var trace = require('./trace')(debug);
+var forest = require('./forest');
+var cursor = {};
+trace('cursor init');
+
+var lineIndex = 0;
+cursor.tree = function () {
+ return syntax.tree.getRoot(cursor.node);
+};
+
+cursor.moveTo = function (newNode) {
+ assert(newNode);
+ if (newNode !== cursor.node) {
+ trace('cursor moveTo');
+ var oldNode = cursor.node;
+ cursor.node = newNode;
+ lineIndex = forest.trees.indexOf(cursor.tree());
+ checkInCheckOut(newNode, oldNode);
+ emitter.emit('move', newNode, oldNode);
+ }
+};
+
+//route = [-1,2,4]
+//'-1' to go above, positive numbers to go below
+//node parameter is optional.
+cursor.getRelative = function (route, node) {
+ assert(_.isArray(route));
+ var finish = node || cursor.node;
+ _.forEach(route, function (direction) {
+ if (direction === -1) {
+ finish = finish.above;
+ } else {
+ finish = finish.below[direction];
+ }
+ assert(finish);
+ });
+ return finish;
+};
+cursor.getAddressInTree = function () {
+ var tree = cursor.tree();
+ var current = cursor.node;
+ var last;
+ var address = [];
+ while (current !== tree) {
+ last = current;
+ current = current.above;
+ //note adding +1, this is because in TERM 0-th index is always string
+ address.unshift(current.below.indexOf(last) + 1);
+ }
+ return address;
+};
+
+cursor.replaceBelow = function (node) {
+ var above = cursor.node.above;
+ if (above) {
+ var pos = above.below.indexOf(cursor.node);
+ assert(pos !== -1);
+ above.below[pos] = node;
+ node.above = above;
+ cursor.moveTo(node);
+ }
+};
+
+cursor.moveLine = function (delta) {
+ return function () {
+ trace('cursor moveLine');
+ var newPos = lineIndex + delta;
+ //out of bounds handling
+ while (newPos < 0) {
+ newPos += forest.trees.length;
+ }
+ while (newPos >= forest.trees.length) {
+ newPos -= forest.trees.length;
+ }
+ var newLine = forest.trees[newPos];
+ assert(newLine);
+ cursor.moveTo(newLine);
+ };
+};
+
+//TODO this is taken from syntax.cursor.tryMove
+var traverseDownLeft = function (node) {
+ while (node.below.length) {
+ node = _.first(node.below);
+ }
+ return node;
+};
+
+var traverseDownRight = function (node) {
+ while (node.below.length) {
+ node = _.last(node.below);
+ }
+ return node;
+};
+
+
+cursor.moveLeft = function () {
+ trace('cursor moveLeft');
+ var node = cursor.node;
+ var above = node.above;
+ while (above !== null) {
+ var pos = _.indexOf(above.below, node);
+ assert(pos >= 0, 'node not found in node.above.below');
+ if (pos > 0) {
+ return cursor.moveTo(traverseDownRight(above.below[pos - 1]));
+ }
+ node = above;
+ above = node.above;
+ }
+ return cursor.moveTo(traverseDownRight(node));
+};
+
+cursor.moveRight = function () {
+ trace('cursor moveRight');
+ var node = cursor.node;
+ var above = node.above;
+ while (above !== null) {
+ var pos = _.indexOf(above.below, node);
+ assert(pos >= 0, 'node not found in node.above.below');
+ if (pos < above.below.length - 1) {
+ return cursor.moveTo(traverseDownLeft(above.below[pos + 1]));
+ }
+ node = above;
+ above = node.above;
+ }
+ return cursor.moveTo(traverseDownLeft(node));
+};
+
+cursor.widenSelection = function () {
+ if (cursor.node.above !== null) {
+ cursor.moveTo(cursor.node.above);
+ }
+};
+
+cursor.on = _.bind(emitter.on, emitter);
+
+
+forest.on('remove', function (id) {
+ trace('Forest remove');
+ if (cursor.tree().id === id) {
+ cursor.moveLine(-1)();
+ }
+});
+
+forest.on('update', function (tree) {
+ trace('Forest update');
+ if (cursor.tree().id === tree.id) {
+ cursor.moveTo(tree);
+ }
+});
+
+forest.on('reset', function (trees) {
+ trace('Forest reset');
+ var firstTree = trees[0];
+ cursor.moveTo(firstTree);
+});
+
+
+module.exports = cursor;
+
+var corpus = require('./corpus');
+
+//checkIn or checkOut lines in corpus according to type of move.
+var checkInCheckOut = function (toNode, fromNode) {
+ assert(toNode !== fromNode);
+ var toTree = syntax.tree.getRoot(toNode);
+ var fromTree;
+ if (fromNode) {
+ fromTree = syntax.tree.getRoot(fromNode);
+ }
+
+ //CHECK-IN
+ if (fromTree !== fromNode) { //only if moving from non root node
+ if (toTree !== fromTree) { //and changing the tree
+ //make sure fromTree is still part of the forest
+ if (forest.isOrphan(fromTree)) {
+ corpus.unCheckOut(fromTree);
+ } else {
+ corpus.checkIn(fromTree);
+ }
+ } else { //or same tree but moving to root node
+ if (toNode === toTree) {
+ corpus.checkIn(fromTree);
+ }
+ }
+ }
+
+ //CHECK-OUT
+ if (toTree !== toNode) { //only if moving to non root node
+ if (toTree !== fromTree) { //and changing the tree
+ corpus.checkOut(toTree);
+ } else { //or same tree but from root node
+ if (fromNode === fromTree) {
+ corpus.checkOut(toTree);
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/puddle-editor/source/app/forest.js b/puddle-editor/source/app/forest.js
new file mode 100644
index 0000000..b010f34
--- /dev/null
+++ b/puddle-editor/source/app/forest.js
@@ -0,0 +1,106 @@
+'use strict';
+var assert = require('assert');
+var _ = require('lodash');
+var EventEmitter = require('events').EventEmitter;
+var emitter = new EventEmitter();
+var syntax = require('puddle-syntax');
+var corpus = require('./corpus');
+var debug = require('debug')('puddle:editor:forest');
+var trace = require('./trace')(debug);
+var trees = []; //array to keep forest sorted in particular order
+var treesHash = {}; //id => tree, hash to access same trees by id.
+trace('forest init');
+
+
+var sortForest = function () {
+ /*
+ Return a heuristically sorted list of definitions.
+
+ TODO use approximately topologically-sorted order.
+ (R1) "A Technique for Drawing Directed Graphs" -Gansner et al
+ http://www.graphviz.org/Documentation/TSE93.pdf
+ (R2) "Combinatorial Algorithms for Feedback Problems in Directed Graphs"
+ -Demetrescu and Finocchi
+ http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.1.9435
+ */
+
+ //simple by ID sort
+ trees.sort(function (a, b) {
+ var keyA = a.id,
+ keyB = b.id;
+ if (keyA < keyB) {
+ return -1;
+ }
+ if (keyA > keyB) {
+ return 1;
+ }
+ return 0;
+ });
+};
+
+var update = function (line) {
+ assert(line.id);
+ var churchTerm = syntax.compiler.loadLine(line);
+ var tree = syntax.tree.load(churchTerm);
+ tree.id = line.id;
+ var oldTree = treesHash[tree.id];
+ assert(oldTree);
+ trees[trees.indexOf(oldTree)] = tree;
+ treesHash[tree.id] = tree;
+ sortForest();
+ emitter.emit('update', tree);
+};
+
+var createLine = function (line) {
+ var id = line.id;
+ var churchTerm = syntax.compiler.loadLine(line);
+ var tree = syntax.tree.load(churchTerm);
+ tree.id = id;
+ trees.push(tree);
+ treesHash[id] = tree;
+ return tree;
+};
+
+corpus.on('create', function (line) {
+ var tree = createLine(line);
+ sortForest();
+ emitter.emit('create', tree);
+});
+
+corpus.on('remove', function (id) {
+ var oldTree = treesHash[id];
+ assert(oldTree);
+ _.pull(trees, oldTree);
+ delete treesHash[id];
+ emitter.emit('remove', id);
+});
+
+corpus.on('update', update);
+
+corpus.on('reset', function (linesHash) {
+ trace('Corpus reset');
+
+ trees.length = 0;
+ _.each(treesHash, function (value, key) {
+ delete treesHash[key];
+ });
+
+ _.each(linesHash, createLine);
+ assert(trees.length > 0, 'corpus is empty');
+ sortForest();
+ emitter.emit('reset', trees);
+});
+
+
+module.exports = {
+ trees: trees,
+ id: function (id) {
+ return treesHash[id];
+ },
+ on: _.bind(emitter.on, emitter),
+ update: update,
+ isOrphan: function (node) {
+ var tree = syntax.tree.getRoot(node);
+ return (trees.indexOf(tree) === -1);
+ }
+};
diff --git a/puddle-editor/source/app/main.js b/puddle-editor/source/app/main.js
new file mode 100644
index 0000000..5316e8a
--- /dev/null
+++ b/puddle-editor/source/app/main.js
@@ -0,0 +1,18 @@
+'use strict';
+//globals are only defined here and are only used for debug
+global._ = require('lodash');
+global.$ = require('jquery');
+global.debug = require('debug');
+global.corpus = require('./corpus');
+global.syntax = require('puddle-syntax');
+var io = require('socket.io-client');
+global.socket = io();
+global.forest = require('./forest');
+global.cursor = require('./cursor');
+require('./view');
+require('./menu-renderer.js');
+
+
+
+
+
diff --git a/puddle-editor/source/app/menu-builder.js b/puddle-editor/source/app/menu-builder.js
new file mode 100644
index 0000000..a3b1460
--- /dev/null
+++ b/puddle-editor/source/app/menu-builder.js
@@ -0,0 +1,174 @@
+'use strict';
+
+var _ = require('lodash');
+var assert = require('assert');
+var syntax = require('puddle-syntax');
+var io = require('socket.io-client');
+var socket = io();
+var debug = require('debug')('puddle:editor:menu');
+var trace = require('./trace')(debug);
+var corpus = require('./corpus');
+var cursor = require('./cursor');
+var forest = require('./forest');
+
+var fragments = syntax.compiler.fragments.church;
+
+var HOLE = fragments.HOLE;
+var TOP = fragments.TOP;
+var BOT = fragments.BOT;
+var VAR = fragments.VAR;
+var LAMBDA = fragments.LAMBDA;
+var LETREC = fragments.LETREC;
+var APP = fragments.APP;
+var JOIN = fragments.JOIN;
+var RAND = fragments.RAND;
+var QUOTE = fragments.QUOTE;
+var EQUAL = fragments.EQUAL;
+var LESS = fragments.LESS;
+var NLESS = fragments.NLESS;
+var ASSERT = fragments.ASSERT;
+var DEFINE = fragments.DEFINE;
+var CURSOR = fragments.CURSOR;
+var DASH = VAR('—');
+
+
+var actions = {
+ revertLine: function () {
+ if (cursor.tree() !== cursor.node) {
+ forest.update(corpus.id(cursor.tree().id));
+ }
+ },
+ commitLine: function () {
+ if (cursor.tree() !== cursor.node) {
+ cursor.moveTo(cursor.tree());
+ }
+ },
+ removeLine: function () {
+ corpus.remove(cursor.tree().id);
+ },
+ insertDefine: function (varName) {
+ var line = corpus.insertDefine(varName);
+ cursor.moveTo(forest.id(line.id));
+ },
+ insertAssert: function () {
+ var newLine = corpus.insertAssert();
+ var newTree = forest.id(newLine.id);
+ cursor.moveTo(cursor.getRelative([0], newTree));
+ }
+};
+
+var socketLogWrapper = function (actions) {
+ return actions.map(function (action) {
+ var name = action[0];
+ var callback = function () {
+ socket.emit('action', action[0]);
+ action[1].apply(this, _.toArray(arguments));
+ };
+ var description = action[2];
+ return [name, callback, description];
+ });
+};
+
+var generic = [
+ ['enter', actions.commitLine, 'commit line'],
+ ['tab', actions.revertLine, 'revert line'],
+ ['up', cursor.moveLine(-1), 'move up'],
+ ['down', cursor.moveLine(1), 'move down'],
+ ['left', cursor.moveLeft, 'move left'],
+ ['right', cursor.moveRight, 'move right'],
+ ['shift+left', cursor.widenSelection, 'widen selection'],
+ ['shift+right', cursor.widenSelection, 'widen selection'],
+ ['A', actions.insertAssert,
+ ASSERT(CURSOR(HOLE))],
+ ['D', actions.insertDefine,
+ DEFINE(CURSOR(VAR('...')), HOLE)]
+];
+
+var getActions = function () {
+ trace('getActions');
+ var node = cursor.node;
+ var actionsArray = [];
+ var on = function (name, term, subsForDash) {
+ actionsArray.push([
+ name,
+ function () {
+ if (subsForDash) {
+ term = syntax.compiler.substitute(
+ '—',
+ subsForDash, term);
+ }
+ var decomposed = syntax.cursorTerm.removeCursor(term);
+ cursor.replaceBelow(syntax.tree.load(decomposed.term));
+ if (decomposed.address) {
+ cursor.moveTo(cursor.getRelative(decomposed.address));
+ }
+ },
+ term
+ ]);
+ };
+
+ var name = node.name;
+ var varName = syntax.tree.getFresh(node);
+ var fresh = VAR(varName);
+ if (name === 'ASSERT') {
+ actionsArray.push(['X', actions.removeLine, 'delete line']);
+ } else if (name === 'DEFINE') {
+ if (!corpus.hasOccurrences(node.below[0].varName)) {
+ actionsArray.push(['X', actions.removeLine, 'delete line']);
+ }
+ } else if (name === 'HOLE') {
+ on('X', HOLE); // TODO define context-specific deletions
+ on('T', TOP);
+ on('_', BOT);
+
+ on('\\', LAMBDA(fresh, CURSOR(HOLE)));
+ on('W', LETREC(fresh, CURSOR(HOLE), HOLE));
+ on('L', LETREC(fresh, HOLE, CURSOR(HOLE)));
+ on('space', APP(HOLE, CURSOR(HOLE)));
+ on('(', APP(CURSOR(HOLE), HOLE));
+ on('|', JOIN(CURSOR(HOLE), HOLE));
+ on('+', RAND(CURSOR(HOLE), HOLE));
+ on('{', QUOTE(CURSOR(HOLE)));
+ on('=', EQUAL(CURSOR(HOLE), HOLE));
+ on('<', LESS(CURSOR(HOLE), HOLE));
+ on('>', NLESS(CURSOR(HOLE), HOLE));
+
+ // TODO filter globals and locals by future validity
+ actionsArray.push([
+ '/',
+ function (name) {
+ assert(name !== undefined, 'name not found: ' + name);
+ var newNode = syntax.tree.load(VAR(name));
+ cursor.replaceBelow(newNode);
+ },
+ VAR('global.variable')
+ ]);
+
+ var locals = syntax.tree.getLocals(node);
+ locals.forEach(function (varName) {
+ on(varName, VAR(varName));
+ // TODO deal with >26 variables
+ });
+
+ } else {
+ var dumped = syntax.tree.dump(node);
+
+ // TODO define context-specific deletions
+ on('X', HOLE);
+
+ on('\\', LAMBDA(fresh, CURSOR(DASH)), dumped);
+ on('W', LETREC(fresh, CURSOR(HOLE), DASH), dumped);
+ on('L', LETREC(fresh, DASH, CURSOR(HOLE)), dumped);
+ on('space', APP(DASH, CURSOR(HOLE)), dumped);
+ on('(', APP(CURSOR(HOLE), DASH), dumped);
+ on('|', JOIN(DASH, CURSOR(HOLE)), dumped);
+ on('+', RAND(DASH, CURSOR(HOLE)), dumped);
+ on('{', QUOTE(CURSOR(DASH)), dumped);
+ on('=', EQUAL(DASH, CURSOR(HOLE)), dumped);
+ on('<', LESS(DASH, CURSOR(HOLE)), dumped);
+ on('>', NLESS(DASH, CURSOR(HOLE)), dumped);
+ }
+ return socketLogWrapper(generic.concat(actionsArray));
+};
+
+module.exports = getActions;
diff --git a/puddle-editor/source/app/menu-renderer.js b/puddle-editor/source/app/menu-renderer.js
new file mode 100644
index 0000000..6bfdda4
--- /dev/null
+++ b/puddle-editor/source/app/menu-renderer.js
@@ -0,0 +1,162 @@
+'use strict';
+
+var debug = require('debug')('puddle:editor:menuRenderer');
+var _ = require('lodash');
+var $ = require('jquery');
+var keypress = require('../lib/keypress').keypress;
+var keyListener = new keypress.Listener();
+var trace = require('./trace')(debug);
+var renderTerm = require('./render-term.js');
+var cursor = require('./cursor');
+var corpus = require('./corpus');
+
+var VAR = require('puddle-syntax').compiler.fragments.church.VAR;
+var menuBuilder = require('./menu-builder');
+
+
+//--------------------------------------------------------------------------
+// Event Handling
+
+var search = function (acceptMatch) {
+ trace('Search init');
+ var strings = [];
+ var $input;
+ var matches = [];
+ var $matches;
+ var render = _.identity;
+
+ var update = function () {
+ var re = new RegExp($input.val());
+ matches = [];
+ $matches.empty();
+ strings.forEach(function (string) {
+ if (re.test(string)) {
+ matches.push(string);
+ $matches.append($('
').html(render(string)));
+ }
+ });
+ $matches.children().first().addClass('selected');
+ debug('DEBUG ' + matches);
+ };
+
+
+ var accept = function () {
+ if (matches.length) {
+ debug('DEBUG accepting ' + matches[0]);
+ acceptMatch(matches[0]);
+ }
+ };
+
+ return function () {
+ trace('Search');
+ strings = _.keys(corpus.getDefinitions());
+ render = function (name) {
+ return renderTerm(VAR(name));
+ };
+
+
+ off();
+ on('enter', accept, 'accept');
+ on('tab', render, 'cancel');
+ $input = $('');
+ $matches = $('');
+ $('#navigate').append($input, $matches);
+ $input.focus().on('keydown', _.debounce(update));
+ update();
+ };
+};
+
+//wrapper to show choose dialog for DEFINE function
+var choose = function (acceptName) {
+ var $input;
+ var input;
+ var valid;
+
+ var update = function () {
+ input = $input.val();
+ valid = corpus.canDefine(input);
+ $input.attr({'class': valid ? 'valid' : 'invalid'});
+ };
+
+ return function () {
+ trace('choose');
+ off();
+
+ on('enter', function () {
+ if (valid) {
+ debug('DEBUG choosing ' + input);
+ acceptName(input);
+ }
+ }, 'accept');
+
+ on('tab', render, 'cancel');
+
+ $input = $('
');
+ $('#navigate').append($input);
+ $input.focus().on('keydown', _.debounce(update));
+ update();
+ };
+};
+var upperCaseAliases = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ?><\":{}|~+_!@#$%^&*()';
+var on = function (name, callback, description) {
+ var listenKey;
+ if (_.contains(upperCaseAliases, name)) {
+ listenKey = 'shift ' + name.toLowerCase();
+ } else {
+ listenKey = name.replace(/\+/g, ' ');
+ }
+
+ /* jshint camelcase:false */
+ keyListener.register_combo({
+ 'keys': listenKey,
+ 'on_keydown': function () {
+ callback();
+ },
+ is_solitary: true
+ });
+ /* jshint camelcase:true */
+
+
+ //TODO this is a hack to add UI wrapper for some functions
+ //which require special input parameters or extra UI
+ if (name === 'D') {
+ callback = choose(callback);
+ }
+ if (name === '/') {
+ callback = search(callback);
+ }
+
+
+ if (description !== undefined) {
+ if (_.isArray(description)) {
+ description = renderTerm(description);
+ }
+ var escaped = name.replace(/\b\+\b/g, '+
');
+ var icon = $('').html('' + escaped + '');
+ $('#navigate table').append(
+ $(' | ')
+ .on('click', callback)
+ .append(icon, $('')
+ .html(description)));
+ }
+};
+
+var off = function () {
+ trace('off');
+ keyListener.reset();
+ $('#navigate').empty().append($(''));
+};
+
+var render = function () {
+ trace('MenuRender');
+ off();
+ menuBuilder().forEach(function (action) {
+ on(action[0], action[1], action[2]);
+ });
+};
+
+cursor.on('move', function () {
+ trace('Cursor move');
+ render();
+});
+
diff --git a/puddle-editor/source/lib/render-term.js b/puddle-editor/source/app/render-term.js
similarity index 92%
rename from puddle-editor/source/lib/render-term.js
rename to puddle-editor/source/app/render-term.js
index 7eff888..d3ee6dd 100644
--- a/puddle-editor/source/lib/render-term.js
+++ b/puddle-editor/source/app/render-term.js
@@ -2,7 +2,7 @@
var _ = require('lodash');
var assert = require('assert');
-var syntax = require('./puddle-syntax-0.1.2');
+var syntax = require('puddle-syntax');
var template = function (string) {
return function (args) {
@@ -41,9 +41,7 @@ var templates = {
}
};
-var render = syntax.compiler.fold(function (token, args) {
+module.exports = syntax.compiler.fold(function (token, args) {
assert(_.has(templates, token), 'unknown token: ' + token);
return templates[token](args);
});
-
-module.exports = render;
diff --git a/puddle-editor/source/lib/render-validity.js b/puddle-editor/source/app/render-validity.js
similarity index 88%
rename from puddle-editor/source/lib/render-validity.js
rename to puddle-editor/source/app/render-validity.js
index cd9192b..158d999 100644
--- a/puddle-editor/source/lib/render-validity.js
+++ b/puddle-editor/source/app/render-validity.js
@@ -23,10 +23,11 @@ var table = {
'false-null-false': svg('yellow', 'delta'),
'null-null-true': svg('gray', 'square'),
'null-false-true': svg('gray', 'nabla'),
- 'false-null-true': svg('gray', 'delta'),
+ 'false-null-true': svg('gray', 'delta')
};
module.exports = function (validity) {
+ validity = validity || {is_top: null, is_bot: null, pending: true};
return table[
validity.is_top + '-' + validity.is_bot + '-' + validity.pending
];
diff --git a/puddle-editor/source/lib/server-syntax.js b/puddle-editor/source/app/server-syntax.js
similarity index 79%
rename from puddle-editor/source/lib/server-syntax.js
rename to puddle-editor/source/app/server-syntax.js
index 263889c..2c43b93 100644
--- a/puddle-editor/source/lib/server-syntax.js
+++ b/puddle-editor/source/app/server-syntax.js
@@ -21,6 +21,14 @@ module.exports = {
};
})(),
dumpStatement : function (statement) {
+ //TODO this is an API difference hot-fix
+ //syntax.compiler.dumpLine should give output compatible
+ //with analyst and server-syntax?
+ delete statement.token;
+ if (!statement.name) {
+ statement.name = null;
+ }
+
if (statement.name === null) {
return 'ASSERT ' + statement.code;
} else {
diff --git a/puddle-editor/source/lib/trace.js b/puddle-editor/source/app/trace.js
similarity index 72%
rename from puddle-editor/source/lib/trace.js
rename to puddle-editor/source/app/trace.js
index 163a3ed..92b9803 100644
--- a/puddle-editor/source/lib/trace.js
+++ b/puddle-editor/source/app/trace.js
@@ -3,7 +3,7 @@
var _ = require('lodash');
module.exports = function (debug) {
return function (name, args, trace) {
- debug.apply(debug, ['Trace:', name].concat(_.toArray(args)));
+ debug.apply(debug, [name].concat(_.toArray(args)));
if (trace) {
console.trace();
}
diff --git a/puddle-editor/source/app/view.js b/puddle-editor/source/app/view.js
new file mode 100644
index 0000000..ecb44d4
--- /dev/null
+++ b/puddle-editor/source/app/view.js
@@ -0,0 +1,139 @@
+'use strict';
+
+var _ = require('lodash');
+var $ = require('jquery');
+var forest = require('./forest');
+var corpus = require('./corpus');
+var cursor = require('./cursor');
+var syntax = require('puddle-syntax');
+var io = require('socket.io-client');
+var socket = io();
+var renderTerm = require('./render-term.js');
+var renderValidity = require('./render-validity.js');
+var debug = require('debug')('puddle:editor:view');
+var trace = require('./trace')(debug);
+trace('view init');
+var checkInOutState = {};
+
+var renderLine = function (tree) {
+ var id = tree.id;
+ var term = syntax.tree.dump(tree);
+ //inject CURSOR in term if necessary.
+ if (cursor.tree() === tree) {
+ term = syntax.cursorTerm.insertCursor(term, cursor.getAddressInTree());
+ }
+
+ var line = syntax.compiler.parenthesize(term);
+ var $line = $('').attr('id', 'line' + id);
+ var validityHtml = renderValidity(corpus.id(id).validity);
+ $line.html(validityHtml + renderTerm(line));
+
+ //add marker if line is checked out.
+ if (checkInOutState[id]) {
+ $line = $line.prepend(
+ '');
+ }
+
+ $line.on('click', function () {
+ cursor.moveTo(tree);
+ });
+ return $line;
+};
+
+var scrollToCursor = function () {
+ var cursorOffset = $('span.cursor').offset();
+ if (cursorOffset) {
+ var pos = cursorOffset.top - $(window).height() / 2;
+ $(document.body).animate({scrollTop: pos}, 50);
+ }
+};
+
+var render = function () {
+ var div = $('#code').empty()[0];
+ forest.trees
+ .map(renderLine)
+ .forEach(function ($line) {
+ $line.appendTo(div);
+ });
+ scrollToCursor();
+};
+
+var renderValidities = function (updatedLines) {
+ updatedLines.forEach(function (line) {
+ var $validity = $('#line' + line.id + ' .validity');
+ $validity.replaceWith(renderValidity(line.validity));
+ });
+};
+
+var createLine = function (tree) {
+ var div = $('#code');
+ var $line = renderLine(tree);
+ var index = forest.trees.indexOf(tree) - 1;
+
+ if (!index) { //if zero or not found
+ div.prepend($line);
+ } else {
+ var prevLineId = forest.trees[index].id;
+ $('#line' + prevLineId).after($line);
+ }
+};
+
+var removeLine = function (id) {
+ $('#line' + id).remove();
+};
+
+var updateLine = function (tree) {
+ $('#line' + tree.id).replaceWith(renderLine(tree));
+};
+
+var cursorMove = function (newNode, oldNode) {
+ var newTree = syntax.tree.getRoot(newNode);
+ updateLine(newTree);
+
+ if (oldNode) {
+ var oldTree = syntax.tree.getRoot(oldNode);
+ if (newTree !== oldTree && !forest.isOrphan(oldNode)) {
+ updateLine(oldTree);
+ }
+ }
+ scrollToCursor();
+};
+
+//this is nice to see how long does rendering take
+var timeLogger = function (event, cb) {
+ return function () {
+ trace('Render ' + event + ' start...');
+ cb.apply(this, _.toArray(arguments));
+ trace('...render ' + event + ' end');
+ };
+};
+
+forest.on('create', timeLogger('forest create', createLine));
+forest.on('remove', timeLogger('forest remove', removeLine));
+forest.on('update', timeLogger('forest update', updateLine));
+forest.on('reset', timeLogger('forest reset', render));
+
+cursor.on('move', timeLogger('cursor move', cursorMove));
+
+corpus.on('updateValidity', timeLogger('updateValidity', renderValidities));
+
+socket.on('checkInOutUpdate', timeLogger('checkInOutUpdate', function (state) {
+
+ var toBeUpdated = _.uniq(_.keys(checkInOutState).concat(_.values(state)));
+
+ //inverse hash from clientId=>lineId to lineId=>clientId
+ checkInOutState = {};
+ _.each(state, function (value, key) {
+ checkInOutState[value] = key;
+ });
+ toBeUpdated.forEach(function (id) {
+ var tree = forest.id(id);
+ if (tree) {
+ updateLine(tree);
+ }
+ });
+
+}));
\ No newline at end of file
diff --git a/puddle-editor/source/index.html b/puddle-editor/source/index.html
index 6c3ad4d..c033461 100644
--- a/puddle-editor/source/index.html
+++ b/puddle-editor/source/index.html
@@ -5,10 +5,10 @@
Pomagma Editor
-
+
|