define(['core/ArrayUtilities'], (array) => { 'use strict'; const CM_END = {type: '', suggest: '\n', then: {}}; const CM_HIDDEN_END = {type: '', then: {}}; const CM_ERROR = {type: 'error line-error', then: {'': 0}}; function makeCMCommaBlock(type, suggest, exits = {}) { return {type, suggest, then: Object.assign({ '': 0, ',': {type: 'operator', suggest: true, then: { '': 1, }}, }, exits)}; } 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_TO_OPTTEXT = {type: 'variable', suggest: 'Agent', then: { '': 0, ':': {type: 'operator', suggest: true, then: { '': CM_TEXT_TO_END, '\n': CM_HIDDEN_END, }}, '\n': CM_END, }}; function makeCMSideNote(side) { return { type: 'keyword', suggest: [side + ' of ', 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', suggest: true, then: { '+': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, '-': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, '': CM_AGENT_TO_OPTTEXT, }}; const CM_CONNECT_FULL = {type: 'variable', suggest: 'Agent', then: { '->': CM_CONNECT, '-->': CM_CONNECT, '<-': CM_CONNECT, '<--': CM_CONNECT, '<->': CM_CONNECT, '<-->': CM_CONNECT, ':': {type: 'operator', suggest: true, override: 'Label', then: {}}, '': 0, }}; const CM_COMMANDS = {type: 'error line-error', then: { 'title': {type: 'keyword', suggest: true, then: { '': CM_TEXT_TO_END, }}, '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, }}, '\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': makeCMSideNote('left'), 'right': makeCMSideNote('right'), 'between': {type: 'keyword', suggest: true, then: { '': CM_AGENT_LIST_TO_TEXT, }}, }}, 'state': {type: 'keyword', suggest: 'state over ', then: { 'over': {type: 'keyword', suggest: true, then: { '': CM_AGENT_LIST_TO_TEXT, }}, }}, 'text': {type: 'keyword', suggest: true, then: { 'left': makeCMSideNote('left'), 'right': makeCMSideNote('right'), }}, '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: {}}, }}, }}, }}, '+': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, '-': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, '': CM_CONNECT_FULL, }}; 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 !== '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) { const suggestions = { type: '', value: '', }; let current = CM_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) { this.tokenDefinitions = tokenDefinitions; this.lineComment = '#'; } startState() { return { currentType: -1, current: '', currentQuoted: false, knownAgent: [], knownLabel: [], beginCompletions: cmMakeCompletions({}, [CM_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()); } _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); 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; } }; });