diff --git a/web/lib/editor.js b/web/lib/editor.js index 2a006f1..025e65e 100644 --- a/web/lib/editor.js +++ b/web/lib/editor.js @@ -558,6 +558,8 @@ const DELAY_STAGECHANGE = 250; const PNG_RESOLUTION = 4; + const PARAM_PATTERN = /\{[^}]+\}/g; + function addNewline(value) { if(value.length > 0 && value.charAt(value.length - 1) !== '\n') { return value + '\n'; @@ -578,6 +580,23 @@ } } + function cmInRange(pos, {from, to}) { + return !( + pos.line < from.line || (pos.line === from.line && pos.ch < from.ch) || + pos.line > to.line || (pos.line === to.line && pos.ch > to.ch) + ); + } + + function findNextToken(block, skip) { + PARAM_PATTERN.lastIndex = 0; + for(let m = null; (m = PARAM_PATTERN.exec(block));) { + if(!skip.includes(m[0])) { + return m[0]; + } + } + return null; + } + function simplifyPreview(code) { return 'headers fade\nterminators fade\n' + code .replace(/\{Agent([0-9]*)\}/g, (match, num) => { @@ -775,7 +794,7 @@ const [touch] = e.touches; pt.x = touch.pageX; pt.y = touch.pageY; - }) + }, {passive: true}) .on('touchend', (e) => { if( pt.x === -1 || @@ -1158,7 +1177,7 @@ buildViewPane() { this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner') .add(this.diagram.dom()) - .on('touchstart', () => this._hideURLBuilder()) + .on('touchstart', () => this._hideURLBuilder(), {passive: true}) .on('mousedown', () => this._hideURLBuilder()); this.errorMsg = this.dom.el('div').setClass('msg-error'); @@ -1269,19 +1288,107 @@ this._enhanceEditor(); } + enterParams(start, end, block) { + const doc = this.code.getDoc(); + const endBookmark = doc.setBookmark(end); + const done = []; + + const keydown = (cm, event) => { + switch(event.keyCode) { + case 13: + case 9: + event.preventDefault(); + this.advanceParams(); + break; + case 27: + event.preventDefault(); + this.cancelParams(); + break; + } + }; + + const move = () => { + if(this.paramMarkers.length === 0) { + return; + } + const m = this.paramMarkers[0].find(); + const [r] = doc.listSelections(); + if(!cmInRange(r.anchor, m) || !cmInRange(r.head, m)) { + this.cancelParams(); + this.code.setSelection(r.anchor, r.head); + } + }; + + this.paramMarkers = []; + this.cancelParams = () => { + this.code.off('keydown', keydown); + this.code.off('cursorActivity', move); + this.paramMarkers.forEach((m) => m.clear()); + this.paramMarkers = null; + endBookmark.clear(); + this.code.setCursor(end); + this.cancelParams = null; + this.advanceParams = null; + }; + this.advanceParams = () => { + this.paramMarkers.forEach((m) => m.clear()); + this.paramMarkers.length = 0; + this.nextParams(start, endBookmark, block, done); + }; + + this.code.on('keydown', keydown); + this.code.on('cursorActivity', move); + + this.advanceParams(); + } + + nextParams(start, endBookmark, block, done) { + const tok = findNextToken(block, done); + if(!tok) { + this.cancelParams(); + return; + } + done.push(tok); + + const doc = this.code.getDoc(); + const ranges = []; + let {ch} = start; + for(let ln = start.line; ln < endBookmark.find().line; ++ ln) { + const line = doc.getLine(ln).slice(ch); + for(let p = 0; (p = line.indexOf(tok, p)) !== -1; p += tok.length) { + const anchor = {ch: p, line: ln}; + const head = {ch: p + tok.length, line: ln}; + ranges.push({anchor, head}); + this.paramMarkers.push(doc.markText(anchor, head, { + className: 'param', + clearWhenEmpty: false, + inclusiveLeft: true, + inclusiveRight: true, + })); + } + ch = 0; + } + + if(ranges.length > 0) { + doc.setSelections(ranges, 0); + } else { + this.cancelParams(); + } + } + addCodeBlock(block) { const lines = block.split('\n').length; if(this.code.getCursor) { const cur = this.code.getCursor('head'); const pos = {ch: 0, line: cur.line + ((cur.ch > 0) ? 1 : 0)}; - this.code.replaceRange( - addNewline(block), - pos, - null, - 'library' - ); - this.code.setCursor({ch: 0, line: pos.line + lines}); + let replaced = addNewline(block); + if(pos.line >= this.code.lineCount()) { + replaced = '\n' + replaced; + } + this.code.replaceRange(replaced, pos, null, 'library'); + const end = {ch: 0, line: pos.line + lines}; + this.enterParams(pos, end, block); } else { const value = this.value(); const cur = this.code.element.selectionStart; diff --git a/web/lib/editor.min.js b/web/lib/editor.min.js index 805ec4a..664a60c 100644 --- a/web/lib/editor.min.js +++ b/web/lib/editor.min.js @@ -1 +1 @@ -!function(){"use strict";function e(e){return null===e?null:e.element?e.element:e}function t(e){return e.length>0&&"\n"!==e.charAt(e.length-1)?e+"\n":e}function i(e,t){let i=0,s=0;for(;;){const n=e.indexOf("\n",i)+1;if(t {Agent2}: {Message}",title:"Simple arrow (synchronous)"},{code:"{Agent1} --\x3e {Agent2}: {Message}",title:"Arrow with dotted line (response)"},{code:"{Agent1} ->> {Agent2}: {Message}",title:"Open arrow (asynchronous)"},{code:"{Agent1} -x {Agent2}: {Message}",title:"Lost message"},{code:"{Agent1} ~> {Agent2}: {Message}",title:"Wavy line"},{code:"{Agent1} -> {Agent1}: {Message}",title:"Self-connection"},{code:"{Agent1} -> ...{id}\n...{id} -> {Agent2}: {Message}",preview:"begin A, B\nA -> ...x\n...x -> B: Message",title:"Asynchronous message"},{code:"* -> {Agent1}: {Message}",title:"Found message"},{code:"{Agent1} -> {Agent2}\n& {Agent1} -> {Agent3}: {Broadcast}",title:"Broadcast message"},{code:"{Agent1} -> +{Agent2}: {Request}\n{Agent1} <-- -{Agent2}: {Response}",title:"Request/response pair"},{code:"{Agent1} -> *{Agent2}: {Request}\n{Agent1} <-- !{Agent2}: {Response}",title:"Inline agent creation / destruction"},{code:"{Agent1} -> {Agent2}: {Request}\n{Agent1} <-- {Agent2}: {Response}\nend {Agent2}",preview:"begin A\n::\nA -> B: Request\nA <-- B: Response\nend B",title:"Agent creation / destruction"},{code:'autolabel "[]