Fix autocomplete for multi-word names, and minor tweaks. Add testing for CodeMirror integration [#34]
This commit is contained in:
parent
ece615e2a0
commit
64e3f6aa03
|
@ -626,32 +626,44 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
const end = {type: '', suggest: '\n', then: {}};
|
||||
const hiddenEnd = {type: '', then: {}};
|
||||
|
||||
function textTo(exit) {
|
||||
return {type: 'string', then: Object.assign({'': 0}, exit)};
|
||||
function textTo(exit, suggest) {
|
||||
return {
|
||||
type: 'string',
|
||||
suggest,
|
||||
then: Object.assign({'': 0}, exit),
|
||||
};
|
||||
}
|
||||
|
||||
const textToEnd = textTo({'\n': end});
|
||||
const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
|
||||
const aliasListToEnd = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
'\n': end,
|
||||
',': {type: 'operator', suggest: true, then: {'': 1}},
|
||||
'as': {type: 'keyword', suggest: true, then: {
|
||||
'': {type: 'variable', suggest: 'Agent', then: {
|
||||
'': {type: 'variable', suggest: {known: 'Agent'}, then: {
|
||||
'': 0,
|
||||
',': {type: 'operator', suggest: true, then: {'': 3}},
|
||||
'\n': end,
|
||||
}},
|
||||
}},
|
||||
}};
|
||||
},
|
||||
};
|
||||
|
||||
function agentListTo(exit) {
|
||||
return {type: 'variable', suggest: 'Agent', then: Object.assign({},
|
||||
return {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: Object.assign({},
|
||||
exit,
|
||||
{
|
||||
'': 0,
|
||||
',': {type: 'operator', suggest: true, then: {'': 1}},
|
||||
}
|
||||
)};
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const colonTextToEnd = {
|
||||
|
@ -662,32 +674,50 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
const agentListToText = agentListTo({
|
||||
':': colonTextToEnd,
|
||||
});
|
||||
const agentList2ToText = {type: 'variable', suggest: 'Agent', then: {
|
||||
const agentList2ToText = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
',': {type: 'operator', suggest: true, then: {'': agentListToText}},
|
||||
',': {type: 'operator', suggest: true, then: {
|
||||
'': agentListToText,
|
||||
}},
|
||||
':': CM_ERROR,
|
||||
}};
|
||||
const singleAgentToText = {type: 'variable', suggest: 'Agent', then: {
|
||||
},
|
||||
};
|
||||
const singleAgentToText = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
',': CM_ERROR,
|
||||
':': colonTextToEnd,
|
||||
}};
|
||||
const agentToOptText = {type: 'variable', suggest: 'Agent', then: {
|
||||
},
|
||||
};
|
||||
const agentToOptText = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
':': {type: 'operator', suggest: true, then: {
|
||||
'': textToEnd,
|
||||
'\n': hiddenEnd,
|
||||
}},
|
||||
'\n': end,
|
||||
}};
|
||||
},
|
||||
};
|
||||
const referenceName = {
|
||||
':': {type: 'operator', suggest: true, then: {
|
||||
'': textTo({
|
||||
'as': {type: 'keyword', suggest: true, then: {
|
||||
'': {type: 'variable', suggest: 'Agent', then: {
|
||||
'': {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
'\n': end,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}),
|
||||
}},
|
||||
|
@ -774,7 +804,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
then: {},
|
||||
};
|
||||
return makeOpBlock(
|
||||
{type: 'variable', suggest: 'Agent', then},
|
||||
{type: 'variable', suggest: {known: 'Agent'}, then},
|
||||
then
|
||||
);
|
||||
}
|
||||
|
@ -868,12 +898,16 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
}},
|
||||
'autolabel': {type: 'keyword', suggest: true, then: {
|
||||
'off': {type: 'keyword', suggest: true, then: {}},
|
||||
'': textToEnd,
|
||||
'': textTo({'\n': end}, [
|
||||
{v: '<label>', suffix: '\n', q: true},
|
||||
{v: '[<inc>] <label>', suffix: '\n', q: true},
|
||||
{v: '[<inc 1,0.01>] <label>', suffix: '\n', q: true},
|
||||
]),
|
||||
}},
|
||||
'simultaneously': {type: 'keyword', suggest: true, then: {
|
||||
':': {type: 'operator', suggest: true, then: {}},
|
||||
'with': {type: 'keyword', suggest: true, then: {
|
||||
'': {type: 'variable', suggest: 'Label', then: {
|
||||
'': {type: 'variable', suggest: {known: 'Label'}, then: {
|
||||
'': 0,
|
||||
':': {type: 'operator', suggest: true, then: {}},
|
||||
}},
|
||||
|
@ -897,31 +931,27 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
}
|
||||
|
||||
function cmGetVarSuggestions(state, previous, current) {
|
||||
if(typeof current.suggest === 'object' && current.suggest.global) {
|
||||
return [current.suggest];
|
||||
}
|
||||
if(
|
||||
typeof current.suggest !== 'string' ||
|
||||
previous.suggest === current.suggest
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return state['known' + current.suggest];
|
||||
function cmGetSuggestions(state, token, current) {
|
||||
let suggestions = current.suggest;
|
||||
if(!Array.isArray(suggestions)) {
|
||||
suggestions = [suggestions];
|
||||
}
|
||||
|
||||
function cmGetSuggestions(state, token, previous, current) {
|
||||
if(token === '') {
|
||||
return cmGetVarSuggestions(state, previous, current);
|
||||
} else if(current.suggest === true) {
|
||||
return array.flatMap(suggestions, (suggest) => {
|
||||
if(suggest === true) {
|
||||
return [cmCappedToken(token, current)];
|
||||
} else if(Array.isArray(current.suggest)) {
|
||||
return current.suggest.map((v) => ({v, q: false}));
|
||||
} else if(current.suggest) {
|
||||
return [{v: current.suggest, q: false}];
|
||||
} else if(typeof suggest === 'object') {
|
||||
if(suggest.known) {
|
||||
return state['known' + suggest.known] || [];
|
||||
} else {
|
||||
return null;
|
||||
return [suggest];
|
||||
}
|
||||
} else if(typeof suggest === 'string' && suggest) {
|
||||
return [{v: suggest, q: (token === '')}];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cmMakeCompletions(state, path) {
|
||||
|
@ -934,7 +964,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
array.mergeSets(
|
||||
comp,
|
||||
cmGetSuggestions(state, token, current, next),
|
||||
cmGetSuggestions(state, token, next),
|
||||
suggestionsEqual
|
||||
);
|
||||
});
|
||||
|
@ -942,8 +972,11 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
|
||||
function updateSuggestion(state, locals, token, {suggest, override}) {
|
||||
if(locals.type) {
|
||||
if(suggest !== locals.type) {
|
||||
let known = null;
|
||||
if(typeof suggest === 'object' && suggest.known) {
|
||||
known = suggest.known;
|
||||
}
|
||||
if(locals.type && known !== locals.type) {
|
||||
if(override) {
|
||||
locals.type = override;
|
||||
}
|
||||
|
@ -955,9 +988,8 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
locals.type = '';
|
||||
locals.value = '';
|
||||
}
|
||||
}
|
||||
if(typeof suggest === 'string' && state['known' + suggest]) {
|
||||
locals.type = suggest;
|
||||
if(known) {
|
||||
locals.type = known;
|
||||
if(locals.value) {
|
||||
locals.value += token.s;
|
||||
}
|
||||
|
@ -978,7 +1010,13 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
state.completions = cmMakeCompletions(state, path);
|
||||
}
|
||||
const keywordToken = token.q ? '' : token.v;
|
||||
const found = current.then[keywordToken] || current.then[''];
|
||||
let found = current.then[keywordToken];
|
||||
if(found === undefined) {
|
||||
found = current.then[''];
|
||||
state.isVar = true;
|
||||
} else {
|
||||
state.isVar = token.q;
|
||||
}
|
||||
if(typeof found === 'number') {
|
||||
path.length -= found;
|
||||
} else {
|
||||
|
@ -1025,6 +1063,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
completions: [],
|
||||
nextCompletions: [],
|
||||
valid: true,
|
||||
isVar: true,
|
||||
line: [],
|
||||
indent: 0,
|
||||
};
|
||||
|
@ -1119,6 +1158,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
|
|||
|
||||
token(stream, state) {
|
||||
state.completions = state.nextCompletions;
|
||||
state.isVar = true;
|
||||
if(stream.sol() && state.currentType === -1) {
|
||||
state.line.length = 0;
|
||||
}
|
||||
|
@ -5646,7 +5686,8 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
|
|||
const TRIMMER = /^([ \t]*)(.*)$/;
|
||||
const SQUASH_START = /^[ \t\r\n:,]/;
|
||||
const SQUASH_END = /[ \t\r\n]$/;
|
||||
const REQUIRED_QUOTED = /[\r\n:,"]/;
|
||||
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
|
||||
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
|
||||
const QUOTE_ESCAPE = /["\\]/g;
|
||||
|
||||
function suggestionsEqual(a, b) {
|
||||
|
@ -5658,29 +5699,36 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
|
|||
);
|
||||
}
|
||||
|
||||
function makeRanges(cm, line, chFrom, chTo) {
|
||||
function makeRangeFrom(cm, line, chFrom) {
|
||||
const ln = cm.getLine(line);
|
||||
const ranges = {
|
||||
wordFrom: {line: line, ch: chFrom},
|
||||
squashFrom: {line: line, ch: chFrom},
|
||||
wordTo: {line: line, ch: chTo},
|
||||
squashTo: {line: line, ch: chTo},
|
||||
word: {line: line, ch: chFrom},
|
||||
squash: {line: line, ch: chFrom},
|
||||
};
|
||||
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
|
||||
ranges.squashFrom.ch --;
|
||||
ranges.squash.ch --;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function makeRangeTo(cm, line, chTo) {
|
||||
const ln = cm.getLine(line);
|
||||
const ranges = {
|
||||
word: {line: line, ch: chTo},
|
||||
squash: {line: line, ch: chTo},
|
||||
};
|
||||
if(ln[chTo] === ' ') {
|
||||
ranges.squashTo.ch ++;
|
||||
ranges.squash.ch ++;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function wrapQuote(entry, quote) {
|
||||
if(!quote && entry.q && REQUIRED_QUOTED.test(entry.v)) {
|
||||
if(!quote && REQUIRED_QUOTED.test(entry.v)) {
|
||||
quote = '"';
|
||||
}
|
||||
let inner = entry.v;
|
||||
if(quote) {
|
||||
if(quote && entry.q) {
|
||||
inner = quote + inner.replace(QUOTE_ESCAPE, '\\$&') + quote;
|
||||
}
|
||||
return (entry.prefix || '') + inner + (entry.suffix || '');
|
||||
|
@ -5688,16 +5736,27 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
|
|||
|
||||
function makeHintItem(entry, ranges, quote) {
|
||||
const quoted = wrapQuote(entry, quote);
|
||||
const from = entry.q ? ranges.fromVar : ranges.fromKey;
|
||||
if(quoted === '\n') {
|
||||
return {
|
||||
text: '\n',
|
||||
displayText: '<END>',
|
||||
className: 'pick-virtual',
|
||||
from: from.squash,
|
||||
to: ranges.to.squash,
|
||||
displayFrom: null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: quoted,
|
||||
displayText: (quoted === '\n') ? '<END>' : quoted.trim(),
|
||||
className: (quoted === '\n') ? 'pick-virtual' : null,
|
||||
from: SQUASH_START.test(quoted) ?
|
||||
ranges.squashFrom : ranges.wordFrom,
|
||||
to: SQUASH_END.test(quoted) ?
|
||||
ranges.squashTo : ranges.wordTo,
|
||||
displayText: quoted.trim(),
|
||||
className: null,
|
||||
from: SQUASH_START.test(quoted) ? from.squash : from.word,
|
||||
to: SQUASH_END.test(quoted) ? ranges.to.squash : ranges.to.word,
|
||||
displayFrom: from.word,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getGlobals({global, prefix = '', suffix = ''}, globals) {
|
||||
const identified = globals[global];
|
||||
|
@ -5719,29 +5778,89 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
}
|
||||
|
||||
function getPartial(cur, token) {
|
||||
let partial = token.string;
|
||||
if(token.end > cur.ch) {
|
||||
partial = partial.substr(0, cur.ch - token.start);
|
||||
function getTokensUpTo(cm, pos) {
|
||||
const tokens = cm.getLineTokens(pos.line);
|
||||
for(let p = 0; p < tokens.length; ++ p) {
|
||||
if(tokens[p].end >= pos.ch) {
|
||||
tokens.length = p + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function getVariablePartial(tokens, pos) {
|
||||
let lastVariable = 0;
|
||||
let partial = '';
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
tokens.forEach((token, p) => {
|
||||
if(token.state.isVar) {
|
||||
partial += token.string;
|
||||
end = token.end;
|
||||
} else {
|
||||
lastVariable = p + 1;
|
||||
partial = '';
|
||||
start = token.end;
|
||||
}
|
||||
});
|
||||
if(end > pos.ch) {
|
||||
partial = partial.substr(0, pos.ch - start);
|
||||
}
|
||||
const parts = TRIMMER.exec(partial);
|
||||
partial = parts[2];
|
||||
let quote = '';
|
||||
if(partial[0] === '"') {
|
||||
if(ONGOING_QUOTE.test(partial)) {
|
||||
quote = partial[0];
|
||||
partial = partial.substr(1);
|
||||
}
|
||||
return {
|
||||
partial,
|
||||
quote,
|
||||
from: token.start + parts[1].length,
|
||||
from: start + parts[1].length,
|
||||
valid: end >= start,
|
||||
};
|
||||
}
|
||||
|
||||
function getKeywordPartial(token, pos) {
|
||||
let partial = token.string;
|
||||
if(token.end > pos.ch) {
|
||||
partial = partial.substr(0, pos.ch - token.start);
|
||||
}
|
||||
const parts = TRIMMER.exec(partial);
|
||||
return {
|
||||
partial: parts[2],
|
||||
from: token.start + parts[1].length,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
function suggestDropdownLocation(list, fromKey) {
|
||||
let p = null;
|
||||
list.forEach(({displayFrom}) => {
|
||||
if(displayFrom) {
|
||||
if(
|
||||
!p ||
|
||||
displayFrom.line > p.line ||
|
||||
(displayFrom.line === p.line && displayFrom.ch > p.ch)
|
||||
) {
|
||||
p = displayFrom;
|
||||
}
|
||||
}
|
||||
});
|
||||
return p || fromKey.word;
|
||||
}
|
||||
|
||||
function partialMatch(v, p) {
|
||||
return p.valid && v.startsWith(p.partial);
|
||||
}
|
||||
|
||||
function getHints(cm, options) {
|
||||
const cur = cm.getCursor();
|
||||
const token = cm.getTokenAt(cur);
|
||||
const {partial, from, quote} = getPartial(cur, token);
|
||||
const tokens = getTokensUpTo(cm, cur);
|
||||
const token = array.last(tokens) || cm.getTokenAt(cur);
|
||||
const pVar = getVariablePartial(tokens, cur);
|
||||
const pKey = getKeywordPartial(token, cur);
|
||||
|
||||
const continuation = (cur.ch > 0 && token.state.line.length > 0);
|
||||
let comp = (continuation ?
|
||||
|
@ -5754,31 +5873,36 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
|
|||
|
||||
populateGlobals(comp, cm.options.globals);
|
||||
|
||||
const ranges = makeRanges(cm, cur.line, from, token.end);
|
||||
let selfValid = false;
|
||||
const ranges = {
|
||||
fromVar: makeRangeFrom(cm, cur.line, pVar.from),
|
||||
fromKey: makeRangeFrom(cm, cur.line, pKey.from),
|
||||
to: makeRangeTo(cm, cur.line, token.end),
|
||||
};
|
||||
let selfValid = null;
|
||||
const list = (comp
|
||||
.filter(({v, q}) => (q || !quote) && v.startsWith(partial))
|
||||
.filter((o) => (
|
||||
(o.q || !pVar.quote) &&
|
||||
partialMatch(o.v, o.q ? pVar : pKey)
|
||||
))
|
||||
.map((o) => {
|
||||
if(o.v === partial + ' ' && !options.completeSingle) {
|
||||
selfValid = true;
|
||||
if(!options.completeSingle) {
|
||||
if(o.v === (o.q ? pVar : pKey).partial) {
|
||||
selfValid = o;
|
||||
return null;
|
||||
}
|
||||
return makeHintItem(o, ranges, quote);
|
||||
}
|
||||
return makeHintItem(o, ranges, pVar.quote);
|
||||
})
|
||||
.filter((opt) => (opt !== null))
|
||||
);
|
||||
if(selfValid && list.length > 0) {
|
||||
list.unshift(makeHintItem(
|
||||
{v: partial, suffix: ' ', q: false},
|
||||
ranges,
|
||||
quote
|
||||
));
|
||||
list.unshift(makeHintItem(selfValid, ranges, pVar.quote));
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
from: ranges.wordFrom,
|
||||
to: ranges.wordTo,
|
||||
from: suggestDropdownLocation(list, ranges.fromKey),
|
||||
to: ranges.to.word,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,7 +4,8 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
const TRIMMER = /^([ \t]*)(.*)$/;
|
||||
const SQUASH_START = /^[ \t\r\n:,]/;
|
||||
const SQUASH_END = /[ \t\r\n]$/;
|
||||
const REQUIRED_QUOTED = /[\r\n:,"]/;
|
||||
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
|
||||
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
|
||||
const QUOTE_ESCAPE = /["\\]/g;
|
||||
|
||||
function suggestionsEqual(a, b) {
|
||||
|
@ -16,29 +17,36 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
);
|
||||
}
|
||||
|
||||
function makeRanges(cm, line, chFrom, chTo) {
|
||||
function makeRangeFrom(cm, line, chFrom) {
|
||||
const ln = cm.getLine(line);
|
||||
const ranges = {
|
||||
wordFrom: {line: line, ch: chFrom},
|
||||
squashFrom: {line: line, ch: chFrom},
|
||||
wordTo: {line: line, ch: chTo},
|
||||
squashTo: {line: line, ch: chTo},
|
||||
word: {line: line, ch: chFrom},
|
||||
squash: {line: line, ch: chFrom},
|
||||
};
|
||||
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
|
||||
ranges.squashFrom.ch --;
|
||||
ranges.squash.ch --;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function makeRangeTo(cm, line, chTo) {
|
||||
const ln = cm.getLine(line);
|
||||
const ranges = {
|
||||
word: {line: line, ch: chTo},
|
||||
squash: {line: line, ch: chTo},
|
||||
};
|
||||
if(ln[chTo] === ' ') {
|
||||
ranges.squashTo.ch ++;
|
||||
ranges.squash.ch ++;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function wrapQuote(entry, quote) {
|
||||
if(!quote && entry.q && REQUIRED_QUOTED.test(entry.v)) {
|
||||
if(!quote && REQUIRED_QUOTED.test(entry.v)) {
|
||||
quote = '"';
|
||||
}
|
||||
let inner = entry.v;
|
||||
if(quote) {
|
||||
if(quote && entry.q) {
|
||||
inner = quote + inner.replace(QUOTE_ESCAPE, '\\$&') + quote;
|
||||
}
|
||||
return (entry.prefix || '') + inner + (entry.suffix || '');
|
||||
|
@ -46,16 +54,27 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
|
||||
function makeHintItem(entry, ranges, quote) {
|
||||
const quoted = wrapQuote(entry, quote);
|
||||
const from = entry.q ? ranges.fromVar : ranges.fromKey;
|
||||
if(quoted === '\n') {
|
||||
return {
|
||||
text: '\n',
|
||||
displayText: '<END>',
|
||||
className: 'pick-virtual',
|
||||
from: from.squash,
|
||||
to: ranges.to.squash,
|
||||
displayFrom: null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: quoted,
|
||||
displayText: (quoted === '\n') ? '<END>' : quoted.trim(),
|
||||
className: (quoted === '\n') ? 'pick-virtual' : null,
|
||||
from: SQUASH_START.test(quoted) ?
|
||||
ranges.squashFrom : ranges.wordFrom,
|
||||
to: SQUASH_END.test(quoted) ?
|
||||
ranges.squashTo : ranges.wordTo,
|
||||
displayText: quoted.trim(),
|
||||
className: null,
|
||||
from: SQUASH_START.test(quoted) ? from.squash : from.word,
|
||||
to: SQUASH_END.test(quoted) ? ranges.to.squash : ranges.to.word,
|
||||
displayFrom: from.word,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getGlobals({global, prefix = '', suffix = ''}, globals) {
|
||||
const identified = globals[global];
|
||||
|
@ -77,29 +96,89 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
}
|
||||
|
||||
function getPartial(cur, token) {
|
||||
let partial = token.string;
|
||||
if(token.end > cur.ch) {
|
||||
partial = partial.substr(0, cur.ch - token.start);
|
||||
function getTokensUpTo(cm, pos) {
|
||||
const tokens = cm.getLineTokens(pos.line);
|
||||
for(let p = 0; p < tokens.length; ++ p) {
|
||||
if(tokens[p].end >= pos.ch) {
|
||||
tokens.length = p + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function getVariablePartial(tokens, pos) {
|
||||
let lastVariable = 0;
|
||||
let partial = '';
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
tokens.forEach((token, p) => {
|
||||
if(token.state.isVar) {
|
||||
partial += token.string;
|
||||
end = token.end;
|
||||
} else {
|
||||
lastVariable = p + 1;
|
||||
partial = '';
|
||||
start = token.end;
|
||||
}
|
||||
});
|
||||
if(end > pos.ch) {
|
||||
partial = partial.substr(0, pos.ch - start);
|
||||
}
|
||||
const parts = TRIMMER.exec(partial);
|
||||
partial = parts[2];
|
||||
let quote = '';
|
||||
if(partial[0] === '"') {
|
||||
if(ONGOING_QUOTE.test(partial)) {
|
||||
quote = partial[0];
|
||||
partial = partial.substr(1);
|
||||
}
|
||||
return {
|
||||
partial,
|
||||
quote,
|
||||
from: token.start + parts[1].length,
|
||||
from: start + parts[1].length,
|
||||
valid: end >= start,
|
||||
};
|
||||
}
|
||||
|
||||
function getKeywordPartial(token, pos) {
|
||||
let partial = token.string;
|
||||
if(token.end > pos.ch) {
|
||||
partial = partial.substr(0, pos.ch - token.start);
|
||||
}
|
||||
const parts = TRIMMER.exec(partial);
|
||||
return {
|
||||
partial: parts[2],
|
||||
from: token.start + parts[1].length,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
function suggestDropdownLocation(list, fromKey) {
|
||||
let p = null;
|
||||
list.forEach(({displayFrom}) => {
|
||||
if(displayFrom) {
|
||||
if(
|
||||
!p ||
|
||||
displayFrom.line > p.line ||
|
||||
(displayFrom.line === p.line && displayFrom.ch > p.ch)
|
||||
) {
|
||||
p = displayFrom;
|
||||
}
|
||||
}
|
||||
});
|
||||
return p || fromKey.word;
|
||||
}
|
||||
|
||||
function partialMatch(v, p) {
|
||||
return p.valid && v.startsWith(p.partial);
|
||||
}
|
||||
|
||||
function getHints(cm, options) {
|
||||
const cur = cm.getCursor();
|
||||
const token = cm.getTokenAt(cur);
|
||||
const {partial, from, quote} = getPartial(cur, token);
|
||||
const tokens = getTokensUpTo(cm, cur);
|
||||
const token = array.last(tokens) || cm.getTokenAt(cur);
|
||||
const pVar = getVariablePartial(tokens, cur);
|
||||
const pKey = getKeywordPartial(token, cur);
|
||||
|
||||
const continuation = (cur.ch > 0 && token.state.line.length > 0);
|
||||
let comp = (continuation ?
|
||||
|
@ -112,31 +191,36 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
|
||||
populateGlobals(comp, cm.options.globals);
|
||||
|
||||
const ranges = makeRanges(cm, cur.line, from, token.end);
|
||||
let selfValid = false;
|
||||
const ranges = {
|
||||
fromVar: makeRangeFrom(cm, cur.line, pVar.from),
|
||||
fromKey: makeRangeFrom(cm, cur.line, pKey.from),
|
||||
to: makeRangeTo(cm, cur.line, token.end),
|
||||
};
|
||||
let selfValid = null;
|
||||
const list = (comp
|
||||
.filter(({v, q}) => (q || !quote) && v.startsWith(partial))
|
||||
.filter((o) => (
|
||||
(o.q || !pVar.quote) &&
|
||||
partialMatch(o.v, o.q ? pVar : pKey)
|
||||
))
|
||||
.map((o) => {
|
||||
if(o.v === partial + ' ' && !options.completeSingle) {
|
||||
selfValid = true;
|
||||
if(!options.completeSingle) {
|
||||
if(o.v === (o.q ? pVar : pKey).partial) {
|
||||
selfValid = o;
|
||||
return null;
|
||||
}
|
||||
return makeHintItem(o, ranges, quote);
|
||||
}
|
||||
return makeHintItem(o, ranges, pVar.quote);
|
||||
})
|
||||
.filter((opt) => (opt !== null))
|
||||
);
|
||||
if(selfValid && list.length > 0) {
|
||||
list.unshift(makeHintItem(
|
||||
{v: partial, suffix: ' ', q: false},
|
||||
ranges,
|
||||
quote
|
||||
));
|
||||
list.unshift(makeHintItem(selfValid, ranges, pVar.quote));
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
from: ranges.wordFrom,
|
||||
to: ranges.wordTo,
|
||||
from: suggestDropdownLocation(list, ranges.fromKey),
|
||||
to: ranges.to.word,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -23,32 +23,44 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
const end = {type: '', suggest: '\n', then: {}};
|
||||
const hiddenEnd = {type: '', then: {}};
|
||||
|
||||
function textTo(exit) {
|
||||
return {type: 'string', then: Object.assign({'': 0}, exit)};
|
||||
function textTo(exit, suggest) {
|
||||
return {
|
||||
type: 'string',
|
||||
suggest,
|
||||
then: Object.assign({'': 0}, exit),
|
||||
};
|
||||
}
|
||||
|
||||
const textToEnd = textTo({'\n': end});
|
||||
const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
|
||||
const aliasListToEnd = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
'\n': end,
|
||||
',': {type: 'operator', suggest: true, then: {'': 1}},
|
||||
'as': {type: 'keyword', suggest: true, then: {
|
||||
'': {type: 'variable', suggest: 'Agent', then: {
|
||||
'': {type: 'variable', suggest: {known: 'Agent'}, then: {
|
||||
'': 0,
|
||||
',': {type: 'operator', suggest: true, then: {'': 3}},
|
||||
'\n': end,
|
||||
}},
|
||||
}},
|
||||
}};
|
||||
},
|
||||
};
|
||||
|
||||
function agentListTo(exit) {
|
||||
return {type: 'variable', suggest: 'Agent', then: Object.assign({},
|
||||
return {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: Object.assign({},
|
||||
exit,
|
||||
{
|
||||
'': 0,
|
||||
',': {type: 'operator', suggest: true, then: {'': 1}},
|
||||
}
|
||||
)};
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const colonTextToEnd = {
|
||||
|
@ -59,32 +71,50 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
const agentListToText = agentListTo({
|
||||
':': colonTextToEnd,
|
||||
});
|
||||
const agentList2ToText = {type: 'variable', suggest: 'Agent', then: {
|
||||
const agentList2ToText = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
',': {type: 'operator', suggest: true, then: {'': agentListToText}},
|
||||
',': {type: 'operator', suggest: true, then: {
|
||||
'': agentListToText,
|
||||
}},
|
||||
':': CM_ERROR,
|
||||
}};
|
||||
const singleAgentToText = {type: 'variable', suggest: 'Agent', then: {
|
||||
},
|
||||
};
|
||||
const singleAgentToText = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
',': CM_ERROR,
|
||||
':': colonTextToEnd,
|
||||
}};
|
||||
const agentToOptText = {type: 'variable', suggest: 'Agent', then: {
|
||||
},
|
||||
};
|
||||
const agentToOptText = {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
':': {type: 'operator', suggest: true, then: {
|
||||
'': textToEnd,
|
||||
'\n': hiddenEnd,
|
||||
}},
|
||||
'\n': end,
|
||||
}};
|
||||
},
|
||||
};
|
||||
const referenceName = {
|
||||
':': {type: 'operator', suggest: true, then: {
|
||||
'': textTo({
|
||||
'as': {type: 'keyword', suggest: true, then: {
|
||||
'': {type: 'variable', suggest: 'Agent', then: {
|
||||
'': {
|
||||
type: 'variable',
|
||||
suggest: {known: 'Agent'},
|
||||
then: {
|
||||
'': 0,
|
||||
'\n': end,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}),
|
||||
}},
|
||||
|
@ -171,7 +201,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
then: {},
|
||||
};
|
||||
return makeOpBlock(
|
||||
{type: 'variable', suggest: 'Agent', then},
|
||||
{type: 'variable', suggest: {known: 'Agent'}, then},
|
||||
then
|
||||
);
|
||||
}
|
||||
|
@ -265,12 +295,16 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}},
|
||||
'autolabel': {type: 'keyword', suggest: true, then: {
|
||||
'off': {type: 'keyword', suggest: true, then: {}},
|
||||
'': textToEnd,
|
||||
'': textTo({'\n': end}, [
|
||||
{v: '<label>', suffix: '\n', q: true},
|
||||
{v: '[<inc>] <label>', suffix: '\n', q: true},
|
||||
{v: '[<inc 1,0.01>] <label>', suffix: '\n', q: true},
|
||||
]),
|
||||
}},
|
||||
'simultaneously': {type: 'keyword', suggest: true, then: {
|
||||
':': {type: 'operator', suggest: true, then: {}},
|
||||
'with': {type: 'keyword', suggest: true, then: {
|
||||
'': {type: 'variable', suggest: 'Label', then: {
|
||||
'': {type: 'variable', suggest: {known: 'Label'}, then: {
|
||||
'': 0,
|
||||
':': {type: 'operator', suggest: true, then: {}},
|
||||
}},
|
||||
|
@ -294,31 +328,27 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
}
|
||||
|
||||
function cmGetVarSuggestions(state, previous, current) {
|
||||
if(typeof current.suggest === 'object' && current.suggest.global) {
|
||||
return [current.suggest];
|
||||
}
|
||||
if(
|
||||
typeof current.suggest !== 'string' ||
|
||||
previous.suggest === current.suggest
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return state['known' + current.suggest];
|
||||
function cmGetSuggestions(state, token, current) {
|
||||
let suggestions = current.suggest;
|
||||
if(!Array.isArray(suggestions)) {
|
||||
suggestions = [suggestions];
|
||||
}
|
||||
|
||||
function cmGetSuggestions(state, token, previous, current) {
|
||||
if(token === '') {
|
||||
return cmGetVarSuggestions(state, previous, current);
|
||||
} else if(current.suggest === true) {
|
||||
return array.flatMap(suggestions, (suggest) => {
|
||||
if(suggest === true) {
|
||||
return [cmCappedToken(token, current)];
|
||||
} else if(Array.isArray(current.suggest)) {
|
||||
return current.suggest.map((v) => ({v, q: false}));
|
||||
} else if(current.suggest) {
|
||||
return [{v: current.suggest, q: false}];
|
||||
} else if(typeof suggest === 'object') {
|
||||
if(suggest.known) {
|
||||
return state['known' + suggest.known] || [];
|
||||
} else {
|
||||
return null;
|
||||
return [suggest];
|
||||
}
|
||||
} else if(typeof suggest === 'string' && suggest) {
|
||||
return [{v: suggest, q: (token === '')}];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cmMakeCompletions(state, path) {
|
||||
|
@ -331,7 +361,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
array.mergeSets(
|
||||
comp,
|
||||
cmGetSuggestions(state, token, current, next),
|
||||
cmGetSuggestions(state, token, next),
|
||||
suggestionsEqual
|
||||
);
|
||||
});
|
||||
|
@ -339,8 +369,11 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
}
|
||||
|
||||
function updateSuggestion(state, locals, token, {suggest, override}) {
|
||||
if(locals.type) {
|
||||
if(suggest !== locals.type) {
|
||||
let known = null;
|
||||
if(typeof suggest === 'object' && suggest.known) {
|
||||
known = suggest.known;
|
||||
}
|
||||
if(locals.type && known !== locals.type) {
|
||||
if(override) {
|
||||
locals.type = override;
|
||||
}
|
||||
|
@ -352,9 +385,8 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
locals.type = '';
|
||||
locals.value = '';
|
||||
}
|
||||
}
|
||||
if(typeof suggest === 'string' && state['known' + suggest]) {
|
||||
locals.type = suggest;
|
||||
if(known) {
|
||||
locals.type = known;
|
||||
if(locals.value) {
|
||||
locals.value += token.s;
|
||||
}
|
||||
|
@ -375,7 +407,13 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
state.completions = cmMakeCompletions(state, path);
|
||||
}
|
||||
const keywordToken = token.q ? '' : token.v;
|
||||
const found = current.then[keywordToken] || current.then[''];
|
||||
let found = current.then[keywordToken];
|
||||
if(found === undefined) {
|
||||
found = current.then[''];
|
||||
state.isVar = true;
|
||||
} else {
|
||||
state.isVar = token.q;
|
||||
}
|
||||
if(typeof found === 'number') {
|
||||
path.length -= found;
|
||||
} else {
|
||||
|
@ -422,6 +460,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
completions: [],
|
||||
nextCompletions: [],
|
||||
valid: true,
|
||||
isVar: true,
|
||||
line: [],
|
||||
indent: 0,
|
||||
};
|
||||
|
@ -516,6 +555,7 @@ define(['core/ArrayUtilities'], (array) => {
|
|||
|
||||
token(stream, state) {
|
||||
state.completions = state.nextCompletions;
|
||||
state.isVar = true;
|
||||
if(stream.sol() && state.currentType === -1) {
|
||||
state.line.length = 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,521 @@
|
|||
defineDescribe('Code Mirror Mode', [
|
||||
'./SequenceDiagram',
|
||||
'cm/lib/codemirror-real',
|
||||
], (
|
||||
SequenceDiagram,
|
||||
CodeMirror
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
SequenceDiagram.registerCodeMirrorMode(CodeMirror);
|
||||
|
||||
const cm = new CodeMirror(null, {
|
||||
value: '',
|
||||
mode: 'sequence',
|
||||
globals: {
|
||||
themes: ['Theme', 'Other Theme'],
|
||||
},
|
||||
});
|
||||
|
||||
function getTokens(line) {
|
||||
return cm.getLineTokens(line).map((token) => ({
|
||||
v: token.string,
|
||||
type: token.type,
|
||||
}));
|
||||
}
|
||||
|
||||
describe('colouring', () => {
|
||||
it('highlights comments', () => {
|
||||
cm.getDoc().setValue('# foo');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: '# foo', type: 'comment'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights valid keywords', () => {
|
||||
cm.getDoc().setValue('terminators cross');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'terminators', type: 'keyword'},
|
||||
{v: ' cross', type: 'keyword'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights comments after content', () => {
|
||||
cm.getDoc().setValue('terminators cross # foo');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'terminators', type: 'keyword'},
|
||||
{v: ' cross', type: 'keyword'},
|
||||
{v: ' # foo', type: 'comment'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights invalid lines', () => {
|
||||
cm.getDoc().setValue('terminators huh');
|
||||
expect(getTokens(0)[1].type).toContain('line-error');
|
||||
});
|
||||
|
||||
it('highlights incomplete lines', () => {
|
||||
cm.getDoc().setValue('terminators');
|
||||
expect(getTokens(0)[0].type).toContain('line-error');
|
||||
});
|
||||
|
||||
it('highlights free text', () => {
|
||||
cm.getDoc().setValue('title my free text');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'title', type: 'keyword'},
|
||||
{v: ' my', type: 'string'},
|
||||
{v: ' free', type: 'string'},
|
||||
{v: ' text', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights quoted text', () => {
|
||||
cm.getDoc().setValue('title "my free text"');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'title', type: 'keyword'},
|
||||
{v: ' "my free text"', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights agent names', () => {
|
||||
cm.getDoc().setValue('A -> B');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'A', type: 'variable'},
|
||||
{v: ' ->', type: 'keyword'},
|
||||
{v: ' B', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not consider quoted tokens as keywords', () => {
|
||||
cm.getDoc().setValue('A "->" -> B');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'A', type: 'variable'},
|
||||
{v: ' "->"', type: 'variable'},
|
||||
{v: ' ->', type: 'keyword'},
|
||||
{v: ' B', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights agent aliasing syntax', () => {
|
||||
cm.getDoc().setValue('define A as B, C as D');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'define', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ' as', type: 'keyword'},
|
||||
{v: ' B', type: 'variable'},
|
||||
{v: ',', type: 'operator'},
|
||||
{v: ' C', type: 'variable'},
|
||||
{v: ' as', type: 'keyword'},
|
||||
{v: ' D', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights multi-word agent names', () => {
|
||||
cm.getDoc().setValue('Foo Bar -> Zig Zag');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'Foo', type: 'variable'},
|
||||
{v: ' Bar', type: 'variable'},
|
||||
{v: ' ->', type: 'keyword'},
|
||||
{v: ' Zig', type: 'variable'},
|
||||
{v: ' Zag', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights connection operators without spaces', () => {
|
||||
cm.getDoc().setValue('abc->xyz');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'abc', type: 'variable'},
|
||||
{v: '->', type: 'keyword'},
|
||||
{v: 'xyz', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights the lost message operator without spaces', () => {
|
||||
cm.getDoc().setValue('abc-xxyz');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'abc', type: 'variable'},
|
||||
{v: '-x', type: 'keyword'},
|
||||
{v: 'xyz', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('recognises agent flags', () => {
|
||||
cm.getDoc().setValue('Foo -> *Bar');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'Foo', type: 'variable'},
|
||||
{v: ' ->', type: 'keyword'},
|
||||
{v: ' *', type: 'operator'},
|
||||
{v: 'Bar', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects missing agent names', () => {
|
||||
cm.getDoc().setValue('+ -> Bar');
|
||||
expect(getTokens(0)[2].type).toContain('line-error');
|
||||
|
||||
cm.getDoc().setValue('Bar -> +');
|
||||
expect(getTokens(0)[2].type).toContain('line-error');
|
||||
});
|
||||
|
||||
it('recognises found messages', () => {
|
||||
cm.getDoc().setValue('* -> Bar');
|
||||
expect(getTokens(0)[2].type).not.toContain('line-error');
|
||||
|
||||
cm.getDoc().setValue('Bar <- *');
|
||||
expect(getTokens(0)[2].type).not.toContain('line-error');
|
||||
});
|
||||
|
||||
it('recognises combined agent flags', () => {
|
||||
cm.getDoc().setValue('Foo -> +*Bar');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'Foo', type: 'variable'},
|
||||
{v: ' ->', type: 'keyword'},
|
||||
{v: ' +', type: 'operator'},
|
||||
{v: '*', type: 'operator'},
|
||||
{v: 'Bar', type: 'variable'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows messages after connections', () => {
|
||||
cm.getDoc().setValue('Foo -> Bar: hello');
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'Foo', type: 'variable'},
|
||||
{v: ' ->', type: 'keyword'},
|
||||
{v: ' Bar', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hello', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('recognises invalid agent flag combinations', () => {
|
||||
cm.getDoc().setValue('Foo -> *!Bar');
|
||||
expect(getTokens(0)[3].type).toContain('line-error');
|
||||
|
||||
cm.getDoc().setValue('Foo -> +-Bar');
|
||||
expect(getTokens(0)[3].type).toContain('line-error');
|
||||
|
||||
cm.getDoc().setValue('Foo -> +*-Bar');
|
||||
expect(getTokens(0)[4].type).toContain('line-error');
|
||||
});
|
||||
|
||||
it('highlights block statements', () => {
|
||||
cm.getDoc().setValue(
|
||||
'if\n' +
|
||||
'if something\n' +
|
||||
'else if another thing\n' +
|
||||
'else\n' +
|
||||
'end\n' +
|
||||
'repeat a few times'
|
||||
);
|
||||
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'if', type: 'keyword'},
|
||||
]);
|
||||
|
||||
expect(getTokens(1)).toEqual([
|
||||
{v: 'if', type: 'keyword'},
|
||||
{v: ' something', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(2)).toEqual([
|
||||
{v: 'else', type: 'keyword'},
|
||||
{v: ' if', type: 'keyword'},
|
||||
{v: ' another', type: 'string'},
|
||||
{v: ' thing', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(3)).toEqual([
|
||||
{v: 'else', type: 'keyword'},
|
||||
]);
|
||||
|
||||
expect(getTokens(4)).toEqual([
|
||||
{v: 'end', type: 'keyword'},
|
||||
]);
|
||||
|
||||
expect(getTokens(5)).toEqual([
|
||||
{v: 'repeat', type: 'keyword'},
|
||||
{v: ' a', type: 'string'},
|
||||
{v: ' few', type: 'string'},
|
||||
{v: ' times', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows colons in block statements', () => {
|
||||
cm.getDoc().setValue(
|
||||
'if: something\n' +
|
||||
'else if: another thing\n' +
|
||||
'repeat: a few times'
|
||||
);
|
||||
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'if', type: 'keyword'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' something', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(1)).toEqual([
|
||||
{v: 'else', type: 'keyword'},
|
||||
{v: ' if', type: 'keyword'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' another', type: 'string'},
|
||||
{v: ' thing', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(2)).toEqual([
|
||||
{v: 'repeat', type: 'keyword'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' a', type: 'string'},
|
||||
{v: ' few', type: 'string'},
|
||||
{v: ' times', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('highlights note statements', () => {
|
||||
cm.getDoc().setValue(
|
||||
'note over A: hi\n' +
|
||||
'note over A, B: hi\n' +
|
||||
'note left of A, B: hi\n' +
|
||||
'note right of A, B: hi\n' +
|
||||
'note between A, B: hi'
|
||||
);
|
||||
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'note', type: 'keyword'},
|
||||
{v: ' over', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hi', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(1)).toEqual([
|
||||
{v: 'note', type: 'keyword'},
|
||||
{v: ' over', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ',', type: 'operator'},
|
||||
{v: ' B', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hi', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(2)).toEqual([
|
||||
{v: 'note', type: 'keyword'},
|
||||
{v: ' left', type: 'keyword'},
|
||||
{v: ' of', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ',', type: 'operator'},
|
||||
{v: ' B', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hi', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(3)).toEqual([
|
||||
{v: 'note', type: 'keyword'},
|
||||
{v: ' right', type: 'keyword'},
|
||||
{v: ' of', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ',', type: 'operator'},
|
||||
{v: ' B', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hi', type: 'string'},
|
||||
]);
|
||||
|
||||
expect(getTokens(4)).toEqual([
|
||||
{v: 'note', type: 'keyword'},
|
||||
{v: ' between', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ',', type: 'operator'},
|
||||
{v: ' B', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hi', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects notes between a single agent', () => {
|
||||
cm.getDoc().setValue('note between A: hi');
|
||||
expect(getTokens(0)[3].type).toContain('line-error');
|
||||
});
|
||||
|
||||
it('highlights state statements', () => {
|
||||
cm.getDoc().setValue('state over A: hi');
|
||||
|
||||
expect(getTokens(0)).toEqual([
|
||||
{v: 'state', type: 'keyword'},
|
||||
{v: ' over', type: 'keyword'},
|
||||
{v: ' A', type: 'variable'},
|
||||
{v: ':', type: 'operator'},
|
||||
{v: ' hi', type: 'string'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects state over multiple agents', () => {
|
||||
cm.getDoc().setValue('state over A, B: hi');
|
||||
expect(getTokens(0)[3].type).toContain('line-error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('autocomplete', () => {
|
||||
function getHints(pos, {completeSingle = true} = {}) {
|
||||
const hintFn = cm.getHelpers(pos, 'hint')[0];
|
||||
cm.setCursor(pos);
|
||||
return hintFn(cm, Object.assign({completeSingle}, cm.options));
|
||||
}
|
||||
|
||||
function getHintTexts(pos, options) {
|
||||
const hints = getHints(pos, options);
|
||||
return hints.list.map((hint) => hint.text);
|
||||
}
|
||||
|
||||
it('suggests commands when used at the start of a line', () => {
|
||||
cm.getDoc().setValue('');
|
||||
const hints = getHintTexts({line: 0, ch: 0});
|
||||
expect(hints).toContain('theme ');
|
||||
expect(hints).toContain('title ');
|
||||
expect(hints).toContain('headers ');
|
||||
expect(hints).toContain('terminators ');
|
||||
expect(hints).toContain('define ');
|
||||
expect(hints).toContain('begin ');
|
||||
expect(hints).toContain('end ');
|
||||
expect(hints).toContain('if ');
|
||||
expect(hints).toContain('else\n');
|
||||
expect(hints).toContain('else if: ');
|
||||
expect(hints).toContain('repeat ');
|
||||
expect(hints).toContain('note ');
|
||||
expect(hints).toContain('state over ');
|
||||
expect(hints).toContain('text ');
|
||||
expect(hints).toContain('autolabel ');
|
||||
expect(hints).toContain('simultaneously ');
|
||||
});
|
||||
|
||||
it('suggests known header types', () => {
|
||||
cm.getDoc().setValue('headers ');
|
||||
const hints = getHintTexts({line: 0, ch: 8});
|
||||
expect(hints).toEqual([
|
||||
'none\n',
|
||||
'cross\n',
|
||||
'box\n',
|
||||
'fade\n',
|
||||
'bar\n',
|
||||
]);
|
||||
});
|
||||
|
||||
it('suggests known terminator types', () => {
|
||||
cm.getDoc().setValue('terminators ');
|
||||
const hints = getHintTexts({line: 0, ch: 12});
|
||||
expect(hints).toEqual([
|
||||
'none\n',
|
||||
'cross\n',
|
||||
'box\n',
|
||||
'fade\n',
|
||||
'bar\n',
|
||||
]);
|
||||
});
|
||||
|
||||
it('suggests useful autolabel values', () => {
|
||||
cm.getDoc().setValue('autolabel ');
|
||||
const hints = getHintTexts({line: 0, ch: 10});
|
||||
expect(hints).toContain('off\n');
|
||||
expect(hints).toContain('"<label>"\n');
|
||||
expect(hints).toContain('"[<inc>] <label>"\n');
|
||||
});
|
||||
|
||||
it('suggests note positioning', () => {
|
||||
cm.getDoc().setValue('note ');
|
||||
const hints = getHintTexts({line: 0, ch: 5});
|
||||
expect(hints).toEqual([
|
||||
'over ',
|
||||
'left of ',
|
||||
'left: ',
|
||||
'right of ',
|
||||
'right: ',
|
||||
'between ',
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters suggestions', () => {
|
||||
cm.getDoc().setValue('term');
|
||||
const hints = getHintTexts({line: 0, ch: 4});
|
||||
expect(hints).toEqual(['terminators ']);
|
||||
});
|
||||
|
||||
it('suggests known agent names and flags', () => {
|
||||
cm.getDoc().setValue('Foo -> ');
|
||||
const hints = getHintTexts({line: 0, ch: 7});
|
||||
expect(hints).toEqual([
|
||||
'+ ',
|
||||
'- ',
|
||||
'* ',
|
||||
'! ',
|
||||
'Foo ',
|
||||
]);
|
||||
});
|
||||
|
||||
it('only suggests valid flag combinations', () => {
|
||||
cm.getDoc().setValue('Foo -> + ');
|
||||
const hints = getHintTexts({line: 0, ch: 10});
|
||||
expect(hints).toContain('* ');
|
||||
expect(hints).not.toContain('! ');
|
||||
expect(hints).not.toContain('+ ');
|
||||
expect(hints).not.toContain('- ');
|
||||
expect(hints).toContain('Foo ');
|
||||
});
|
||||
|
||||
it('suggests known agent names at the start of lines', () => {
|
||||
cm.getDoc().setValue('Foo -> Bar\n');
|
||||
const hints = getHintTexts({line: 1, ch: 0});
|
||||
expect(hints).toContain('Foo ');
|
||||
expect(hints).toContain('Bar ');
|
||||
});
|
||||
|
||||
it('suggests known labels', () => {
|
||||
cm.getDoc().setValue('Abc:\nsimultaneously with ');
|
||||
const hints = getHintTexts({line: 1, ch: 20});
|
||||
expect(hints).toEqual(['Abc ']);
|
||||
});
|
||||
|
||||
it('suggests known themes', () => {
|
||||
cm.getDoc().setValue('theme ');
|
||||
const hints = getHintTexts({line: 0, ch: 6});
|
||||
expect(hints).toEqual(['Theme\n', 'Other Theme\n']);
|
||||
});
|
||||
|
||||
it('suggests filtered multi-word themes', () => {
|
||||
cm.getDoc().setValue('theme Other ');
|
||||
const hints = getHintTexts({line: 0, ch: 12});
|
||||
expect(hints).toContain('Other Theme\n');
|
||||
expect(hints).not.toContain('Theme\n');
|
||||
});
|
||||
|
||||
it('suggests multi-word agents', () => {
|
||||
cm.getDoc().setValue('Zig Zag -> Meh\nFoo Bar -> ');
|
||||
const hints = getHintTexts({line: 1, ch: 11});
|
||||
expect(hints).toContain('Zig Zag ');
|
||||
expect(hints).toContain('Meh ');
|
||||
expect(hints).toContain('Foo Bar ');
|
||||
});
|
||||
|
||||
it('suggests filtered multi-word agents', () => {
|
||||
cm.getDoc().setValue('Zig Zag -> Meh\nFoo Bar -> Foo ');
|
||||
const hints = getHintTexts({line: 1, ch: 15});
|
||||
expect(hints).toContain('Foo Bar ');
|
||||
expect(hints).not.toContain('Zig Zag ');
|
||||
expect(hints).not.toContain('Meh ');
|
||||
});
|
||||
|
||||
it('suggests quoted names where required', () => {
|
||||
cm.getDoc().setValue('"Zig -> Zag" -> ');
|
||||
const hints = getHintTexts({line: 0, ch: 16});
|
||||
expect(hints).toContain('"Zig -> Zag" ');
|
||||
});
|
||||
|
||||
it('filters quoted names ignoring quotes', () => {
|
||||
cm.getDoc().setValue('"Zig -> Zag" -> Zig');
|
||||
let hints = getHintTexts({line: 0, ch: 19});
|
||||
expect(hints).toContain('"Zig -> Zag" ');
|
||||
|
||||
cm.getDoc().setValue('"Zig -> Zag" -> Zag');
|
||||
hints = getHintTexts({line: 0, ch: 19});
|
||||
expect(hints).not.toContain('"Zig -> Zag" ');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ define([
|
|||
'sequence/LabelPatternParser_spec',
|
||||
'sequence/Generator_spec',
|
||||
'sequence/Renderer_spec',
|
||||
'sequence/CodeMirrorMode_spec',
|
||||
'sequence/themes/Basic_spec',
|
||||
'sequence/themes/Monospace_spec',
|
||||
'sequence/themes/Chunky_spec',
|
||||
|
|
6
test.htm
6
test.htm
|
@ -60,6 +60,12 @@
|
|||
<meta name="cdn-cm/addon/edit/trailingspace" content="stubs/codemirror-trailingspace">
|
||||
<meta name="cdn-cm/addon/comment/comment" content="stubs/codemirror-comment">
|
||||
|
||||
<meta
|
||||
name="cdn-cm/lib/codemirror-real"
|
||||
content="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.31.0/codemirror.min.js"
|
||||
data-integrity="sha256-eue5ceZRwKVQ1OXOZSyU7MXCTZMlqsPi/TOIqh1Vlzo="
|
||||
>
|
||||
|
||||
<!-- test files defined in scripts/specs.js -->
|
||||
|
||||
</head>
|
||||
|
|
Loading…
Reference in New Issue