Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactored editor to be vim-like modeful. Got insert mode workingish.

  • Loading branch information...
commit 21cdead56e78fd94bc4585d3bad43894ae47a552 1 parent 71d08d3
@luqui authored
View
164 StructuralEditor.js
@@ -21,69 +21,131 @@ var elt = function(name, attrs) {
var text_node = function(text) { return document.createTextNode(text) };
// End CodeCatalog Snippet
-var container = elt('div');
+// CodeCatalog Snippet http://www.codecatalog.net/323/2/
+var for_kv = function(object, body) {
+ for (var k in object) {
+ if (object.hasOwnProperty(k)) {
+ body(k, object[k]);
+ }
+ }
+};
+// End CodeCatalog Snippet
-var input_buffer = '';
+// CodeCatalog Snippet http://www.codecatalog.net/331/2/
+var object = function(methods) {
+ var constr = methods['init'] || function() {};
+ for_kv(methods, function(k,v) {
+ constr.prototype[k] = v;
+ });
+ return constr;
+};
+// End CodeCatalog Snippet
-var zipper = new SF.Zipper([], top_node);
-var update = function(z) {
- if (!z) return;
- zipper = z;
- container.empty();
- container.append(elt('pre', {}, SF.render_zipper_with(z, function(t) {
- return elt('span', {'class': 'selected'}, t, text_node(input_buffer))
- })));
-};
-update(zipper);
+var code_container = elt('div');
+var mode_container = elt('span', {'class': 'modeline'});
+var container = elt('div', {}, code_container, mode_container);
-event_node.keydown(function(e) {
- var head = typeof(zipper.expr) === 'string' ? new SF.SynClass({}) : zipper.expr.head;
-
- var navigate = function(dir) {
- input_buffer = '';
- update(head[dir].call(head, zipper));
- };
-
- if (37 == e.which) { // left
- navigate('nav_left');
- }
- else if (38 == e.which) { // up
- navigate('nav_up');
- }
- else if (39 == e.which) { // right
- navigate('nav_right');
- }
- else if (40 == e.which) { // down
- navigate('nav_down');
- }
- else if (8 == e.which) { // backspace
- input_buffer = input_buffer.slice(0, input_buffer.length-1);
- update(zipper);
- return false; // prevent browser from handling it
- }
- else {
- //console.log(e.which, e.charCode);
+var mode;
+
+var NormalMode = object({
+ init: function(zipper) {
+ this.zipper = zipper;
+ this.update(zipper);
+ mode_container.text('Normal');
+ },
+ keydown: function(e) {
+ var head = typeof(this.zipper.expr) === 'string' ? new SF.SynClass({}) : this.zipper.expr.head;
+
+ var navigate = function(dir) {
+ this.update(head[dir].call(head, this.zipper));
+ };
+
+ if (37 == e.which) { // left
+ navigate('nav_left');
+ }
+ else if (38 == e.which) { // up
+ navigate('nav_up');
+ }
+ else if (39 == e.which) { // right
+ navigate('nav_right');
+ }
+ else if (40 == e.which) { // down
+ navigate('nav_down');
+ }
+ else {
+ //console.log(e.which, e.charCode);
+ }
+ },
+ keypress: function(e) {
+ var ch = String.fromCharCode(e.charCode);
+ if (ch == 'i') {
+ mode = new InsertMode(new SF.Cursor(this.zipper, 0));
+ }
+ },
+ render: function() {
+ code_container.empty();
+ code_container.append(elt('pre', {}, SF.render_zipper(this.zipper)));
+ },
+ update: function(z) {
+ if (!z) return;
+ console.log("Zipper:", z);
+ this.zipper = z;
+ this.render();
}
});
-event_node.keypress(function(e) {
- if (!(32 <= e.which && e.which <= 127)) return; // non-printable
+var InsertMode = object({
+ init: function(cursor) {
+ this.cursor = cursor;
+ this.input_buffer = '';
+ this.update(cursor);
+ mode_container.text('Insert');
+ },
+ keydown: function(e) {
+ if (39 == e.which) { // right
+ this.update(this.cursor.forward_token());
+ }
+ else if (8 == e.which) { // backspace
+ this.input_buffer = this.input_buffer.slice(0, this.input_buffer.length-1);
+ this.render();
+ }
+ },
+ keypress: function(e) {
+ if (!(32 <= e.which && e.which <= 127)) return; // non-printable
- if (typeof(zipper.expr) !== 'string') {
var ch = String.fromCharCode(e.charCode);
- input_buffer += ch;
- var tokresult = zipper.expr.head.parse_insert(zipper.expr)(input_buffer);
+ this.input_buffer += ch;
+ console.log("buffer:", this.input_buffer);
+
+ var tokresult = this.cursor.parse_insert(this.input_buffer);
if (tokresult) {
- input_buffer = tokresult[1];
- update(new SF.Zipper(zipper.contexts, tokresult[0]));
- }
- else {
- update(zipper);
+ this.input_buffer = tokresult[1];
+ this.update(tokresult[0]);
}
- }
- return false;
+ this.render();
+ },
+ render: function() {
+ code_container.empty();
+ code_container.append(elt('pre', {}, SF.render_cursor(this.cursor, text_node(this.input_buffer))));
+ },
+ update: function(c) {
+ if (!c) return;
+ console.log("Cursor:", c);
+ this.cursor = c;
+ this.render();
+ },
+});
+
+mode = new NormalMode(new SF.Zipper([], top_node));
+
+event_node.keydown(function(e) {
+ return mode.keydown(e);
+});
+
+event_node.keypress(function(e) {
+ return mode.keypress(e);
});
return container;
View
106 StructuralFramework.js
@@ -183,6 +183,13 @@ $$.Context = object({
}
});
+
+var context_in = function(expr, pos) {
+ var args = expr.args.slice(0);
+ args[pos] = null;
+ return new $$.Context(expr.head, args);
+};
+
$$.Zipper = object({
init: function(contexts, expr) {
this.contexts = contexts;
@@ -203,12 +210,9 @@ $$.Zipper = object({
down: function(n) {
if (!(0 <= n && typeof(this.expr) === 'object' && n < this.expr.args.length)) return null;
- var args = this.expr.args.slice(0);
- var focus = args[n];
- args[n] = null;
-
+ var focus = this.expr.args[n];
return new $$.Zipper(
- [new $$.Context(this.expr.head, args)].concat(this.contexts),
+ [context_in(this.expr, n)].concat(this.contexts),
focus);
},
left: function() {
@@ -223,6 +227,84 @@ $$.Zipper = object({
},
});
+
+// A cursor is a zipper combined with a position. The position represents a position
+// *between* symbols in the focused expression. So we are inside some symbols above
+// us, and between symbols below us. The position cannot be at the start or the end
+// of the focused expression, because then we are really between two symbols one level
+// up. If the zipper's context is empty, then we can be at the start or the end.
+$$.Cursor = object({
+ // pos represents the position right before zipper.expr.args[pos].
+ init: function(zipper, pos) {
+ // normalize to invariant
+ while (zipper.contexts.length > 0) {
+ if (pos == 0) {
+ pos = zipper.position();
+ zipper = zipper.up();
+ }
+ else if (pos == zipper.expr.args.length) {
+ pos = zipper.position()+1;
+ zipper = zipper.up();
+ }
+ else {
+ break;
+ }
+ }
+ this.zipper = zipper;
+ this.pos = pos;
+ },
+ after: function() {
+ return this.zipper.expr.args[this.pos];
+ },
+ forward_token: function() {
+ var zipper = this.zipper;
+ var pos = this.pos;
+ while (typeof(this.after()) === 'object') {
+ if (zipper.expr.args.length == 0) {
+ pos = zipper.position()+1;
+ zipper = zipper.up();
+ }
+ else {
+ zipper = zipper.down(0);
+ pos = 0;
+ }
+ }
+ return new $$.Cursor(zipper, pos+1);
+ },
+ parse_insert: function(text) {
+ var expr = this.zipper.expr;
+ if (expr.args.length == 0) {
+ var tokresult = expr.head.parse_prefix(text);
+ if (tokresult) {
+ var newcursor = tokresult[0];
+ return [new $$.Cursor(
+ new $$.Zipper(newcursor.zipper.contexts.concat(this.zipper.contexts),
+ newcursor.zipper.expr),
+ newcursor.pos),
+ tokresult[1] ];
+ }
+ else {
+ return null;
+ }
+ }
+
+ var tokresult = expr.args[this.pos].head.parse_prefix(text);
+ if (tokresult) {
+ var newcursor = tokresult[0];
+ var thiscx = new $$.Context(expr.head,
+ [].concat(expr.args.slice(0, this.pos), [null], expr.args.slice(this.pos+1)));
+ var contexts = [].concat(newcursor.zipper.contexts, [thiscx], this.zipper.contexts);
+ return [new $$.Cursor(
+ new $$.Zipper(contexts, newcursor.zipper.expr),
+ newcursor.pos),
+ tokresult[1] ];
+ }
+ else {
+ return null;
+ }
+ }
+});
+
var render_head = function(head, args) {
var ret = elt('span');
foreach(head.render.apply(head, args), function(arg) {
@@ -264,6 +346,20 @@ $$.render_zipper = function(zipper) {
});
};
+$$.render_cursor = function(cursor, ins) {
+ if (typeof(ins) === 'undefined') { ins = $([]) }
+ var args = cursor.zipper.expr.args.map(function(a,i) {
+ var r = render_expr_tree(a);
+ return i == cursor.pos ? elt('span', {'class': 'cursor_selected'}, ins, r) : r;
+ });
+ if (args.length > 0 && cursor.pos == cursor.zipper.expr.length) {
+ args[args.length-1] = elt('span', {'class': 'cursor_selected_right'}, args[args.length-1], ins);
+ }
+ return render_context(
+ cursor.zipper.contexts,
+ render_head(cursor.zipper.expr.head, args));
+};
+
return $$;
};
View
43 StructuralGrammar.js
@@ -170,6 +170,7 @@ var box_synclass = function(cls) {
render: function() {
return elt('span', {'class': 'box'}, text_node(' '));
},
+ parse_prefix: cls.parse_prefix,
parse_insert: function() {
return cls.parse_prefix;
}
@@ -180,10 +181,21 @@ $$.sym = function(name) {
return function(grammar) { return grammar(name) }
};
+var cursor = function(expr, pos) {
+ return new SF.Cursor(new SF.Zipper([], expr), pos);
+};
+
+var cons_context = function(cx, cursor) {
+ var cxs = [cx].concat(cursor.zipper.contexts);
+ return new SF.Cursor(new SF.Zipper(cxs, cursor.zipper.expr), cursor.pos);
+};
+
$$.empty = function(grammar) {
var c = new SF.SynClass({
open: function() { return this.make([]) },
- parse_prefix: function(str) { return [ c.make([]), str ] }
+ parse_prefix: function(str) {
+ return [ cursor(c.make([]), 0), str ]
+ }
});
return c;
};
@@ -202,22 +214,11 @@ $$.literal = function(str) {
return function(grammar) {
var c = new SF.SynClass({
open: function() {
- return this.make([str]);
+ return c.make([str]);
},
- parse_prefix: string_tokenizer(str, function() { return c.make([str]) })
- });
- return c;
- };
-};
-
-// "variable literal", a literal token that has multiple parsings
-$$.varlit = function(rx, tok) {
- return function(grammar) {
- var toks = {};
- toks[rx.source] = function(m) { return $$.literal(tok)(grammar).open() };
- var c = new SF.SynClass({
- open: function() { return box_synclass(c).make([tok]) },
- parse_prefix: regexp_tokenizer(toks)
+ parse_prefix: string_tokenizer(str, function() {
+ return cursor(c.make([str]), 1);
+ })
});
return c;
};
@@ -226,7 +227,9 @@ $$.varlit = function(rx, tok) {
$$.token = function(rx) {
return function(grammar) {
var toks = {};
- toks[rx.source] = function(m) { return $$.literal(m[0])(grammar).open() };
+ toks[rx.source] = function(m) {
+ return cursor($$.literal(m[0])(grammar).make([m[0]]), 1);
+ };
var c = new SF.SynClass({
open: function() { return box_synclass(c).make([]) },
parse_prefix: regexp_tokenizer(toks)
@@ -251,7 +254,9 @@ $$.seq = function() {
if (tokresult) {
rs[i] = tokresult[0];
if (tokresult[1].length < str.length) { // consumed input
- return [ c.make(rs), tokresult[1] ]
+ rs[i] = null;
+ var newcx = new SF.Context(c, rs);
+ return [ cons_context(newcx, tokresult[0]), tokresult[1] ]
}
}
else {
@@ -259,7 +264,7 @@ $$.seq = function() {
}
}
// no input consumed. the sequence accepts the empty string.
- return [ c.make(rs), str ];
+ return [ cursor(c.make(rs), rs.length), str ];
}
});
return c;
View
8 demo/structural.html
@@ -9,6 +9,10 @@
.eatom { color: red }
.selected { background: #fdd }
.box { background: lightgreen }
+ .unparsed { opacity: 0.5 }
+ .cursor_selected { border-left: 1px solid blue }
+ .cursor_selected_right { border-right: 1px solid blue }
+ .modeline { font-weight: bold }
</style>
<script>
@@ -56,12 +60,12 @@
var text_node = function(text) { return document.createTextNode(text) };
// End CodeCatalog Snippet
-var space = SG.varlit(/\s*/, ' ');
+var space = SG.literal(' ');
var top_class = SG.grammar('stmts', {
'stmts': SG.choice(
SG.empty,
- SG.seq(SG.sym('stmt'), SG.literal(';'), SG.sym('stmts'))),
+ SG.seq(SG.sym('stmt'), SG.literal(';\n'), SG.sym('stmts'))),
'stmt': SG.choice(
SG.sym('var_decl'),
SG.sym('funcall')),
Please sign in to comment.
Something went wrong with that request. Please try again.