SequenceDiagram/scripts/sequence/CodeMirrorMode.js

457 lines
11 KiB
JavaScript

define(['core/ArrayUtilities'], (array) => {
'use strict';
const CM_ERROR = {type: 'error line-error', then: {'': 0}};
const makeCommands = ((() => {
const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', then: {}};
const textToEnd = {type: 'string', then: {'': 0, '\n': end}};
const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
'': 0,
'as': {type: 'keyword', suggest: true, then: {
'': {type: 'variable', suggest: 'Agent', then: {
'': 0,
',': {type: 'operator', suggest: true, then: {'': 3}},
'\n': end,
}},
}},
',': {type: 'operator', suggest: true, then: {'': 1}},
'\n': end,
}};
const agentListToText = {type: 'variable', suggest: 'Agent', then: {
'': 0,
',': {type: 'operator', suggest: true, then: {'': 1}},
':': {type: 'operator', suggest: true, then: {'': textToEnd}},
}};
const agentList2ToText = {type: 'variable', suggest: 'Agent', then: {
'': 0,
',': {type: 'operator', suggest: true, then: {'': agentListToText}},
':': CM_ERROR,
}};
const singleAgentToText = {type: 'variable', suggest: 'Agent', then: {
'': 0,
',': CM_ERROR,
':': {type: 'operator', suggest: true, then: {'': textToEnd}},
}};
const agentToOptText = {type: 'variable', suggest: 'Agent', then: {
'': 0,
':': {type: 'operator', suggest: true, then: {
'': textToEnd,
'\n': hiddenEnd,
}},
'\n': end,
}};
function makeSideNote(side) {
return {
type: 'keyword',
suggest: [side + ' of ', side + ': '],
then: {
'of': {type: 'keyword', suggest: true, then: {
'': agentListToText,
}},
':': {type: 'operator', suggest: true, then: {
'': textToEnd,
}},
'': agentListToText,
},
};
}
function makeOpBlock(exit) {
const op = {type: 'operator', suggest: true, then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}};
return {
'+': {type: 'operator', suggest: true, then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': CM_ERROR,
'': exit,
}},
'-': {type: 'operator', suggest: true, then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}},
'': exit,
}},
'*': {type: 'operator', suggest: true, then: {
'+': op,
'-': op,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}},
'!': op,
'': exit,
};
}
function makeCMConnect(arrows) {
const connect = {
type: 'keyword',
suggest: true,
then: makeOpBlock(agentToOptText),
};
const then = {
':': {
type: 'operator',
suggest: true,
override: 'Label',
then: {},
},
'': 0,
};
arrows.forEach((arrow) => (then[arrow] = connect));
return makeOpBlock({type: 'variable', suggest: 'Agent', then});
}
const BASE_THEN = {
'title': {type: 'keyword', suggest: true, then: {
'': textToEnd,
}},
'theme': {type: 'keyword', suggest: true, then: {
'': {
type: 'string',
suggest: {
global: 'themes',
suffix: '\n',
},
then: {
'': 0,
'\n': end,
},
},
}},
'headers': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}},
'cross': {type: 'keyword', suggest: true, then: {}},
'box': {type: 'keyword', suggest: true, then: {}},
'fade': {type: 'keyword', suggest: true, then: {}},
'bar': {type: 'keyword', suggest: true, 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: {}},
'fade': {type: 'keyword', suggest: true, then: {}},
'bar': {type: 'keyword', suggest: true, then: {}},
}},
'define': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd,
'as': CM_ERROR,
}},
'begin': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd,
'as': CM_ERROR,
}},
'end': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd,
'as': CM_ERROR,
'\n': end,
}},
'if': {type: 'keyword', suggest: true, then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
'': textToEnd,
}},
'\n': end,
}},
'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
'if': {type: 'keyword', suggest: 'if: ', then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
'': textToEnd,
}},
}},
'\n': end,
}},
'repeat': {type: 'keyword', suggest: true, then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
'': textToEnd,
}},
'\n': end,
}},
'note': {type: 'keyword', suggest: true, then: {
'over': {type: 'keyword', suggest: true, then: {
'': agentListToText,
}},
'left': makeSideNote('left'),
'right': makeSideNote('right'),
'between': {type: 'keyword', suggest: true, then: {
'': agentList2ToText,
}},
}},
'state': {type: 'keyword', suggest: 'state over ', then: {
'over': {type: 'keyword', suggest: true, then: {
'': singleAgentToText,
}},
}},
'text': {type: 'keyword', suggest: true, then: {
'left': makeSideNote('left'),
'right': makeSideNote('right'),
}},
'autolabel': {type: 'keyword', suggest: true, then: {
'off': {type: 'keyword', suggest: true, then: {}},
'': textToEnd,
}},
'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', suggest: true, then: {}},
}},
}},
}},
};
return (arrows) => {
return {
type: 'error line-error',
then: Object.assign(BASE_THEN, makeCMConnect(arrows)),
};
};
})());
function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) {
return token + ' ';
} else {
return token + '\n';
}
}
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, previous, current) {
if(token === '') {
return cmGetVarSuggestions(state, previous, current);
} else if(current.suggest === true) {
return [cmCappedToken(token, current)];
} else if(Array.isArray(current.suggest)) {
return current.suggest;
} else if(current.suggest) {
return [current.suggest];
} else {
return null;
}
}
function cmMakeCompletions(state, path) {
const comp = [];
const current = array.last(path);
Object.keys(current.then).forEach((token) => {
let next = current.then[token];
if(typeof next === 'number') {
next = path[path.length - next - 1];
}
array.mergeSets(
comp,
cmGetSuggestions(state, token, current, 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, commands) {
const suggestions = {
type: '',
value: '',
};
let current = commands;
const path = [current];
state.line.forEach((token, i) => {
if(i === state.line.length - 1) {
state.completions = cmMakeCompletions(state, path);
}
const keywordToken = token.q ? '' : token.v;
const found = current.then[keywordToken] || current.then[''];
if(typeof found === 'number') {
path.length -= found;
} else {
path.push(found || CM_ERROR);
}
current = array.last(path);
updateSuggestion(state, suggestions, token.v, current);
});
if(eol) {
updateSuggestion(state, suggestions, '', {});
}
state.nextCompletions = cmMakeCompletions(state, path);
state.valid = (
Boolean(current.then['\n']) ||
Object.keys(current.then).length === 0
);
return current.type;
}
function getInitialToken(block) {
const baseToken = (block.baseToken || {});
return {
value: baseToken.v || '',
quoted: baseToken.q || false,
};
}
return class Mode {
constructor(tokenDefinitions, arrows) {
this.tokenDefinitions = tokenDefinitions;
this.commands = makeCommands(arrows);
this.lineComment = '#';
}
startState() {
return {
currentType: -1,
current: '',
currentQuoted: false,
knownAgent: [],
knownLabel: [],
beginCompletions: cmMakeCompletions({}, [this.commands]),
completions: [],
nextCompletions: [],
valid: true,
line: [],
indent: 0,
};
}
_matchPattern(stream, pattern, consume) {
if(!pattern) {
return null;
}
pattern.lastIndex = 0;
return stream.match(pattern, consume);
}
_tokenBegin(stream, state) {
while(true) {
if(stream.eol()) {
return false;
}
for(let i = 0; i < this.tokenDefinitions.length; ++ i) {
const block = this.tokenDefinitions[i];
if(this._matchPattern(stream, block.start, true)) {
state.currentType = i;
const {value, quoted} = getInitialToken(block);
state.current = value;
state.currentQuoted = quoted;
return true;
}
}
stream.next();
}
}
_tokenCheckEscape(stream, state, block) {
const match = this._matchPattern(stream, block.escape, true);
if(match) {
state.current += block.escapeWith(match);
}
}
_tokenEndFound(stream, state, block) {
state.currentType = -1;
if(block.omit) {
return 'comment';
}
state.line.push({v: state.current, q: state.currentQuoted});
return cmCheckToken(state, stream.eol(), this.commands);
}
_tokenEOLFound(stream, state, block) {
state.current += '\n';
if(block.omit) {
return 'comment';
}
state.line.push(({v: state.current, q: state.currentQuoted}));
const type = cmCheckToken(state, false, this.commands);
state.line.pop();
return type;
}
_tokenEnd(stream, state) {
while(true) {
const block = this.tokenDefinitions[state.currentType];
this._tokenCheckEscape(stream, state, block);
if(!block.end || this._matchPattern(stream, block.end, true)) {
return this._tokenEndFound(stream, state, block);
}
if(stream.eol()) {
return this._tokenEOLFound(stream, state, block);
}
state.current += stream.next();
}
}
token(stream, state) {
state.completions = state.nextCompletions;
if(stream.sol() && state.currentType === -1) {
state.line.length = 0;
}
let type = '';
if(state.currentType !== -1 || this._tokenBegin(stream, state)) {
type = this._tokenEnd(stream, state);
}
if(state.currentType === -1 && stream.eol() && !state.valid) {
return 'line-error ' + type;
} else {
return type;
}
}
indent(state) {
return state.indent;
}
};
});