diff --git a/index.html b/index.html
index 71e79ed..f8754d0 100644
--- a/index.html
+++ b/index.html
@@ -8,6 +8,9 @@
'self'
'sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk='
'sha256-eue5ceZRwKVQ1OXOZSyU7MXCTZMlqsPi/TOIqh1Vlzo='
+ 'sha256-OvxDPyq6KQAoWh11DLJdBVlHHLkYYiy4EzqTjIEJbb4='
+ 'sha256-vnm8bzrI+krtz5228JDC2DoTv0e+sfnfTCiUnO2EBAM='
+ 'sha256-HYX1RusN7a369vYuOd1mGvxLcNL4z/MihkahAI2CH8k='
;
style-src 'self' https://cdnjs.cloudflare.com;
img-src 'self' blob:;
@@ -23,6 +26,12 @@
crossorigin="anonymous"
>
+
+
@@ -35,11 +44,29 @@
>
+
+
+
+
+
+
diff --git a/scripts/interface/Interface.js b/scripts/interface/Interface.js
index 342666c..66f0509 100644
--- a/scripts/interface/Interface.js
+++ b/scripts/interface/Interface.js
@@ -1,4 +1,9 @@
-define(['codemirror'], (CodeMirror) => {
+define([
+ 'cm/lib/codemirror',
+ 'cm/addon/hint/show-hint',
+ 'cm/addon/edit/trailingspace',
+ 'cm/addon/comment/comment',
+], (CodeMirror) => {
'use strict';
const DELAY_AGENTCHANGE = 500;
@@ -82,27 +87,68 @@ define(['codemirror'], (CodeMirror) => {
return options;
}
- build(container) {
- this.codePane = makeNode('div', {'class': 'pane-code'});
- this.viewPane = makeNode('div', {'class': 'pane-view'});
- this.viewPaneInner = makeNode('div', {'class': 'pane-view-inner'});
-
- const options = this.buildOptions();
- this.viewPane.appendChild(this.viewPaneInner);
- this.viewPane.appendChild(options);
-
- container.appendChild(this.codePane);
- container.appendChild(this.viewPane);
-
- const code = this.loadCode() || this.defaultCode;
+ buildEditor(container) {
+ const value = this.loadCode() || this.defaultCode;
CodeMirror.defineMode(
'sequence',
() => this.parser.getCodeMirrorMode()
);
- this.code = new CodeMirror(this.codePane, {
- value: code,
+ CodeMirror.registerHelper(
+ 'hint',
+ 'sequence',
+ this.parser.getCodeMirrorHints()
+ );
+ const code = new CodeMirror(container, {
+ value,
mode: 'sequence',
+ lineNumbers: true,
+ showTrailingSpace: true,
+ extraKeys: {
+ 'Tab': (cm) => cm.execCommand('indentMore'),
+ 'Shift-Tab': (cm) => cm.execCommand('indentLess'),
+ 'Cmd-/': (cm) => cm.toggleComment({padding: ''}),
+ 'Ctrl-/': (cm) => cm.toggleComment({padding: ''}),
+ 'Ctrl-Space': 'autocomplete',
+ 'Ctrl-Enter': 'autocomplete',
+ 'Cmd-Enter': 'autocomplete',
+ },
});
+ let lastKey = 0;
+ code.on('keydown', (cm, event) => {
+ lastKey = event.keyCode;
+ });
+ code.on('endCompletion', () => {
+ lastKey = 0;
+ });
+ code.on('change', (cm) => {
+ if(cm.state.completionActive) {
+ return;
+ }
+ if(lastKey === 13 || lastKey === 8) {
+ return;
+ }
+ lastKey = 0;
+ CodeMirror.commands.autocomplete(cm, null, {
+ completeSingle: false,
+ });
+ });
+
+ return code;
+ }
+
+ build(container) {
+ const codePane = makeNode('div', {'class': 'pane-code'});
+ const viewPane = makeNode('div', {'class': 'pane-view'});
+ this.viewPaneInner = makeNode('div', {'class': 'pane-view-inner'});
+
+ const options = this.buildOptions();
+ viewPane.appendChild(this.viewPaneInner);
+ viewPane.appendChild(options);
+
+ container.appendChild(codePane);
+ container.appendChild(viewPane);
+
+ this.code = this.buildEditor(codePane);
this.viewPaneInner.appendChild(this.renderer.svg());
this.code.on('change', () => this.update(false));
diff --git a/scripts/interface/Interface_spec.js b/scripts/interface/Interface_spec.js
index bfa698a..02f2865 100644
--- a/scripts/interface/Interface_spec.js
+++ b/scripts/interface/Interface_spec.js
@@ -8,7 +8,11 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
let ui = null;
beforeEach(() => {
- parser = jasmine.createSpyObj('parser', ['parse']);
+ parser = jasmine.createSpyObj('parser', [
+ 'parse',
+ 'getCodeMirrorMode',
+ 'getCodeMirrorHints',
+ ]);
parser.parse.and.returnValue({
meta: {},
stages: [],
diff --git a/scripts/sequence/CodeMirrorHints.js b/scripts/sequence/CodeMirrorHints.js
new file mode 100644
index 0000000..2179d2a
--- /dev/null
+++ b/scripts/sequence/CodeMirrorHints.js
@@ -0,0 +1,57 @@
+define(() => {
+ 'use strict';
+
+ const TRIMMER = /^([ \t]*)(.*)$/;
+ const SQUASH_START = /^[ \t\r\n:,]/;
+
+ function getHints(cm) {
+ const cur = cm.getCursor();
+ const token = cm.getTokenAt(cur);
+ let partial = token.string;
+ if(token.end > cur.ch) {
+ partial = partial.substr(0, cur.ch - token.start);
+ }
+ const parts = TRIMMER.exec(partial);
+ partial = parts[2];
+ const from = token.start + parts[1].length;
+
+ const continuation = (cur.ch > 0 && token.state.line.length > 0);
+ let comp = (continuation ?
+ token.state.completions :
+ token.state.beginCompletions
+ );
+ if(!continuation) {
+ comp = comp.concat(token.state.knownAgent);
+ }
+
+ const ln = cm.getLine(cur.line);
+ const wordFrom = {line: cur.line, ch: from};
+ const squashFrom = {line: cur.line, ch: from};
+ if(from > 0 && ln[from - 1] === ' ') {
+ squashFrom.ch --;
+ }
+ const wordTo = {line: cur.line, ch: token.end};
+ const list = (comp
+ .filter((opt) => opt.startsWith(partial))
+ .map((opt) => {
+ return {
+ text: opt,
+ displayText: (opt === '\n') ? '' : opt.trim(),
+ className: (opt === '\n') ? 'pick-virtual' : null,
+ from: SQUASH_START.test(opt) ? squashFrom : wordFrom,
+ to: wordTo,
+ };
+ })
+ );
+
+ return {
+ list,
+ from: wordFrom,
+ to: wordTo,
+ };
+ }
+
+ return {
+ getHints,
+ };
+});
diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js
index 84ba0d1..de41f5b 100644
--- a/scripts/sequence/CodeMirrorMode.js
+++ b/scripts/sequence/CodeMirrorMode.js
@@ -1,114 +1,215 @@
define(['core/ArrayUtilities'], (array) => {
'use strict';
- function makeCMCommaBlock(type, exits = {}) {
- return {type, then: Object.assign({
+ const CM_END = {type: '', suggest: '\n', then: {}};
+ const CM_ERROR = {type: 'error', then: {'': 0}};
+
+ function makeCMCommaBlock(type, suggest, exits = {}) {
+ return {type, suggest, then: Object.assign({
'': 0,
- ',': {type: 'operator', then: {
+ ',': {type: 'operator', suggest: true, then: {
'': 1,
}},
}, exits)};
}
- const CM_TEXT_TO_END = {type: 'string', then: {'': 0}};
- const CM_IDENT_LIST_TO_END = makeCMCommaBlock('variable');
- const CM_IDENT_LIST_TO_TEXT = makeCMCommaBlock('variable', {
- ':': {type: 'operator', then: {'': CM_TEXT_TO_END}},
+ const CM_TEXT_TO_END = {type: 'string', then: {'': 0, '\n': CM_END}};
+ const CM_AGENT_LIST_TO_END = makeCMCommaBlock('variable', 'Agent', {
+ '\n': CM_END,
+ });
+ const CM_AGENT_LIST_TO_TEXT = makeCMCommaBlock('variable', 'Agent', {
+ ':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}},
+ });
+ const CM_AGENT_LIST_TO_OPTTEXT = makeCMCommaBlock('variable', 'Agent', {
+ ':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}},
+ '\n': CM_END,
});
- const CM_NOTE_SIDE = {type: 'keyword', then: {
- 'of': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_TEXT}},
- ':': {type: 'operator', then: {'': CM_TEXT_TO_END}},
- '': CM_IDENT_LIST_TO_TEXT,
- }};
+ const CM_NOTE_SIDE_THEN = {
+ 'of': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_TEXT,
+ }},
+ ':': {type: 'operator', suggest: true, then: {
+ '': CM_TEXT_TO_END,
+ }},
+ '': CM_AGENT_LIST_TO_TEXT,
+ };
- const CM_CONNECT = {type: 'keyword', then: {
- '': CM_IDENT_LIST_TO_TEXT,
+ const CM_NOTE_LSIDE = {
+ type: 'keyword',
+ suggest: ['left of ', 'left: '],
+ then: CM_NOTE_SIDE_THEN,
+ };
+
+ const CM_NOTE_RSIDE = {
+ type: 'keyword',
+ suggest: ['right of ', 'right: '],
+ then: CM_NOTE_SIDE_THEN,
+ };
+
+ const CM_CONNECT = {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_OPTTEXT,
}};
const CM_COMMANDS = {type: 'error', then: {
- 'title': {type: 'keyword', then: {'': CM_TEXT_TO_END}},
- 'terminators': {type: 'keyword', then: {
- 'none': {type: 'keyword', then: {}},
- 'cross': {type: 'keyword', then: {}},
- 'box': {type: 'keyword', then: {}},
- 'bar': {type: 'keyword', then: {}},
- }},
- 'define': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_END}},
- 'begin': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_END}},
- 'end': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_END}},
- 'if': {type: 'keyword', then: {
+ 'title': {type: 'keyword', suggest: true, then: {
'': CM_TEXT_TO_END,
- ':': {type: 'operator', then: {'': CM_TEXT_TO_END}},
}},
- 'else': {type: 'keyword', then: {
- 'if': {type: 'keyword', then: {
+ 'terminators': {type: 'keyword', suggest: true, then: {
+ 'none': {type: 'keyword', suggest: true, then: {}},
+ 'cross': {type: 'keyword', suggest: true, then: {}},
+ 'box': {type: 'keyword', suggest: true, then: {}},
+ 'bar': {type: 'keyword', suggest: true, then: {}},
+ }},
+ 'define': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_END,
+ }},
+ 'begin': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_END,
+ }},
+ 'end': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_END,
+ '\n': CM_END,
+ }},
+ 'if': {type: 'keyword', suggest: true, then: {
+ '': CM_TEXT_TO_END,
+ ':': {type: 'operator', suggest: true, then: {
'': CM_TEXT_TO_END,
- ':': {type: 'operator', then: {'': CM_TEXT_TO_END}},
+ }},
+ '\n': CM_END,
+ }},
+ 'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
+ 'if': {type: 'keyword', suggest: 'if: ', then: {
+ '': CM_TEXT_TO_END,
+ ':': {type: 'operator', suggest: true, then: {
+ '': CM_TEXT_TO_END,
+ }},
+ }},
+ '\n': CM_END,
+ }},
+ 'repeat': {type: 'keyword', suggest: true, then: {
+ '': CM_TEXT_TO_END,
+ ':': {type: 'operator', suggest: true, then: {
+ '': CM_TEXT_TO_END,
+ }},
+ '\n': CM_END,
+ }},
+ 'note': {type: 'keyword', suggest: true, then: {
+ 'over': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_TEXT,
+ }},
+ 'left': CM_NOTE_LSIDE,
+ 'right': CM_NOTE_RSIDE,
+ 'between': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_TEXT,
}},
}},
- 'repeat': {type: 'keyword', then: {
- '': CM_TEXT_TO_END,
- ':': {type: 'operator', then: {'': CM_TEXT_TO_END}},
+ 'state': {type: 'keyword', suggest: 'state over ', then: {
+ 'over': {type: 'keyword', suggest: true, then: {
+ '': CM_AGENT_LIST_TO_TEXT,
+ }},
}},
- 'note': {type: 'keyword', then: {
- 'over': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_TEXT}},
- 'left': CM_NOTE_SIDE,
- 'right': CM_NOTE_SIDE,
- 'between': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_TEXT}},
+ 'text': {type: 'keyword', suggest: true, then: {
+ 'left': CM_NOTE_LSIDE,
+ 'right': CM_NOTE_RSIDE,
}},
- 'state': {type: 'keyword', then: {
- 'over': {type: 'keyword', then: {'': CM_IDENT_LIST_TO_TEXT}},
- }},
- 'text': {type: 'keyword', then: {
- 'left': CM_NOTE_SIDE,
- 'right': CM_NOTE_SIDE,
- }},
- 'simultaneously': {type: 'keyword', then: {
- ':': {type: 'operator', then: {}},
- 'with': {type: 'keyword', then: {
- '': {type: 'variable', then: {
+ 'simultaneously': {type: 'keyword', suggest: true, then: {
+ ':': {type: 'operator', suggest: true, then: {}},
+ 'with': {type: 'keyword', suggest: true, then: {
+ '': {type: 'variable', suggest: 'Label', then: {
'': 0,
- ':': {type: 'operator', then: {}},
+ ':': {type: 'operator', suggest: true, then: {}},
}},
}},
}},
- '': {type: 'variable', then: {
+ '': {type: 'variable', suggest: 'Agent', then: {
'->': CM_CONNECT,
'-->': CM_CONNECT,
'<-': CM_CONNECT,
'<--': CM_CONNECT,
'<->': CM_CONNECT,
'<-->': CM_CONNECT,
- ':': {type: 'operator', then: {}},
+ ':': {type: 'operator', suggest: true, override: 'Label', then: {}},
'': 0,
}},
}};
- function cmCheckToken(state, partial) {
- if(!partial && state.current === '\n') {
- // quoted newline is interpreted as a command separator;
- // probably not what the writer expected, so highlight it
- state.line.length = 0;
- return 'warning';
+ function cmGetSuggestions(state, token, {suggest, then}) {
+ if(token === '') {
+ return state['known' + suggest];
+ } else if(suggest === true) {
+ if(Object.keys(then).length > 0) {
+ return [token + ' '];
+ } else {
+ return [token + '\n'];
+ }
+ } else if(Array.isArray(suggest)) {
+ return suggest;
+ } else if(suggest) {
+ return [suggest];
+ } else {
+ return null;
}
+ }
+ function cmMakeCompletions(state, path) {
+ const comp = [];
+ const {then} = array.last(path);
+ Object.keys(then).forEach((token) => {
+ let next = then[token];
+ if(typeof next === 'number') {
+ next = path[path.length - next - 1];
+ }
+ array.mergeSets(comp, cmGetSuggestions(state, token, next));
+ });
+ return comp;
+ }
+
+ function updateSuggestion(state, locals, token, {suggest, override}) {
+ if(locals.type) {
+ if(suggest !== locals.type) {
+ if(override) {
+ locals.type = override;
+ }
+ array.mergeSets(
+ state['known' + locals.type],
+ [locals.value]
+ );
+ locals.type = '';
+ } else {
+ locals.value += token + ' ';
+ }
+ } else if(typeof suggest === 'string' && state['known' + suggest]) {
+ locals.type = suggest;
+ locals.value = token + ' ';
+ }
+ }
+
+ function cmCheckToken(state, eol) {
+ const suggestions = {
+ type: '',
+ value: '',
+ };
let current = CM_COMMANDS;
const path = [current];
- for(let i = 0; i < state.line.length; ++ i) {
- const token = state.line[i];
- let found = current.then[token] || current.then[''];
- if(found === undefined) {
- return 'error';
+
+ state.line.forEach((token, i) => {
+ if(i === state.line.length - 1) {
+ state.completions = cmMakeCompletions(state, path);
}
+ const found = current.then[token] || current.then[''];
if(typeof found === 'number') {
path.length -= found;
- current = array.last(path);
} else {
- path.push(found);
- current = found;
+ path.push(found || CM_ERROR);
}
+ current = array.last(path);
+ updateSuggestion(state, suggestions, token, current);
+ });
+ if(eol) {
+ updateSuggestion(state, suggestions, '', {});
}
+ state.nextCompletions = cmMakeCompletions(state, path);
return current.type;
}
@@ -122,6 +223,11 @@ define(['core/ArrayUtilities'], (array) => {
return {
currentType: -1,
current: '',
+ knownAgent: [],
+ knownLabel: [],
+ beginCompletions: cmMakeCompletions({}, [CM_COMMANDS]),
+ completions: [],
+ nextCompletions: [],
line: [],
indent: 0,
};
@@ -165,7 +271,13 @@ define(['core/ArrayUtilities'], (array) => {
return 'comment';
}
state.line.push(state.current);
- return cmCheckToken(state, false);
+ if(state.current === '\n') {
+ // quoted newline is interpreted as a command separator;
+ // probably not what the writer expected, so highlight it
+ state.line.length = 0;
+ return 'warning';
+ }
+ return cmCheckToken(state, stream.eol());
}
_tokenEOLFound(stream, state, block) {
@@ -174,7 +286,7 @@ define(['core/ArrayUtilities'], (array) => {
return 'comment';
}
state.line.push(state.current);
- const type = cmCheckToken(state, true);
+ const type = cmCheckToken(state, false);
state.line.pop();
return type;
}
@@ -194,6 +306,7 @@ define(['core/ArrayUtilities'], (array) => {
}
token(stream, state) {
+ state.completions = state.nextCompletions;
if(stream.sol() && state.currentType === -1) {
state.line.length = 0;
}
diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js
index a3c2304..e0e67a8 100644
--- a/scripts/sequence/Generator.js
+++ b/scripts/sequence/Generator.js
@@ -247,7 +247,7 @@ define(['core/ArrayUtilities'], (array) => {
meta: {
title: meta.title,
},
- agents: this.agents,
+ agents: this.agents.slice(),
stages: globals.stages,
};
}
diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js
index 207aab8..affa291 100644
--- a/scripts/sequence/Parser.js
+++ b/scripts/sequence/Parser.js
@@ -1,4 +1,12 @@
-define(['core/ArrayUtilities', './CodeMirrorMode'], (array, CMMode) => {
+define([
+ 'core/ArrayUtilities',
+ './CodeMirrorMode',
+ './CodeMirrorHints',
+], (
+ array,
+ CMMode,
+ CMHints
+) => {
'use strict';
function execAt(str, reg, i) {
@@ -355,6 +363,10 @@ define(['core/ArrayUtilities', './CodeMirrorMode'], (array, CMMode) => {
return new CMMode(TOKENS);
}
+ getCodeMirrorHints() {
+ return CMHints.getHints;
+ }
+
splitLines(tokens) {
const lines = [];
let line = [];
diff --git a/scripts/stubs/codemirror-comment.js b/scripts/stubs/codemirror-comment.js
new file mode 100644
index 0000000..752e825
--- /dev/null
+++ b/scripts/stubs/codemirror-comment.js
@@ -0,0 +1,5 @@
+define([], () => {
+ 'use strict';
+
+ return null;
+});
diff --git a/scripts/stubs/codemirror-show-hint.js b/scripts/stubs/codemirror-show-hint.js
new file mode 100644
index 0000000..752e825
--- /dev/null
+++ b/scripts/stubs/codemirror-show-hint.js
@@ -0,0 +1,5 @@
+define([], () => {
+ 'use strict';
+
+ return null;
+});
diff --git a/scripts/stubs/codemirror-trailingspace.js b/scripts/stubs/codemirror-trailingspace.js
new file mode 100644
index 0000000..752e825
--- /dev/null
+++ b/scripts/stubs/codemirror-trailingspace.js
@@ -0,0 +1,5 @@
+define([], () => {
+ 'use strict';
+
+ return null;
+});
diff --git a/scripts/stubs/codemirror.js b/scripts/stubs/codemirror.js
index 2b6b067..addbf19 100644
--- a/scripts/stubs/codemirror.js
+++ b/scripts/stubs/codemirror.js
@@ -13,6 +13,7 @@ define([], () => {
}
CodeMirror.defineMode = () => null;
+ CodeMirror.registerHelper = () => null;
return CodeMirror;
});
diff --git a/styles/main.css b/styles/main.css
index 92ca412..f163091 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -24,6 +24,17 @@ html, body {
.cm-s-default .cm-string {color: #221111;}
.cm-s-default .cm-error {color: #FF0000;}
+.cm-s-default .cm-warning {
+ background: #FFFF00;
+}
+.cm-s-default .cm-trailingspace {
+ background: rgba(255, 0, 0, 0.5);
+}
+
+.pick-virtual {
+ color: #777777;
+}
+
.pane-view {
position: absolute;
left: 30%;
diff --git a/test.htm b/test.htm
index 29359c3..0c5e630 100644
--- a/test.htm
+++ b/test.htm
@@ -57,7 +57,10 @@
data-integrity="sha256-Re9XxIL3x1flvE6WD58jWPdDzKYQLXwxS2HAVfmM6Z8="
>
-
+
+
+
+